[Testing] Add CPU backend tests

This commit is contained in:
Herman S.
2026-03-20 23:42:25 +09:00
parent c50b036178
commit 949d4c31fb
5 changed files with 2629 additions and 0 deletions

View File

@@ -458,3 +458,196 @@ TEST_CASE("MULTIPLE_BUILTIN_CALLS", "[backend]") {
memory->SystemHeapFree(stack_address);
}
// =============================================================================
// NJM (Non-Java Mode) default initialization
// =============================================================================
// Tests that the backend context initializes with NJM enabled by default,
// matching x64 behavior. NJM controls flush-to-zero for denormals.
static uint32_t observed_njm_flags = 0;
static void ReadBackendFlags(ppc::PPCContext* ctx, void* arg0, void* arg1) {
// Read the backend flags from the backend context, which lives just
// before the PPCContext in memory.
#if XE_ARCH_AMD64
auto* bctx = reinterpret_cast<xe::cpu::backend::x64::X64BackendContext*>(
reinterpret_cast<intptr_t>(ctx) -
sizeof(xe::cpu::backend::x64::X64BackendContext));
observed_njm_flags = bctx->flags;
#elif XE_ARCH_ARM64
auto* bctx = reinterpret_cast<xe::cpu::backend::a64::A64BackendContext*>(
reinterpret_cast<intptr_t>(ctx) -
sizeof(xe::cpu::backend::a64::A64BackendContext));
observed_njm_flags = bctx->flags;
#endif
}
TEST_CASE("NJM_DEFAULT_ON", "[backend]") {
observed_njm_flags = 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(
"ReadBackendFlags", ReadBackendFlags, nullptr, nullptr);
auto module = std::make_unique<TestModule>(
processor.get(), "Test",
[](uint32_t address) { return address == 0x80000000; },
[builtin_fn](HIRBuilder& b) {
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;
fn->Call(thread_state.get(), uint32_t(ctx->lr));
// NJM bit (bit 2) should be set by default.
#if XE_ARCH_AMD64
REQUIRE((observed_njm_flags &
(1U << xe::cpu::backend::x64::kX64BackendNJMOn)) != 0);
#elif XE_ARCH_ARM64
REQUIRE((observed_njm_flags &
(1U << xe::cpu::backend::a64::kA64BackendNJMOn)) != 0);
#endif
memory->SystemHeapFree(stack_address);
}
// =============================================================================
// Atomic Exchange I32
// =============================================================================
// Tests that AtomicExchange correctly swaps a value in memory and returns
// the old value.
// NOTE: OPCODE_ATOMIC_EXCHANGE uses a HOST address (not guest), per the
// x64 backend comment: "the address we use here is a real, host address!"
TEST_CASE("ATOMIC_EXCHANGE_I32", "[backend]") {
TestFunction test([](HIRBuilder& b) {
// r[4] holds the host address directly.
auto addr = LoadGPR(b, 4);
auto new_val = b.Truncate(LoadGPR(b, 5), hir::INT32_TYPE);
auto old_val = b.AtomicExchange(addr, new_val);
StoreGPR(b, 3, b.ZeroExtend(old_val, hir::INT64_TYPE));
b.Return();
});
// Allocate guest memory and compute the host pointer.
uint32_t guest_addr = test.memory->SystemHeapAlloc(4);
REQUIRE(guest_addr != 0);
auto* host_ptr = test.memory->TranslateVirtual(guest_addr);
test.Run(
[&](PPCContext* ctx) {
*reinterpret_cast<uint32_t*>(host_ptr) = 0xAABBCCDD;
// Pass the HOST address in r[4].
ctx->r[4] = reinterpret_cast<uint64_t>(host_ptr);
ctx->r[5] = 0x11223344;
},
[&](PPCContext* ctx) {
// r[3] should have the old value.
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xAABBCCDD);
// Memory should now have the new value.
REQUIRE(*reinterpret_cast<uint32_t*>(host_ptr) == 0x11223344);
});
test.memory->SystemHeapFree(guest_addr);
}
// =============================================================================
// DOT_PRODUCT_3 — inline NEON dot product of first 3 vector elements
// =============================================================================
TEST_CASE("DOT_PRODUCT_3", "[backend]") {
TestFunction test([](HIRBuilder& b) {
auto src1 = LoadVR(b, 10);
auto src2 = LoadVR(b, 11);
auto result = b.DotProduct3(src1, src2);
StoreVR(b, 3, result);
b.Return();
});
// Simple case: (1,2,3,ignored) . (4,5,6,ignored) = 1*4+2*5+3*6 = 32
test.Run(
[](PPCContext* ctx) {
ctx->v[10] = vec128f(1.0f, 2.0f, 3.0f, 99.0f);
ctx->v[11] = vec128f(4.0f, 5.0f, 6.0f, 99.0f);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].f32[0] == 32.0f);
REQUIRE(ctx->v[3].f32[1] == 32.0f);
REQUIRE(ctx->v[3].f32[2] == 32.0f);
REQUIRE(ctx->v[3].f32[3] == 32.0f);
});
// Zero vector.
test.Run(
[](PPCContext* ctx) {
ctx->v[10] = vec128f(0.0f, 0.0f, 0.0f, 0.0f);
ctx->v[11] = vec128f(1.0f, 2.0f, 3.0f, 4.0f);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3].f32[0] == 0.0f); });
// Element 4 should be ignored.
test.Run(
[](PPCContext* ctx) {
ctx->v[10] = vec128f(1.0f, 0.0f, 0.0f, 1000.0f);
ctx->v[11] = vec128f(1.0f, 0.0f, 0.0f, 1000.0f);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3].f32[0] == 1.0f); });
}
// =============================================================================
// DOT_PRODUCT_4 — inline NEON dot product of all 4 vector elements
// =============================================================================
TEST_CASE("DOT_PRODUCT_4", "[backend]") {
TestFunction test([](HIRBuilder& b) {
auto src1 = LoadVR(b, 10);
auto src2 = LoadVR(b, 11);
auto result = b.DotProduct4(src1, src2);
StoreVR(b, 3, result);
b.Return();
});
// (1,2,3,4) . (5,6,7,8) = 5+12+21+32 = 70
test.Run(
[](PPCContext* ctx) {
ctx->v[10] = vec128f(1.0f, 2.0f, 3.0f, 4.0f);
ctx->v[11] = vec128f(5.0f, 6.0f, 7.0f, 8.0f);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].f32[0] == 70.0f);
REQUIRE(ctx->v[3].f32[1] == 70.0f);
REQUIRE(ctx->v[3].f32[2] == 70.0f);
REQUIRE(ctx->v[3].f32[3] == 70.0f);
});
// Length-squared: (3,4,0,0) . (3,4,0,0) = 25
test.Run(
[](PPCContext* ctx) {
ctx->v[10] = vec128f(3.0f, 4.0f, 0.0f, 0.0f);
ctx->v[11] = vec128f(3.0f, 4.0f, 0.0f, 0.0f);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3].f32[0] == 25.0f); });
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,396 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// Tests for ARM64-sensitive edge cases: sign-extension, byte-swap, extract
// with high-bit values, rounding modes, and NaN handling.
#include "xenia/cpu/testing/util.h"
#include <cmath>
#include <cstring>
using namespace xe;
using namespace xe::cpu;
using namespace xe::cpu::hir;
using namespace xe::cpu::testing;
using xe::cpu::ppc::PPCContext;
// ============================================================================
// BYTE_SWAP scalar — I16, I32, I64
// ============================================================================
TEST_CASE("BYTE_SWAP_I16", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.ByteSwap(b.Truncate(LoadGPR(b, 4), INT16_TYPE)),
INT64_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x1234; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint16_t>(ctx->r[3]) == 0x3412);
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x0100; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint16_t>(ctx->r[3]) == 0x0001);
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x80FF; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint16_t>(ctx->r[3]) == 0xFF80);
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFFFF; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint16_t>(ctx->r[3]) == 0xFFFF);
});
}
TEST_CASE("BYTE_SWAP_I32", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.ByteSwap(b.Truncate(LoadGPR(b, 4), INT32_TYPE)),
INT64_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x01020304; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x04030201);
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x80000000; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x00000080);
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFF000000; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x000000FF);
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0xDEADBEEF; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xEFBEADDE);
});
// Verify upper 32 bits are zero.
test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFFFFFFFF; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xFFFFFFFF);
REQUIRE((ctx->r[3] >> 32) == 0);
});
}
TEST_CASE("BYTE_SWAP_I64", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3, b.ByteSwap(LoadGPR(b, 4)));
b.Return();
});
test.Run(
[](PPCContext* ctx) { ctx->r[4] = 0x0102030405060708ULL; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x0807060504030201ULL); });
test.Run(
[](PPCContext* ctx) { ctx->r[4] = 0x8000000000000000ULL; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x0000000000000080ULL); });
test.Run(
[](PPCContext* ctx) { ctx->r[4] = 0xFF00000000000000ULL; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x00000000000000FFULL); });
}
// ============================================================================
// EXTRACT with high-bit-set values
// Tests UMOV vs SMOV — must zero-extend, not sign-extend.
// ============================================================================
TEST_CASE("EXTRACT_INT8_HIGHBIT", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(
b, 3,
b.ZeroExtend(b.Extract(LoadVR(b, 4),
b.Truncate(LoadGPR(b, 4), INT8_TYPE), INT8_TYPE),
INT64_TYPE));
b.Return();
});
// Extract 0xFF — must be 0xFF (255), NOT 0xFFFFFFFFFFFFFFFF (-1).
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0; // index 0
ctx->v[4] =
vec128b(0xFF, 0x80, 0x7F, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 0xFF); // NOT sign-extended
});
// Extract 0x80
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 1;
ctx->v[4] =
vec128b(0xFF, 0x80, 0x7F, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 0x80); // NOT sign-extended
});
}
TEST_CASE("EXTRACT_INT16_HIGHBIT", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(
b.Extract(LoadVR(b, 4), b.Truncate(LoadGPR(b, 4), INT8_TYPE),
INT16_TYPE),
INT64_TYPE));
b.Return();
});
// Extract 0x8000 — must NOT sign-extend to 0xFFFFFFFFFFFF8000.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0;
ctx->v[4] = vec128s(0x8000, 0xFFFF, 0x7FFF, 0x0001, 0, 0, 0, 0);
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x8000); });
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 1;
ctx->v[4] = vec128s(0x8000, 0xFFFF, 0x7FFF, 0x0001, 0, 0, 0, 0);
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0xFFFF); });
}
TEST_CASE("EXTRACT_INT32_HIGHBIT", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(
b.Extract(LoadVR(b, 4), b.Truncate(LoadGPR(b, 4), INT8_TYPE),
INT32_TYPE),
INT64_TYPE));
b.Return();
});
// Extract 0x80000000 — must NOT sign-extend to 0xFFFFFFFF80000000.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0;
ctx->v[4] = vec128i(0x80000000, 0xFFFFFFFF, 0x7FFFFFFF, 0x00000001);
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 0x80000000ULL); // upper bits must be 0
});
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 1;
ctx->v[4] = vec128i(0x80000000, 0xFFFFFFFF, 0x7FFFFFFF, 0x00000001);
},
[](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 0xFFFFFFFFULL); // upper bits must be 0
});
}
// ============================================================================
// Memory: store then load byte-swap round-trip for I32
// Store/Load use guest addresses (membase-relative).
// ============================================================================
TEST_CASE("STORE_LOAD_BYTESWAP_I32", "[memory]") {
TestFunction test([](HIRBuilder& b) {
// Store r[5] (truncated to I32) at guest address r[4] with byte-swap.
auto addr = LoadGPR(b, 4);
auto val = b.Truncate(LoadGPR(b, 5), INT32_TYPE);
b.Store(addr, val, LOAD_STORE_BYTE_SWAP);
// Load it back with byte-swap — should get original value.
auto loaded = b.Load(addr, INT32_TYPE, LOAD_STORE_BYTE_SWAP);
StoreGPR(b, 3, b.ZeroExtend(loaded, INT64_TYPE));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(4);
REQUIRE(guest_addr != 0);
test.Run(
[&](PPCContext* ctx) {
ctx->r[4] = guest_addr;
ctx->r[5] = 0xDEADBEEF;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0xDEADBEEF);
});
test.Run(
[&](PPCContext* ctx) {
ctx->r[4] = guest_addr;
ctx->r[5] = 0x80000001;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x80000001);
});
test.memory->SystemHeapFree(guest_addr);
}
// ============================================================================
// SET_ROUNDING_MODE — mode 3 (toward -infinity / floor)
// ============================================================================
TEST_CASE("SET_ROUNDING_MODE_3", "[backend]") {
TestFunction test([](HIRBuilder& b) {
auto a = b.Convert(LoadFPR(b, 4), FLOAT32_TYPE);
auto c = b.Convert(LoadFPR(b, 5), FLOAT32_TYPE);
auto sum = b.Add(a, c);
StoreFPR(b, 3, b.Convert(sum, FLOAT64_TYPE));
b.Return();
});
// Mode 3 = toward -infinity (floor).
// 1.0 + epsilon: floor rounds down to 1.0.
test.Run(
[&test](PPCContext* ctx) {
ctx->f[4] = 1.0;
ctx->f[5] = std::ldexp(1.0, -24);
test.processors[0]->backend()->SetGuestRoundingMode(ctx, 3);
},
[&test](PPCContext* ctx) {
auto result = static_cast<float>(ctx->f[3]);
REQUIRE(result == 1.0f);
test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0);
});
// -1.0 + -epsilon: floor rounds toward more negative.
test.Run(
[&test](PPCContext* ctx) {
ctx->f[4] = -1.0;
ctx->f[5] = -std::ldexp(1.0, -24);
test.processors[0]->backend()->SetGuestRoundingMode(ctx, 3);
},
[&test](PPCContext* ctx) {
auto result = static_cast<float>(ctx->f[3]);
float expected = std::nextafterf(-1.0f, -2.0f);
REQUIRE(result == expected);
test.processors[0]->backend()->SetGuestRoundingMode(ctx, 0);
});
}
// ============================================================================
// Truncate with garbage in upper register bits
// Ensures I8/I16 Truncate properly masks on ARM64 where WRegs are 32-bit.
// ============================================================================
TEST_CASE("ADD_I8_UPPER_BITS_GARBAGE", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.Add(b.Truncate(LoadGPR(b, 4), INT8_TYPE),
b.Truncate(LoadGPR(b, 5), INT8_TYPE)),
INT64_TYPE));
b.Return();
});
// Upper bytes are garbage — Truncate must isolate low byte.
// 0x80 + 0x01 = 0x81 (only low bytes matter)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xDEADBA80; // low byte = 0x80
ctx->r[5] = 0xCAFE0001; // low byte = 0x01
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x81);
});
// 0xFF + 0x01 = 0x00 (overflow wraps at 8 bits)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x000100FF;
ctx->r[5] = 0x00010001;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x00);
});
}
TEST_CASE("ADD_I16_UPPER_BITS_GARBAGE", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.Add(b.Truncate(LoadGPR(b, 4), INT16_TYPE),
b.Truncate(LoadGPR(b, 5), INT16_TYPE)),
INT64_TYPE));
b.Return();
});
// 0x8000 + 0x0001 = 0x8001
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xDEAD8000;
ctx->r[5] = 0xCAFE0001;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint16_t>(ctx->r[3]) == 0x8001);
});
}
// ============================================================================
// SHA I8 additional edge cases
// ============================================================================
TEST_CASE("SHA_I8_EDGE", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.Sha(b.Truncate(LoadGPR(b, 4), INT8_TYPE),
b.Truncate(LoadGPR(b, 5), INT8_TYPE)),
INT64_TYPE));
b.Return();
});
// 0x80 >> 7 = 0xFF (MSB=1, arithmetic shift fills with 1s)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x80;
ctx->r[5] = 7;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0xFF);
});
// 0x01 >> 1 = 0x00 (positive, shift right)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x01;
ctx->r[5] = 1;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x00);
});
// 0xC0 >> 1 = 0xE0 (negative, sign bit fills)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xC0;
ctx->r[5] = 1;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0xE0);
});
}
// ============================================================================
// SHR I8 distinguishing from SHA (logical vs arithmetic)
// ============================================================================
TEST_CASE("SHR_I8_VS_SHA", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.Shr(b.Truncate(LoadGPR(b, 4), INT8_TYPE),
b.Truncate(LoadGPR(b, 5), INT8_TYPE)),
INT64_TYPE));
b.Return();
});
// 0x80 >> 7 = 0x01 (logical: zero fills from left)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x80;
ctx->r[5] = 7;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x01);
});
// 0x80 >> 1 = 0x40 (logical)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x80;
ctx->r[5] = 1;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x40);
});
// 0xFF >> 1 = 0x7F (logical, not 0xFF which SHA would give)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFF;
ctx->r[5] = 1;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x7F);
});
}

