diff --git a/src/xenia/cpu/testing/backend_integration_test.cc b/src/xenia/cpu/testing/backend_integration_test.cc index 2d89ba321..3acc7f77a 100644 --- a/src/xenia/cpu/testing/backend_integration_test.cc +++ b/src/xenia/cpu/testing/backend_integration_test.cc @@ -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( + reinterpret_cast(ctx) - + sizeof(xe::cpu::backend::x64::X64BackendContext)); + observed_njm_flags = bctx->flags; +#elif XE_ARCH_ARM64 + auto* bctx = reinterpret_cast( + reinterpret_cast(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->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( + "ReadBackendFlags", ReadBackendFlags, nullptr, nullptr); + + auto module = std::make_unique( + 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(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(host_ptr) = 0xAABBCCDD; + // Pass the HOST address in r[4]. + ctx->r[4] = reinterpret_cast(host_ptr); + ctx->r[5] = 0x11223344; + }, + [&](PPCContext* ctx) { + // r[3] should have the old value. + REQUIRE(static_cast(ctx->r[3]) == 0xAABBCCDD); + // Memory should now have the new value. + REQUIRE(*reinterpret_cast(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); }); +} diff --git a/src/xenia/cpu/testing/compare_test.cc b/src/xenia/cpu/testing/compare_test.cc new file mode 100644 index 000000000..03a0f3ae2 --- /dev/null +++ b/src/xenia/cpu/testing/compare_test.cc @@ -0,0 +1,1130 @@ +/** + ****************************************************************************** + * 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. * + ****************************************************************************** + */ + +#include "xenia/cpu/testing/util.h" + +using namespace xe; +using namespace xe::cpu; +using namespace xe::cpu::hir; +using namespace xe::cpu::testing; +using xe::cpu::ppc::PPCContext; + +// Helper: build a compare test function for a given type width and compare op. +// Loads r[4] and r[5] as the given integer type, compares, stores result +// (0 or 1) in r[3]. + +// ============================================================================ +// COMPARE_EQ +// ============================================================================ +TEST_CASE("COMPARE_EQ_I8", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareEQ(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF; + ctx->r[5] = 0xFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); +} + +TEST_CASE("COMPARE_EQ_I16", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareEQ(b.Truncate(LoadGPR(b, 4), INT16_TYPE), + b.Truncate(LoadGPR(b, 5), INT16_TYPE)), + INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x8000; + ctx->r[5] = 0x8000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +TEST_CASE("COMPARE_EQ_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareEQ(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] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 0xFFFFFFFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 2; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +TEST_CASE("COMPARE_EQ_I64", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR( + b, 3, + b.ZeroExtend(b.CompareEQ(LoadGPR(b, 4), LoadGPR(b, 5)), INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFFFFFFFFFFULL; + ctx->r[5] = 0xFFFFFFFFFFFFFFFFULL; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // Same low 32 bits, different upper — must NOT be equal. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x0000000080000000ULL; + ctx->r[5] = 0xFFFFFFFF80000000ULL; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_NE +// ============================================================================ +TEST_CASE("COMPARE_NE_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareNE(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] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 2; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_SLT (signed less than) +// ============================================================================ +TEST_CASE("COMPARE_SLT_I8", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareSLT(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // -1 < 0 (signed) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0 < -1 (signed) = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0xFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + // Equal = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 5; + ctx->r[5] = 5; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +TEST_CASE("COMPARE_SLT_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareSLT(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Truncate(LoadGPR(b, 5), INT32_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // -1 < 0 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0 < -1 = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0xFFFFFFFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + // INT_MIN < 0 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0 < INT_MAX + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0x7FFFFFFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // INT_MAX < INT_MIN = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x7FFFFFFF; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + // Equal = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 42; + ctx->r[5] = 42; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +TEST_CASE("COMPARE_SLT_I64", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR( + b, 3, + b.ZeroExtend(b.CompareSLT(LoadGPR(b, 4), LoadGPR(b, 5)), INT64_TYPE)); + b.Return(); + }); + // -1 < 0 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFFFFFFFFFFULL; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // Sign-extended 32-bit: 0xFFFFFFFF80000000 as signed i64 is negative. + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF80000000ULL; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0x0000000080000000 as signed i64 is positive (2^31). + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x0000000080000000ULL; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_SLE (signed less or equal) +// ============================================================================ +TEST_CASE("COMPARE_SLE_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareSLE(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] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_SGT (signed greater than) +// ============================================================================ +TEST_CASE("COMPARE_SGT_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareSGT(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] = 1; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0xFFFFFFFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_SGE (signed greater or equal) +// ============================================================================ +TEST_CASE("COMPARE_SGE_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareSGE(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] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0xFFFFFFFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); +} + +// ============================================================================ +// COMPARE_ULT (unsigned less than) +// ============================================================================ +TEST_CASE("COMPARE_ULT_I8", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareULT(b.Truncate(LoadGPR(b, 4), INT8_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // 0 < 0xFF (unsigned: 0 < 255) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0xFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0xFF < 0 (unsigned: 255 < 0 = false) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +TEST_CASE("COMPARE_ULT_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareULT(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Truncate(LoadGPR(b, 5), INT32_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // 0 < 0x80000000 (unsigned: 0 < 2^31) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0x80000000 < 0 (unsigned: 2^31 < 0 = false) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + // 0xFFFFFFFF < 1 (unsigned: MAX < 1 = false) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + // 1 < 0xFFFFFFFF (unsigned) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 0xFFFFFFFF; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // Equal = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +TEST_CASE("COMPARE_ULT_I64", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR( + b, 3, + b.ZeroExtend(b.CompareULT(LoadGPR(b, 4), LoadGPR(b, 5)), INT64_TYPE)); + b.Return(); + }); + // 0 < 1 + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0x0000000080000000 < 0xFFFFFFFF80000000 (unsigned: yes) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x0000000080000000ULL; + ctx->r[5] = 0xFFFFFFFF80000000ULL; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0xFFFFFFFF80000000 < 0x0000000080000000 (unsigned: no) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF80000000ULL; + ctx->r[5] = 0x0000000080000000ULL; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_ULE (unsigned less or equal) +// ============================================================================ +TEST_CASE("COMPARE_ULE_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareULE(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] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_UGT (unsigned greater than) +// ============================================================================ +TEST_CASE("COMPARE_UGT_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareUGT(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Truncate(LoadGPR(b, 5), INT32_TYPE)), + INT64_TYPE)); + b.Return(); + }); + // 0x80000000 > 0 (unsigned: 2^31 > 0 = true) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // 0 > 0x80000000 (unsigned: false) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 0x80000000; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); + // 0xFFFFFFFF > 0 (unsigned: MAX > 0 = true) + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + // Equal = false + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 5; + ctx->r[5] = 5; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// COMPARE_UGE (unsigned greater or equal) +// ============================================================================ +TEST_CASE("COMPARE_UGE_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.CompareUGE(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] = 0; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0); }); +} + +// ============================================================================ +// SELECT — conditional select based on I8 condition +// ============================================================================ +TEST_CASE("SELECT_I32", "[compare]") { + TestFunction test([](HIRBuilder& b) { + auto cond = b.Truncate(LoadGPR(b, 4), INT8_TYPE); + auto val_true = b.Truncate(LoadGPR(b, 5), INT32_TYPE); + auto val_false = b.Truncate(LoadGPR(b, 6), INT32_TYPE); + StoreGPR(b, 3, + b.ZeroExtend(b.Select(cond, val_true, val_false), INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 100; + ctx->r[6] = 200; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 100); + }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 100; + ctx->r[6] = 200; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 200); + }); +} + +// ============================================================================ +// BRANCH_TRUE / BRANCH_FALSE — test that comparisons feed branches correctly +// ============================================================================ +// Branch tests use TestModule with skip_cf_simplification=true because +// manually-constructed HIR branches don't set up block structure the same +// way the PPC frontend does, and CF simplification mishandles them. +TEST_CASE("BRANCH_ON_COMPARE_SLT_I32", "[compare]") { + 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 module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [](HIRBuilder& b) { + auto cmp = b.CompareSLT(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Truncate(LoadGPR(b, 5), INT32_TYPE)); + auto* if_true = b.NewLabel(); + auto* if_false = b.NewLabel(); + auto* end = b.NewLabel(); + b.BranchTrue(cmp, if_true); + b.MarkLabel(if_false); + StoreGPR(b, 3, b.LoadConstantUint64(0)); + b.Branch(end); + b.MarkLabel(if_true); + StoreGPR(b, 3, b.LoadConstantUint64(1)); + b.Branch(end); + b.MarkLabel(end); + 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 run = [&](uint64_t r4, uint64_t r5, uint64_t expected_r3) { + auto thread_state = std::make_unique( + processor.get(), 0x100, stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + ctx->r[3] = 99; + ctx->r[4] = r4; + ctx->r[5] = r5; + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + REQUIRE(ctx->r[3] == expected_r3); + }; + + run(0xFFFFFFFF, 0, 1); // -1 < 0 = true + run(0, 0xFFFFFFFF, 0); // 0 < -1 = false + run(0x80000000, 0, 1); // INT_MIN < 0 = true + run(0, 0x80000000, 0); // 0 < INT_MIN = false + run(42, 42, 0); // equal = false + + memory->SystemHeapFree(stack_address); +} + +TEST_CASE("BRANCH_ON_COMPARE_UGT_I32", "[compare]") { + 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 module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [](HIRBuilder& b) { + auto cmp = b.CompareUGT(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Truncate(LoadGPR(b, 5), INT32_TYPE)); + auto* if_true = b.NewLabel(); + auto* if_false = b.NewLabel(); + auto* end = b.NewLabel(); + b.BranchTrue(cmp, if_true); + b.MarkLabel(if_false); + StoreGPR(b, 3, b.LoadConstantUint64(0)); + b.Branch(end); + b.MarkLabel(if_true); + StoreGPR(b, 3, b.LoadConstantUint64(1)); + b.Branch(end); + b.MarkLabel(end); + 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 run = [&](uint64_t r4, uint64_t r5, uint64_t expected_r3) { + auto thread_state = std::make_unique( + processor.get(), 0x100, stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + ctx->r[3] = 99; + ctx->r[4] = r4; + ctx->r[5] = r5; + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + REQUIRE(ctx->r[3] == expected_r3); + }; + + run(0x80000000, 0, 1); // 2^31 > 0 = true + run(0, 0x80000000, 0); // 0 > 2^31 = false + run(0xFFFFFFFF, 0xFFFFFFFE, 1); // MAX > MAX-1 = true + run(5, 5, 0); // equal = false + + memory->SystemHeapFree(stack_address); +} + +// ============================================================================ +// BRANCH_TRUE F32 — float-conditioned branch (nonzero bits = true) +// ============================================================================ +TEST_CASE("BRANCH_ON_F32_TRUE", "[compare]") { + 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 module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [](HIRBuilder& b) { + // Branch if f[4] (as F32) is nonzero. + auto cond = b.Convert(LoadFPR(b, 4), FLOAT32_TYPE); + auto* if_true = b.NewLabel(); + auto* if_false = b.NewLabel(); + auto* end = b.NewLabel(); + b.BranchTrue(cond, if_true); + b.MarkLabel(if_false); + StoreGPR(b, 3, b.LoadConstantUint64(0)); + b.Branch(end); + b.MarkLabel(if_true); + StoreGPR(b, 3, b.LoadConstantUint64(1)); + b.Branch(end); + b.MarkLabel(end); + 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 run = [&](double f4, uint64_t expected_r3) { + auto thread_state = std::make_unique( + processor.get(), 0x100, stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + ctx->r[3] = 99; + ctx->f[4] = f4; + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + REQUIRE(ctx->r[3] == expected_r3); + }; + + run(1.0, 1); // nonzero → true + run(0.0, 0); // zero → false + run(-1.0, 1); // nonzero → true + run(-0.0, 1); // -0.0f = 0x80000000 (sign bit set), nonzero bits → taken + + memory->SystemHeapFree(stack_address); +} + +// ============================================================================ +// BRANCH_FALSE F32 — float-conditioned branch (zero bits = true) +// ============================================================================ +TEST_CASE("BRANCH_ON_F32_FALSE", "[compare]") { + 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 module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [](HIRBuilder& b) { + auto cond = b.Convert(LoadFPR(b, 4), FLOAT32_TYPE); + auto* if_false = b.NewLabel(); + auto* if_not = b.NewLabel(); + auto* end = b.NewLabel(); + b.BranchFalse(cond, if_false); + b.MarkLabel(if_not); + StoreGPR(b, 3, b.LoadConstantUint64(0)); + b.Branch(end); + b.MarkLabel(if_false); + StoreGPR(b, 3, b.LoadConstantUint64(1)); + b.Branch(end); + b.MarkLabel(end); + 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 run = [&](double f4, uint64_t expected_r3) { + auto thread_state = std::make_unique( + processor.get(), 0x100, stack_address + stack_size); + auto ctx = thread_state->context(); + ctx->lr = 0xBCBCBCBC; + ctx->r[3] = 99; + ctx->f[4] = f4; + fn->Call(thread_state.get(), uint32_t(ctx->lr)); + REQUIRE(ctx->r[3] == expected_r3); + }; + + run(0.0, 1); // zero → branch taken + run(1.0, 0); // nonzero → not taken + + memory->SystemHeapFree(stack_address); +} + +// ============================================================================ +// AND, OR, XOR, NOT — basic bitwise ops +// ============================================================================ +TEST_CASE("AND_I32", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.And(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] = 0xFF00FF00; + ctx->r[5] = 0x0F0F0F0F; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x0F000F00); + }); +} + +TEST_CASE("OR_I32", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.Or(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] = 0xFF00FF00; + ctx->r[5] = 0x00FF00FF; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xFFFFFFFF); + }); +} + +TEST_CASE("XOR_I32", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.Xor(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] = 0xFFFFFFFF; + ctx->r[5] = 0xFF00FF00; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x00FF00FF); + }); +} + +TEST_CASE("NOT_I32", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR( + b, 3, + b.ZeroExtend(b.Not(b.Truncate(LoadGPR(b, 4), INT32_TYPE)), INT64_TYPE)); + b.Return(); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xFFFFFFFF); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFF00FF00; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x00FF00FF); + }); +} + +// ============================================================================ +// SUB +// ============================================================================ +TEST_CASE("SUB_I32", "[arithmetic]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.Sub(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(ctx->r[3]) == 7); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xFFFFFFFF); + }); +} + +// ============================================================================ +// MUL +// ============================================================================ +TEST_CASE("MUL_I32", "[arithmetic]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.Mul(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] = 7; + ctx->r[5] = 6; + }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 42); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 2; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xFFFFFFFE); + }); +} + +// ============================================================================ +// NEG +// ============================================================================ +TEST_CASE("NEG_I32", "[arithmetic]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR( + b, 3, + b.ZeroExtend(b.Neg(b.Truncate(LoadGPR(b, 4), INT32_TYPE)), INT64_TYPE)); + b.Return(); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 1; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xFFFFFFFF); + }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0xFFFFFFFF; }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 1); }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0; }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 0); }); +} + +// ============================================================================ +// SIGN_EXTEND / ZERO_EXTEND +// ============================================================================ +TEST_CASE("SIGN_EXTEND_I32_TO_I64", "[convert]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.SignExtend(b.Truncate(LoadGPR(b, 4), INT32_TYPE), INT64_TYPE)); + b.Return(); + }); + // Positive value — upper bits should be 0. + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0x7FFFFFFF; }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x000000007FFFFFFFULL); }); + // Negative value — upper bits should be 1. + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0x80000000; }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0xFFFFFFFF80000000ULL); }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0xFFFFFFFF; }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0xFFFFFFFFFFFFFFFFULL); }); +} + +TEST_CASE("ZERO_EXTEND_I32_TO_I64", "[convert]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.Truncate(LoadGPR(b, 4), INT32_TYPE), INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0x80000000; }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x0000000080000000ULL); }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0xFFFFFFFF; }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 0x00000000FFFFFFFFULL); }); +} + +// ============================================================================ +// CNTLZ +// ============================================================================ +TEST_CASE("CNTLZ_I32", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR( + b, 3, + b.ZeroExtend(b.CountLeadingZeros(b.Truncate(LoadGPR(b, 4), INT32_TYPE)), + INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0; }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 32); }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 1; }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 31); }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0x80000000; }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 0); }); + test.Run( + [](PPCContext* ctx) { ctx->r[4] = 0x00010000; }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 15); }); +} + +// ============================================================================ +// ROTATE_LEFT +// ============================================================================ +TEST_CASE("ROTATE_LEFT_I32", "[bitwise]") { + TestFunction test([](HIRBuilder& b) { + StoreGPR(b, 3, + b.ZeroExtend(b.RotateLeft(b.Truncate(LoadGPR(b, 4), INT32_TYPE), + b.Truncate(LoadGPR(b, 5), INT8_TYPE)), + INT64_TYPE)); + b.Return(); + }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 1; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 2); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x80000000; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0x12345678; + ctx->r[5] = 0; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x12345678); + }); +} diff --git a/src/xenia/cpu/testing/edge_case_test.cc b/src/xenia/cpu/testing/edge_case_test.cc new file mode 100644 index 000000000..75d8c20d3 --- /dev/null +++ b/src/xenia/cpu/testing/edge_case_test.cc @@ -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 +#include + +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(ctx->r[3]) == 0x3412); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0x0100; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x0001); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0x80FF; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xFF80); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFFFF; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(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(ctx->r[3]) == 0x04030201); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0x80000000; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x00000080); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFF000000; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x000000FF); + }); + test.Run([](PPCContext* ctx) { ctx->r[4] = 0xDEADBEEF; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0xEFBEADDE); + }); + // Verify upper 32 bits are zero. + test.Run([](PPCContext* ctx) { ctx->r[4] = 0xFFFFFFFF; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(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(ctx->r[3]) == 0xDEADBEEF); + }); + test.Run( + [&](PPCContext* ctx) { + ctx->r[4] = guest_addr; + ctx->r[5] = 0x80000001; + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(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(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(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(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(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(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(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(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(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(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(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(ctx->r[3]) == 0x7F); + }); +} diff --git a/src/xenia/cpu/testing/misc_opcode_test.cc b/src/xenia/cpu/testing/misc_opcode_test.cc new file mode 100644 index 000000000..b77f5031b --- /dev/null +++ b/src/xenia/cpu/testing/misc_opcode_test.cc @@ -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 +#include +#include + +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(ctx->r[3]) == 0x3F800000); + }); + // -0.0f = 0x80000000 + test.Run([](PPCContext* ctx) { ctx->r[4] = 0x80000000; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(ctx->r[3]) == 0x80000000); + }); + // NaN = 0x7FC00000 + test.Run([](PPCContext* ctx) { ctx->r[4] = 0x7FC00000; }, + [](PPCContext* ctx) { + REQUIRE(static_cast(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(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(ctx->f[3]); + REQUIRE(f32 == static_cast(-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(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(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(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(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(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(test.memory->TranslateVirtual(guest_addr)); + + test.Run( + [&](PPCContext* ctx) { + *host_ptr = 42; + ctx->r[4] = guest_addr; + }, + [&](PPCContext* ctx) { + REQUIRE(static_cast(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(test.memory->TranslateVirtual(aligned_addr)); + for (int i = 0; i < 16; ++i) host_ptr[i] = static_cast(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(test.memory->TranslateVirtual(aligned_addr)); + for (int i = 0; i < 16; ++i) host_ptr[i] = static_cast(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); +} diff --git a/src/xenia/cpu/testing/opcode_coverage_test.cc b/src/xenia/cpu/testing/opcode_coverage_test.cc new file mode 100644 index 000000000..3449c11fe --- /dev/null +++ b/src/xenia/cpu/testing/opcode_coverage_test.cc @@ -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 +#include +#include + +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(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(0x7FFFFFFFU)); + // 0xFFFFFFFF = 4294967295, rounds to 4294967296.0f. + REQUIRE(ctx->v[3].f32[3] == static_cast(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::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(ctx->v[3].u32[0]) == 0); + REQUIRE(static_cast(ctx->v[3].u32[1]) == -1); + REQUIRE(static_cast(ctx->v[3].u32[2]) == 1); + REQUIRE(static_cast(ctx->v[3].u32[3]) == -1); + }); + // NaN should produce 0. + test.Run( + [](PPCContext* ctx) { + float nan = std::numeric_limits::quiet_NaN(); + ctx->v[4] = vec128f(nan, 0.0f, 0.0f, 0.0f); + }, + [](PPCContext* ctx) { + REQUIRE(static_cast(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(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::quiet_NaN(); + }, + [](PPCContext* ctx) { REQUIRE(ctx->r[3] == 1); }); + test.Run( + [](PPCContext* ctx) { + ctx->f[4] = std::numeric_limits::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(ctx->r[3]) == 3); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 0xFFFFFFFF; + ctx->r[5] = 1; + }, + [](PPCContext* ctx) { REQUIRE(static_cast(ctx->r[3]) == -1); }); + test.Run( + [](PPCContext* ctx) { + ctx->r[4] = 7; + ctx->r[5] = 2; + }, + [](PPCContext* ctx) { REQUIRE(static_cast(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(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); +}