diff --git a/src/xenia/cpu/backend/a64/a64_sequences.cc b/src/xenia/cpu/backend/a64/a64_sequences.cc index 129e0c17d..2937ffb45 100644 --- a/src/xenia/cpu/backend/a64/a64_sequences.cc +++ b/src/xenia/cpu/backend/a64/a64_sequences.cc @@ -4794,31 +4794,55 @@ EMITTER_OPCODE_TABLE(OPCODE_TO_SINGLE, TOSINGLE); // ============================================================================ struct SET_NJM : Sequence> { static void Emit(A64Emitter& e, const EmitArgType& i) { - // NJM (Non-Java Mode) maps to ARM64 FPCR FZ (Flush-to-Zero) bit (bit 24). - // When NJM=1, enable flush-to-zero. When NJM=0, disable. - // mrs x0, FPCR (op0=3, op1=3, CRn=4, CRm=4, op2=0) - e.mrs(e.x0, 3, 3, 4, 4, 0); + // NJM (Non-Java Mode) is a VMX/AltiVec feature (VSCR bit 16) that + // controls flush-to-zero for vector operations. It does NOT affect + // scalar FPU behaviour. On ARM64 this maps to FPCR.FZ (bit 24) in + // the cached fpcr_vmx value, which EmitWithVmxFpcr loads before + // each vector FP operation. + auto bctx = e.GetBackendCtxReg(); + + // Toggle FZ bit in cached fpcr_vmx. + e.ldr(e.w0, ptr(bctx, static_cast( + offsetof(A64BackendContext, fpcr_vmx)))); if (i.src1.is_constant) { if (i.src1.constant()) { - e.orr(e.x0, e.x0, (1u << 24)); // Set FZ bit + e.orr(e.w0, e.w0, (1u << 24)); // NJM=1: set FZ } else { - e.and_(e.x0, e.x0, ~(1ull << 24)); // Clear FZ bit + e.and_(e.w0, e.w0, ~(1u << 24)); // NJM=0: clear FZ } } else { - // Dynamic: test src1, set/clear FZ accordingly. auto& set_fz = e.NewCachedLabel(); auto& done = e.NewCachedLabel(); e.cbnz(i.src1, set_fz); - // NJM=0: clear FZ - e.and_(e.x0, e.x0, ~(1ull << 24)); + e.and_(e.w0, e.w0, ~(1u << 24)); // NJM=0: clear FZ e.b(done); e.L(set_fz); - // NJM=1: set FZ - e.orr(e.x0, e.x0, (1u << 24)); + e.orr(e.w0, e.w0, (1u << 24)); // NJM=1: set FZ e.L(done); } - // msr FPCR, x0 (op0=3, op1=3, CRn=4, CRm=4, op2=0) - e.msr(3, 3, 4, 4, 0, e.x0); + e.str(e.w0, ptr(bctx, static_cast( + offsetof(A64BackendContext, fpcr_vmx)))); + + // Update kA64BackendNJMOn flag. + e.ldr(e.w0, + ptr(bctx, static_cast(offsetof(A64BackendContext, flags)))); + if (i.src1.is_constant) { + if (i.src1.constant()) { + e.orr(e.w0, e.w0, 1u << kA64BackendNJMOn); + } else { + e.mov(e.w1, 1u << kA64BackendNJMOn); + e.bic(e.w0, e.w0, e.w1); + } + } else { + e.mov(e.w1, 1u << kA64BackendNJMOn); + e.bic(e.w0, e.w0, e.w1); + e.tst(i.src1, 0xFF); + e.csel(e.w1, e.w1, e.wzr, Xbyak_aarch64::Cond::NE); + e.orr(e.w0, e.w0, e.w1); + } + e.str(e.w0, + ptr(bctx, static_cast(offsetof(A64BackendContext, flags)))); + e.ForgetFpcrMode(); } }; diff --git a/src/xenia/cpu/testing/backend_integration_test.cc b/src/xenia/cpu/testing/backend_integration_test.cc index d4d323a82..b17a7229c 100644 --- a/src/xenia/cpu/testing/backend_integration_test.cc +++ b/src/xenia/cpu/testing/backend_integration_test.cc @@ -537,6 +537,115 @@ TEST_CASE("NJM_DEFAULT_ON", "[backend]") { memory->SystemHeapFree(stack_address); } +// ============================================================================= +// SET_NJM — verify NJM toggle updates backend context correctly +// ============================================================================= +// NJM (Non-Java Mode) is a VMX feature (VSCR bit 16) that controls +// flush-to-zero for vector FP operations. We verify that SET_NJM +// correctly updates the cached VMX FPCR/MXCSR and the NJM flag. +static uint32_t observed_njm_flags_after_set = 0; +static uint32_t observed_vmx_fpcr_after_set = 0; + +static void ReadBackendNJMState(ppc::PPCContext* ctx, void* arg0, void* arg1) { +#if XE_ARCH_AMD64 + auto* bctx = reinterpret_cast( + reinterpret_cast(ctx) - + sizeof(xe::cpu::backend::x64::X64BackendContext)); + observed_njm_flags_after_set = bctx->flags; + observed_vmx_fpcr_after_set = bctx->mxcsr_vmx; +#elif XE_ARCH_ARM64 + auto* bctx = reinterpret_cast( + reinterpret_cast(ctx) - + sizeof(xe::cpu::backend::a64::A64BackendContext)); + observed_njm_flags_after_set = bctx->flags; + observed_vmx_fpcr_after_set = bctx->fpcr_vmx; +#endif +} + +// Helper to build and run a SET_NJM test function. +static void RunSetNJMTest(int njm_value) { + observed_njm_flags_after_set = 0; + observed_vmx_fpcr_after_set = 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( + "ReadBackendNJMState", ReadBackendNJMState, nullptr, nullptr); + + auto module = std::make_unique( + processor.get(), "Test", + [](uint32_t address) { return address == 0x80000000; }, + [builtin_fn, njm_value](HIRBuilder& b) { + b.SetNJM(b.LoadConstantInt8(njm_value ? 1 : 0)); + 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)); + + memory->SystemHeapFree(stack_address); +} + +TEST_CASE("SET_NJM_ON", "[backend]") { + RunSetNJMTest(1); + // NJM flag should be set. +#if XE_ARCH_AMD64 + REQUIRE((observed_njm_flags_after_set & + (1U << xe::cpu::backend::x64::kX64BackendNJMOn)) != 0); + // MXCSR should have FZ and DAZ set. + REQUIRE((observed_vmx_fpcr_after_set & (1 << 15)) != 0); // FZ + REQUIRE((observed_vmx_fpcr_after_set & (1 << 6)) != 0); // DAZ +#elif XE_ARCH_ARM64 + REQUIRE((observed_njm_flags_after_set & + (1U << xe::cpu::backend::a64::kA64BackendNJMOn)) != 0); + // FPCR_VMX should have FZ (bit 24) set. + REQUIRE((observed_vmx_fpcr_after_set & (1 << 24)) != 0); +#endif +} + +TEST_CASE("SET_NJM_OFF", "[backend]") { + RunSetNJMTest(0); + // NJM flag should be cleared. +#if XE_ARCH_AMD64 + REQUIRE((observed_njm_flags_after_set & + (1U << xe::cpu::backend::x64::kX64BackendNJMOn)) == 0); + // MXCSR should NOT have FZ or DAZ. + REQUIRE((observed_vmx_fpcr_after_set & (1 << 15)) == 0); + REQUIRE((observed_vmx_fpcr_after_set & (1 << 6)) == 0); +#elif XE_ARCH_ARM64 + REQUIRE((observed_njm_flags_after_set & + (1U << xe::cpu::backend::a64::kA64BackendNJMOn)) == 0); + // FPCR_VMX should NOT have FZ (bit 24). + REQUIRE((observed_vmx_fpcr_after_set & (1 << 24)) == 0); +#endif +} + // ============================================================================= // Atomic Exchange I32 // =============================================================================