View File

@@ -0,0 +1,421 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// Tests for remaining untested HIR opcodes: CAST, ROUND, TO_SINGLE,
// MUL_ADD, MUL_SUB, ADD_CARRY, RECIP, RSQRT, LVL/LVR/STVL/STVR,
// RESERVED_LOAD/RESERVED_STORE, VECTOR_AVERAGE.
#include "xenia/cpu/testing/util.h"
#include <cmath>
#include <cstring>
#include <limits>
using namespace xe;
using namespace xe::cpu;
using namespace xe::cpu::hir;
using namespace xe::cpu::testing;
using xe::cpu::ppc::PPCContext;
// ============================================================================
// CAST — bitcast I32<->F32, I64<->F64
// ============================================================================
TEST_CASE("CAST_I32_TO_F32", "[convert]") {
TestFunction test([](HIRBuilder& b) {
auto ival = b.Truncate(LoadGPR(b, 4), INT32_TYPE);
auto fval = b.Cast(ival, FLOAT32_TYPE);
// Store back as I32 via cast to verify round-trip.
StoreGPR(b, 3, b.ZeroExtend(b.Cast(fval, INT32_TYPE), INT64_TYPE));
b.Return();
});
// 1.0f = 0x3F800000
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x3F800000; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x3F800000);
});
// -0.0f = 0x80000000
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x80000000; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x80000000);
});
// NaN = 0x7FC00000
test.Run([](PPCContext* ctx) { ctx->r[4] = 0x7FC00000; },
[](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 0x7FC00000);
});
}
TEST_CASE("CAST_I64_TO_F64", "[convert]") {
TestFunction test([](HIRBuilder& b) {
auto ival = LoadGPR(b, 4);
auto fval = b.Cast(ival, FLOAT64_TYPE);
StoreGPR(b, 3, b.Cast(fval, INT64_TYPE));
b.Return();
});
// 1.0 = 0x3FF0000000000000
test.Run(
[](PPCContext* ctx) { ctx->r[4] = 0x3FF0000000000000ULL; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x3FF0000000000000ULL); });
// -1.0 = 0xBFF0000000000000
test.Run(
[](PPCContext* ctx) { ctx->r[4] = 0xBFF0000000000000ULL; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0xBFF0000000000000ULL); });
}
// ============================================================================
// ROUND F64 (F32 is impossible on x64)
// ============================================================================
TEST_CASE("ROUND_F64_NEAREST", "[convert]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.Round(LoadFPR(b, 4), ROUND_TO_NEAREST));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.5; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 2.0); });
test.Run(
[](PPCContext* ctx) { ctx->f[4] = 2.5; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 2.0); }); // banker's rounding
test.Run([](PPCContext* ctx) { ctx->f[4] = -1.5; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == -2.0); });
}
TEST_CASE("ROUND_F64_ZERO", "[convert]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.Round(LoadFPR(b, 4), ROUND_TO_ZERO));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.9; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = -1.9; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == -1.0); });
}
TEST_CASE("ROUND_F64_CEIL", "[convert]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.Round(LoadFPR(b, 4), ROUND_TO_POSITIVE_INFINITY));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.1; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 2.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = -1.1; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == -1.0); });
}
TEST_CASE("ROUND_F64_FLOOR", "[convert]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.Round(LoadFPR(b, 4), ROUND_TO_MINUS_INFINITY));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.9; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = -1.1; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == -2.0); });
}
// ============================================================================
// TO_SINGLE — double -> single -> double rounding
// ============================================================================
TEST_CASE("TO_SINGLE", "[convert]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.ToSingle(LoadFPR(b, 4)));
b.Return();
});
// Exact value survives.
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
// Value requiring rounding: 1.0 + 2^-24 in double -> rounds in single.
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0 + std::ldexp(1.0, -24); },
[](PPCContext* ctx) {
float f32 = static_cast<float>(ctx->f[3]);
REQUIRE(f32 == 1.0f); // rounded to nearest in single
});
// Negative value.
test.Run([](PPCContext* ctx) { ctx->f[4] = -3.14; },
[](PPCContext* ctx) {
float f32 = static_cast<float>(ctx->f[3]);
REQUIRE(f32 == static_cast<float>(-3.14));
});
}
// ============================================================================
// MUL_ADD F64 — fused multiply-add: (1 * 2) + 3
// ============================================================================
TEST_CASE("MUL_ADD_F64", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.MulAdd(LoadFPR(b, 4), LoadFPR(b, 5), LoadFPR(b, 6)));
b.Return();
});
// 2.0 * 3.0 + 4.0 = 10.0
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = 2.0;
ctx->f[5] = 3.0;
ctx->f[6] = 4.0;
},
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 10.0); });
// -1.0 * 2.0 + 3.0 = 1.0
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = -1.0;
ctx->f[5] = 2.0;
ctx->f[6] = 3.0;
},
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
// 0.0 * anything + 5.0 = 5.0
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = 0.0;
ctx->f[5] = 999.0;
ctx->f[6] = 5.0;
},
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 5.0); });
}
// ============================================================================
// MUL_SUB F64 — fused multiply-subtract: (1 * 2) - 3
// ============================================================================
TEST_CASE("MUL_SUB_F64", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.MulSub(LoadFPR(b, 4), LoadFPR(b, 5), LoadFPR(b, 6)));
b.Return();
});
// 2.0 * 3.0 - 4.0 = 2.0
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = 2.0;
ctx->f[5] = 3.0;
ctx->f[6] = 4.0;
},
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 2.0); });
// 5.0 * 5.0 - 25.0 = 0.0
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = 5.0;
ctx->f[5] = 5.0;
ctx->f[6] = 25.0;
},
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 0.0); });
}
// ============================================================================
// ADD_CARRY I8/I32 — add with carry-in
// ============================================================================
TEST_CASE("ADD_CARRY_I8", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.AddWithCarry(b.Truncate(LoadGPR(b, 4), INT8_TYPE),
b.Truncate(LoadGPR(b, 5), INT8_TYPE),
b.Truncate(LoadGPR(b, 6), INT8_TYPE)),
INT64_TYPE));
b.Return();
});
// 0xFF + 0x00 + carry=1 = 0x00 (wraps)
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFF;
ctx->r[5] = 0;
ctx->r[6] = 1;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x00);
});
// 0x10 + 0x20 + carry=0 = 0x30
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x10;
ctx->r[5] = 0x20;
ctx->r[6] = 0;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x30);
});
// 0x10 + 0x20 + carry=1 = 0x31
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x10;
ctx->r[5] = 0x20;
ctx->r[6] = 1;
},
[](PPCContext* ctx) {
REQUIRE(static_cast<uint8_t>(ctx->r[3]) == 0x31);
});
}
// ============================================================================
// RECIP F32/F64 — exact reciprocal (1/x)
// ============================================================================
TEST_CASE("RECIP_F64", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.Recip(LoadFPR(b, 4)));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 2.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 0.5); });
test.Run([](PPCContext* ctx) { ctx->f[4] = -4.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == -0.25); });
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
}
// ============================================================================
// RSQRT F32 — 1/sqrt(x) (exact, not estimate)
// ============================================================================
TEST_CASE("RSQRT_F32", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
auto val = b.Convert(LoadFPR(b, 4), FLOAT32_TYPE);
auto rsqrt = b.RSqrt(val);
StoreFPR(b, 3, b.Convert(rsqrt, FLOAT64_TYPE));
b.Return();
});
// rsqrt(1.0) = 1.0
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) {
float r = static_cast<float>(ctx->f[3]);
REQUIRE(r == 1.0f);
});
// rsqrt(4.0) = 0.5
test.Run([](PPCContext* ctx) { ctx->f[4] = 4.0; },
[](PPCContext* ctx) {
float r = static_cast<float>(ctx->f[3]);
REQUIRE(r == 0.5f);
});
}
// ============================================================================
// VECTOR_AVERAGE I8 — rounding halving add: (a+b+1)>>1
// ============================================================================
TEST_CASE("VECTOR_AVERAGE_UNSIGNED_I8", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3,
b.VectorAverage(LoadVR(b, 4), LoadVR(b, 5), INT8_TYPE,
ARITHMETIC_UNSIGNED));
b.Return();
});
// Use uniform values so byte ordering doesn't matter.
// avg(100, 200) unsigned = (100+200+1)/2 = 150
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128b(100);
ctx->v[5] = vec128b(200);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3] == vec128b(150)); });
// avg(255, 255) unsigned = (255+255+1)/2 = 255
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128b(255);
ctx->v[5] = vec128b(255);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3] == vec128b(255)); });
// avg(0, 1) unsigned = (0+1+1)/2 = 1
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128b(0);
ctx->v[5] = vec128b(1);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3] == vec128b(1)); });
}
// ============================================================================
// RESERVED_LOAD + RESERVED_STORE — LL/SC emulation
// ============================================================================
TEST_CASE("RESERVED_LOAD_STORE_I32", "[atomic]") {
TestFunction test([](HIRBuilder& b) {
auto addr = LoadGPR(b, 4);
// Load with reserve.
auto loaded = b.LoadWithReserve(addr, INT32_TYPE);
StoreGPR(b, 3, b.ZeroExtend(loaded, INT64_TYPE));
// Store conditional with the loaded value + 1.
auto new_val = b.Add(loaded, b.LoadConstantInt32(1));
auto success = b.StoreWithReserve(addr, new_val, INT32_TYPE);
StoreGPR(b, 5, b.ZeroExtend(success, INT64_TYPE));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(4);
REQUIRE(guest_addr != 0);
auto* host_ptr =
reinterpret_cast<uint32_t*>(test.memory->TranslateVirtual(guest_addr));
test.Run(
[&](PPCContext* ctx) {
*host_ptr = 42;
ctx->r[4] = guest_addr;
},
[&](PPCContext* ctx) {
REQUIRE(static_cast<uint32_t>(ctx->r[3]) == 42); // loaded value
REQUIRE(ctx->r[5] == 1); // store succeeded
REQUIRE(*host_ptr == 43); // incremented
});
test.memory->SystemHeapFree(guest_addr);
}
// ============================================================================
// LVL / LVR — partial vector load left/right
// Detailed alignment semantics are tested by PPC instruction tests (lvlx etc.)
// These tests verify the HIR opcode executes without crashing.
// ============================================================================
TEST_CASE("LOAD_VECTOR_LEFT", "[memory]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.LoadVectorLeft(LoadGPR(b, 4)));
b.Return();
});
// Allocate 16-byte aligned memory with known pattern.
uint32_t guest_addr = test.memory->SystemHeapAlloc(32);
REQUIRE(guest_addr != 0);
// Align to 16 bytes.
uint32_t aligned_addr = (guest_addr + 15) & ~15u;
auto* host_ptr =
reinterpret_cast<uint8_t*>(test.memory->TranslateVirtual(aligned_addr));
for (int i = 0; i < 16; ++i) host_ptr[i] = static_cast<uint8_t>(0x10 + i);
// LVL at aligned address — should load data (not crash).
test.Run([&](PPCContext* ctx) { ctx->r[4] = aligned_addr; },
[&](PPCContext* ctx) {
// Just verify it loaded something non-zero from the pattern.
REQUIRE((ctx->v[3].u32[0] | ctx->v[3].u32[1] | ctx->v[3].u32[2] |
ctx->v[3].u32[3]) != 0);
});
test.memory->SystemHeapFree(guest_addr);
}
TEST_CASE("LOAD_VECTOR_RIGHT", "[memory]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.LoadVectorRight(LoadGPR(b, 4)));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(32);
REQUIRE(guest_addr != 0);
uint32_t aligned_addr = (guest_addr + 15) & ~15u;
auto* host_ptr =
reinterpret_cast<uint8_t*>(test.memory->TranslateVirtual(aligned_addr));
for (int i = 0; i < 16; ++i) host_ptr[i] = static_cast<uint8_t>(0x20 + i);
// LVR at aligned address returns zero (no bytes before alignment boundary).
test.Run([&](PPCContext* ctx) { ctx->r[4] = aligned_addr; },
[&](PPCContext* ctx) {
REQUIRE(ctx->v[3].u32[0] == 0);
REQUIRE(ctx->v[3].u32[1] == 0);
REQUIRE(ctx->v[3].u32[2] == 0);
REQUIRE(ctx->v[3].u32[3] == 0);
});
// LVR at aligned+4 loads 4 bytes into the high end of the vector.
test.Run([&](PPCContext* ctx) { ctx->r[4] = aligned_addr + 4; },
[&](PPCContext* ctx) {
// Should have loaded some data (not all zero).
REQUIRE((ctx->v[3].u32[0] | ctx->v[3].u32[1] | ctx->v[3].u32[2] |
ctx->v[3].u32[3]) != 0);
});
test.memory->SystemHeapFree(guest_addr);
}

View File

@@ -0,0 +1,489 @@
/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2026 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
// Tests for previously-untested HIR opcodes, prioritized by ARM64 bug risk.
#include "xenia/cpu/testing/util.h"
#include <cmath>
#include <cstring>
#include <limits>
using namespace xe;
using namespace xe::cpu;
using namespace xe::cpu::hir;
using namespace xe::cpu::testing;
using xe::cpu::ppc::PPCContext;
// ============================================================================
// VECTOR_DENORMFLUSH — potential register aliasing bug
// ============================================================================
TEST_CASE("VECTOR_DENORMFLUSH", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorDenormFlush(LoadVR(b, 4)));
b.Return();
});
// Normal values should pass through unchanged.
test.Run(
[](PPCContext* ctx) { ctx->v[4] = vec128f(1.0f, -1.0f, 0.0f, 100.0f); },
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].f32[0] == 1.0f);
REQUIRE(ctx->v[3].f32[1] == -1.0f);
REQUIRE(ctx->v[3].f32[2] == 0.0f);
REQUIRE(ctx->v[3].f32[3] == 100.0f);
});
// Denormals should be flushed to signed zero.
test.Run(
[](PPCContext* ctx) {
// Smallest positive denormal.
uint32_t pos_denorm = 0x00000001;
// Smallest negative denormal.
uint32_t neg_denorm = 0x80000001;
memcpy(&ctx->v[4].f32[0], &pos_denorm, 4);
memcpy(&ctx->v[4].f32[1], &neg_denorm, 4);
ctx->v[4].f32[2] = 1.0f; // normal, should survive
ctx->v[4].f32[3] = -1.0f;
},
[](PPCContext* ctx) {
uint32_t r0, r1;
memcpy(&r0, &ctx->v[3].f32[0], 4);
memcpy(&r1, &ctx->v[3].f32[1], 4);
REQUIRE(r0 == 0x00000000); // +0.0
REQUIRE(r1 == 0x80000000); // -0.0 (sign preserved)
REQUIRE(ctx->v[3].f32[2] == 1.0f);
REQUIRE(ctx->v[3].f32[3] == -1.0f);
});
}
// ============================================================================
// CONVERT F64<->F32 (the only CONVERT variants the PPC frontend generates)
// ============================================================================
TEST_CASE("CONVERT_F64_TO_F32", "[convert]") {
TestFunction test([](HIRBuilder& b) {
auto fval = b.Convert(LoadFPR(b, 4), FLOAT32_TYPE);
StoreFPR(b, 3, b.Convert(fval, FLOAT64_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = -0.0; },
[](PPCContext* ctx) {
uint64_t bits;
memcpy(&bits, &ctx->f[3], 8);
REQUIRE(bits == 0x8000000000000000ULL); // -0.0 preserved
});
// Large value that loses precision in F32.
test.Run([](PPCContext* ctx) { ctx->f[4] = 16777217.0; }, // 2^24+1
[](PPCContext* ctx) {
float f32 = static_cast<float>(ctx->f[3]);
REQUIRE(f32 == 16777216.0f); // rounds to 2^24
});
}
// ============================================================================
// VECTOR_CONVERT_I2F (unsigned and signed)
// ============================================================================
TEST_CASE("VECTOR_CONVERT_I2F_UNSIGNED", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorConvertI2F(LoadVR(b, 4), ARITHMETIC_UNSIGNED));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(0, 1, 0x7FFFFFFF, 0xFFFFFFFF);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].f32[0] == 0.0f);
REQUIRE(ctx->v[3].f32[1] == 1.0f);
// 0x7FFFFFFF rounds to 2147483648.0f in single precision.
REQUIRE(ctx->v[3].f32[2] == static_cast<float>(0x7FFFFFFFU));
// 0xFFFFFFFF = 4294967295, rounds to 4294967296.0f.
REQUIRE(ctx->v[3].f32[3] == static_cast<float>(0xFFFFFFFFU));
});
}
TEST_CASE("VECTOR_CONVERT_I2F_SIGNED", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorConvertI2F(LoadVR(b, 4), 0));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(0, 1, 0x80000000, 0xFFFFFFFF);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].f32[0] == 0.0f);
REQUIRE(ctx->v[3].f32[1] == 1.0f);
REQUIRE(ctx->v[3].f32[2] == -2147483648.0f);
REQUIRE(ctx->v[3].f32[3] == -1.0f);
});
}
// ============================================================================
// VECTOR_CONVERT_F2I (unsigned and signed)
// ============================================================================
TEST_CASE("VECTOR_CONVERT_F2I_UNSIGNED", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorConvertF2I(LoadVR(b, 4), ARITHMETIC_UNSIGNED));
b.Return();
});
test.Run(
[](PPCContext* ctx) { ctx->v[4] = vec128f(0.0f, 1.0f, 1.5f, 255.0f); },
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].u32[0] == 0);
REQUIRE(ctx->v[3].u32[1] == 1);
REQUIRE(ctx->v[3].u32[2] == 1); // truncate toward zero
REQUIRE(ctx->v[3].u32[3] == 255);
});
// NaN should produce 0.
test.Run(
[](PPCContext* ctx) {
float nan = std::numeric_limits<float>::quiet_NaN();
ctx->v[4] = vec128f(nan, 0.0f, 0.0f, 0.0f);
},
[](PPCContext* ctx) { REQUIRE(ctx->v[3].u32[0] == 0); });
}
TEST_CASE("VECTOR_CONVERT_F2I_SIGNED", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorConvertF2I(LoadVR(b, 4), 0));
b.Return();
});
test.Run(
[](PPCContext* ctx) { ctx->v[4] = vec128f(0.0f, -1.0f, 1.5f, -1.5f); },
[](PPCContext* ctx) {
REQUIRE(static_cast<int32_t>(ctx->v[3].u32[0]) == 0);
REQUIRE(static_cast<int32_t>(ctx->v[3].u32[1]) == -1);
REQUIRE(static_cast<int32_t>(ctx->v[3].u32[2]) == 1);
REQUIRE(static_cast<int32_t>(ctx->v[3].u32[3]) == -1);
});
// NaN should produce 0.
test.Run(
[](PPCContext* ctx) {
float nan = std::numeric_limits<float>::quiet_NaN();
ctx->v[4] = vec128f(nan, 0.0f, 0.0f, 0.0f);
},
[](PPCContext* ctx) {
REQUIRE(static_cast<int32_t>(ctx->v[3].u32[0]) == 0);
});
}
// ============================================================================
// VECTOR_COMPARE_EQ / SGT / UGT — basic coverage
// ============================================================================
TEST_CASE("VECTOR_COMPARE_EQ_I32", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorCompareEQ(LoadVR(b, 4), LoadVR(b, 5), INT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(1, 2, 3, 4);
ctx->v[5] = vec128i(1, 99, 3, 99);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].u32[0] == 0xFFFFFFFF); // equal
REQUIRE(ctx->v[3].u32[1] == 0x00000000); // not equal
REQUIRE(ctx->v[3].u32[2] == 0xFFFFFFFF); // equal
REQUIRE(ctx->v[3].u32[3] == 0x00000000); // not equal
});
}
TEST_CASE("VECTOR_COMPARE_SGT_I32", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorCompareSGT(LoadVR(b, 4), LoadVR(b, 5), INT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(1, 0xFFFFFFFF, 0x80000000, 0);
ctx->v[5] = vec128i(0, 0, 0, 0x80000000);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].u32[0] == 0xFFFFFFFF); // 1 > 0
REQUIRE(ctx->v[3].u32[1] == 0x00000000); // -1 > 0 = false
REQUIRE(ctx->v[3].u32[2] == 0x00000000); // INT_MIN > 0 = false
REQUIRE(ctx->v[3].u32[3] == 0xFFFFFFFF); // 0 > INT_MIN = true
});
}
TEST_CASE("VECTOR_COMPARE_UGT_I32", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorCompareUGT(LoadVR(b, 4), LoadVR(b, 5), INT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(1, 0xFFFFFFFF, 0x80000000, 0);
ctx->v[5] = vec128i(0, 0, 0, 0x80000000);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].u32[0] == 0xFFFFFFFF); // 1 > 0
REQUIRE(ctx->v[3].u32[1] == 0xFFFFFFFF); // 0xFFFFFFFF > 0 (unsigned)
REQUIRE(ctx->v[3].u32[2] == 0xFFFFFFFF); // 0x80000000 > 0 (unsigned)
REQUIRE(ctx->v[3].u32[3] == 0x00000000); // 0 > 0x80000000 = false
});
}
// ============================================================================
// SPLAT
// ============================================================================
TEST_CASE("SPLAT_I32", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.Splat(b.Truncate(LoadGPR(b, 4), INT32_TYPE), VEC128_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0xDEADBEEF; },
[](PPCContext* ctx) {
REQUIRE(ctx->v[3] ==
vec128i(0xDEADBEEF, 0xDEADBEEF, 0xDEADBEEF, 0xDEADBEEF));
});
test.Run([](PPCContext* ctx) { ctx->r[4] = 0; },
[](PPCContext* ctx) { REQUIRE(ctx->v[3] == vec128i(0, 0, 0, 0)); });
}
TEST_CASE("SPLAT_F32", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.Splat(b.Convert(LoadFPR(b, 4), FLOAT32_TYPE), VEC128_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 3.14; },
[](PPCContext* ctx) {
float expected = static_cast<float>(3.14);
REQUIRE(ctx->v[3].f32[0] == expected);
REQUIRE(ctx->v[3].f32[1] == expected);
REQUIRE(ctx->v[3].f32[2] == expected);
REQUIRE(ctx->v[3].f32[3] == expected);
});
}
// ============================================================================
// IS_NAN (F64 — F32 variant is impossible on x64)
// ============================================================================
TEST_CASE("IS_NAN_F64", "[instr]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3, b.ZeroExtend(b.IsNan(LoadFPR(b, 4)), INT64_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = 0.0; },
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); });
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = std::numeric_limits<double>::quiet_NaN();
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); });
test.Run(
[](PPCContext* ctx) {
ctx->f[4] = std::numeric_limits<double>::infinity();
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); });
}
// ============================================================================
// DIV I32 — divide by zero and INT_MIN/-1 edge cases
// ============================================================================
TEST_CASE("DIV_I32", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3,
b.ZeroExtend(b.Div(b.Truncate(LoadGPR(b, 4), INT32_TYPE),
b.Truncate(LoadGPR(b, 5), INT32_TYPE)),
INT64_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 10;
ctx->r[5] = 3;
},
[](PPCContext* ctx) { REQUIRE(static_cast<int32_t>(ctx->r[3]) == 3); });
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFFFFFFFF;
ctx->r[5] = 1;
},
[](PPCContext* ctx) { REQUIRE(static_cast<int32_t>(ctx->r[3]) == -1); });
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 7;
ctx->r[5] = 2;
},
[](PPCContext* ctx) { REQUIRE(static_cast<int32_t>(ctx->r[3]) == 3); });
}
// ============================================================================
// VECTOR_SUB — basic coverage with saturation
// ============================================================================
TEST_CASE("VECTOR_SUB_I32", "[vector]") {
TestFunction test([](HIRBuilder& b) {
StoreVR(b, 3, b.VectorSub(LoadVR(b, 4), LoadVR(b, 5), INT32_TYPE));
b.Return();
});
test.Run(
[](PPCContext* ctx) {
ctx->v[4] = vec128i(10, 0, 0x80000000, 0xFFFFFFFF);
ctx->v[5] = vec128i(3, 1, 1, 0xFFFFFFFF);
},
[](PPCContext* ctx) {
REQUIRE(ctx->v[3].u32[0] == 7);
REQUIRE(ctx->v[3].u32[1] == 0xFFFFFFFF); // 0-1 wraps
REQUIRE(ctx->v[3].u32[2] == 0x7FFFFFFF); // INT_MIN-1 wraps
REQUIRE(ctx->v[3].u32[3] == 0);
});
}
// ============================================================================
// ABS F32/F64
// ============================================================================
TEST_CASE("ABS_F32", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
auto val = b.Convert(LoadFPR(b, 4), FLOAT32_TYPE);
auto absval = b.Abs(val);
StoreFPR(b, 3, b.Convert(absval, FLOAT64_TYPE));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = -1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = 0.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 0.0); });
// ABS of -0.0 should be +0.0.
test.Run([](PPCContext* ctx) { ctx->f[4] = -0.0; },
[](PPCContext* ctx) {
uint64_t bits;
memcpy(&bits, &ctx->f[3], 8);
REQUIRE(bits == 0); // +0.0, not -0.0
});
}
// ============================================================================
// SQRT F64
// ============================================================================
TEST_CASE("SQRT_F64", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreFPR(b, 3, b.Sqrt(LoadFPR(b, 4)));
b.Return();
});
test.Run([](PPCContext* ctx) { ctx->f[4] = 4.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 2.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = 0.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 0.0); });
test.Run([](PPCContext* ctx) { ctx->f[4] = 1.0; },
[](PPCContext* ctx) { REQUIRE(ctx->f[3] == 1.0); });
}
// ============================================================================
// MUL_HI I64 — unsigned (ARITHMETIC_UNSIGNED)
// ============================================================================
TEST_CASE("MUL_HI_I64_UNSIGNED", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3, b.MulHi(LoadGPR(b, 4), LoadGPR(b, 5), ARITHMETIC_UNSIGNED));
b.Return();
});
// Low values: high 64 bits of 2*3 = 0.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 2;
ctx->r[5] = 3;
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); });
// 2^63 * 2: high 64 bits = 1.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0x8000000000000000ULL;
ctx->r[5] = 2;
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); });
// MAX * MAX: (2^64-1)^2 high bits = 2^64-2.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFFFFFFFFFFFFFFFFULL;
ctx->r[5] = 0xFFFFFFFFFFFFFFFFULL;
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0xFFFFFFFFFFFFFFFEULL); });
}
// MUL_HI I64 — signed (default)
TEST_CASE("MUL_HI_I64_SIGNED", "[arithmetic]") {
TestFunction test([](HIRBuilder& b) {
StoreGPR(b, 3, b.MulHi(LoadGPR(b, 4), LoadGPR(b, 5)));
b.Return();
});
// 2 * 3 signed: high bits = 0.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 2;
ctx->r[5] = 3;
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); });
// -1 * -1 signed: result is 1 (128-bit), high bits = 0.
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFFFFFFFFFFFFFFFFULL;
ctx->r[5] = 0xFFFFFFFFFFFFFFFFULL;
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); });
// -1 * 2 signed: result is -2, high bits = -1 (0xFFFFFFFFFFFFFFFF).
test.Run(
[](PPCContext* ctx) {
ctx->r[4] = 0xFFFFFFFFFFFFFFFFULL;
ctx->r[5] = 2;
},
[](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0xFFFFFFFFFFFFFFFFULL); });
}
// ============================================================================
// ATOMIC_COMPARE_EXCHANGE I32
// ============================================================================
TEST_CASE("ATOMIC_COMPARE_EXCHANGE_I32", "[atomic]") {
TestFunction test([](HIRBuilder& b) {
// r[4] = address (guest), r[5] = expected, r[6] = desired.
auto addr = LoadGPR(b, 4);
auto expected = b.Truncate(LoadGPR(b, 5), INT32_TYPE);
auto desired = b.Truncate(LoadGPR(b, 6), INT32_TYPE);
auto result = b.AtomicCompareExchange(addr, expected, desired);
StoreGPR(b, 3, b.ZeroExtend(result, INT64_TYPE));
b.Return();
});
uint32_t guest_addr = test.memory->SystemHeapAlloc(4);
REQUIRE(guest_addr != 0);
auto* host_ptr =
reinterpret_cast<uint32_t*>(test.memory->TranslateVirtual(guest_addr));
// Success case: expected matches current value.
test.Run(
[&](PPCContext* ctx) {
*host_ptr = 0xAAAAAAAA;
ctx->r[4] = guest_addr;
ctx->r[5] = 0xAAAAAAAA; // expected
ctx->r[6] = 0xBBBBBBBB; // desired
},
[&](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 1); // success
REQUIRE(*host_ptr == 0xBBBBBBBB);
});
// Failure case: expected does NOT match.
test.Run(
[&](PPCContext* ctx) {
*host_ptr = 0xCCCCCCCC;
ctx->r[4] = guest_addr;
ctx->r[5] = 0xDDDDDDDD; // wrong expected
ctx->r[6] = 0xEEEEEEEE; // desired
},
[&](PPCContext* ctx) {
REQUIRE(ctx->r[3] == 0); // failure
REQUIRE(*host_ptr == 0xCCCCCCCC); // unchanged
});
test.memory->SystemHeapFree(guest_addr);
}