Files
WizzardSK 5f2eea7bfb AArch64 (arm64) port: native EE/IOP/VU recompilers
Upstream PCSX2 emulation relies on x86-64 recompilers for the EE (R5900),
IOP (R3000A) and the VUs, none of which exist on ARM. This adds native
AArch64 recompilers plus the supporting infrastructure to run PS2 games
correctly and reasonably fast on 64-bit ARM Linux devices (developed and
tested on a Snapdragon-class device: Adreno 618, 4 KB-page Linux, glibc
RetroArch flatpak + musl test host). Verified booting to real in-game
content (Mega Man X7 gameplay, Gran Turismo 3 intro/FMV and in-race) on
Vulkan/Turnip.

Native components:
- IOP (R3000A) JIT ~98%: ALU incl. trap forms + RFE, aligned/unaligned
  mem, branches + delay slots, block linking, mult/div + HI/LO, COP0
  moves, write-back register cache, RAM fastmem, event-gated tails,
  native J (only the HLE import-stub form stays interpreted, by design),
  cap-chaining, idle-loop fast-forward. Only the J import stub (HLE) and
  SYSCALL still break blocks.
- EE (R5900) JIT ~89%: ALU, loads/stores incl. LQ/SQ + unaligned family
  (fused canonical pairs), branches + likely branches + BC0x/BC1x, block
  linking/chaining, MULT/DIV/MADD + HI/LO (both pipelines), complete
  COP1/FPU (data moves + all S-format arithmetic + CVT + C.cond,
  interpreter-exact clamp/flag semantics), MMI -> NEON (packed integer
  incl. PPAC5, PMFHI/PMFLO/PMTHI/PMTLO, PLZCW, PREVH), CFC1/CTC1,
  MFSA/MTSA, ERET, COP0 moves + EI/DI, kernel idle-loop skip,
  block-revalidation cache, write-back GPR register cache. Only SYSCALL /
  TLB / exceptions still break blocks.
- VU0 macro (COP2) ~78%: runs inline in the EE thread; all SPECIAL1/2 ALU
  ops emit native NEON directly into EE blocks (microVU0 single-op
  emitters) with run-local flag liveness; DIV/SQRT/RSQRT, ITOF/FTOI, VI
  ALU, VI load/store, RNG, MOVE/MR32, MTIR/MFIR, CLIP; COP2 transfers
  native. Only CALLMS(R) falls to the interpreter (as x86).
- VU1 micro (microVU1) ~70%: full microVU recompiler port incl. the
  register allocator; runs on the MTVU worker thread by default.
- VIF 100%: NEON-optimized unpack.

Overall recompiler-suite progress ~88% (weighted by remaining work;
dynamic instruction coverage on the tested titles is much higher).

Supporting infrastructure:
- VIXL vendored as the AArch64 assembler (3rdparty/vixl).
- Build gating: x86 recompiler/emitter sources gated off aarch64; build
  detects and accepts aarch64.
- Fastmem: a 4 GB host mapping where every memory-backed guest page sits
  at its own guest address, so a load/store is one instruction; HW-reg /
  unmapped touches fault into a compile-time backpatch stub.
- Emitted-code quality passes: x19 guest-reg base, displacement folding,
  pinned vmap/fastmem base (x28), pinned &microVU base (x27), out-of-line
  cold code, fused chain epilogues, constant-LUT block chaining,
  adrp+pageoff folds, run-local COP2 flag liveness, lazy MTVU kick.
- Env-gated diagnostics (LRPS2_*) for A/B bisection + an in-tree SIGPROF
  sampling profiler bucketed by JIT code cache.

Verification: every phase is checked bit-identically against a golden
framebuffer (pixel-sum + nonzero count) on a headless Vulkan harness and,
for deep coverage, a Gran Turismo 3 in-race save state:
- GT3 1500f = 106138289 / 860124, GT3 12000f = 29063788 / 855955
- MMX7 1500f = 111415017 / 800725
- GT3 in-race 600f = 49204167 / 858359
Most opcode/emitter changes carry a kill switch and leave these
byte-identical; the microVU work is additionally checked with an
interpreter-vs-microVU shadow differential.

Known limits: SYSCALL/TLB/exceptions and the IOP HLE import stub stay on
the interpreter by design; demanding titles (e.g. GT3) run below full
speed on this hardware class, so audio underruns in heavy scenes (the
emulated audio itself is correct -- a raw-performance ceiling, not a bug).
2026-07-17 21:58:23 +02:00

217 lines
5.5 KiB
C++

// SPDX-FileCopyrightText: 2002-2023 PCSX2 Dev Team
// SPDX-License-Identifier: LGPL-3.0+
// This file abstracts the floating-point control registers, known as MXCSR on x86, and FPCR on AArch64.
#pragma once
#include "Pcsx2Defs.h"
#include "VectorIntrin.h"
enum class FPRoundMode : u8
{
Nearest,
NegativeInfinity,
PositiveInfinity,
ChopZero,
MaxCount
};
struct FPControlRegister
{
#if defined(_M_X86) || defined(_M_X64) || defined(_M_AMD64) || defined(__amd64__) || defined(__x86_64__) || defined(__x86_64)
u32 bitmask;
static constexpr u32 EXCEPTION_MASK = (0x3Fu << 7);
static constexpr u32 ROUNDING_CONTROL_SHIFT = 13;
static constexpr u32 ROUNDING_CONTROL_MASK = 3u;
static constexpr u32 ROUNDING_CONTROL_BITS = (ROUNDING_CONTROL_MASK << ROUNDING_CONTROL_SHIFT);
static constexpr u32 DENORMALS_ARE_ZERO_BIT = (1u << 6);
static constexpr u32 FLUSH_TO_ZERO_BIT = (1u << 15);
__fi static FPControlRegister GetCurrent()
{
return FPControlRegister{_mm_getcsr()};
}
__fi static void SetCurrent(FPControlRegister value)
{
_mm_setcsr(value.bitmask);
}
__fi static constexpr FPControlRegister GetDefault()
{
// 0x1f80 - all exceptions masked, nearest rounding
return FPControlRegister{0x1f80};
}
__fi constexpr FPControlRegister& EnableExceptions()
{
bitmask &= ~EXCEPTION_MASK;
return *this;
}
__fi constexpr FPControlRegister DisableExceptions()
{
bitmask |= EXCEPTION_MASK;
return *this;
}
__fi constexpr FPRoundMode GetRoundMode() const
{
return static_cast<FPRoundMode>((bitmask >> ROUNDING_CONTROL_SHIFT) & ROUNDING_CONTROL_MASK);
}
__fi constexpr FPControlRegister& SetRoundMode(FPRoundMode mode)
{
// These bits match on x86.
bitmask = (bitmask & ~ROUNDING_CONTROL_BITS) | ((static_cast<u32>(mode) & ROUNDING_CONTROL_MASK) << ROUNDING_CONTROL_SHIFT);
return *this;
}
__fi constexpr bool GetDenormalsAreZero() const
{
return ((bitmask & DENORMALS_ARE_ZERO_BIT) != 0);
}
__fi constexpr FPControlRegister SetDenormalsAreZero(bool daz)
{
if (daz)
bitmask |= DENORMALS_ARE_ZERO_BIT;
else
bitmask &= ~DENORMALS_ARE_ZERO_BIT;
return *this;
}
__fi constexpr bool GetFlushToZero() const
{
return ((bitmask & FLUSH_TO_ZERO_BIT) != 0);
}
__fi constexpr FPControlRegister SetFlushToZero(bool ftz)
{
if (ftz)
bitmask |= FLUSH_TO_ZERO_BIT;
else
bitmask &= ~FLUSH_TO_ZERO_BIT;
return *this;
}
__fi constexpr bool operator==(const FPControlRegister& rhs) const { return bitmask == rhs.bitmask; }
__fi constexpr bool operator!=(const FPControlRegister& rhs) const { return bitmask != rhs.bitmask; }
#elif (defined(_M_ARM64) || defined(__aarch64__))
u64 bitmask;
static constexpr u64 FZ_BIT = (0x1ULL << 24);
static constexpr u32 RMODE_SHIFT = 22;
static constexpr u64 RMODE_MASK = 0x3ULL;
static constexpr u64 RMODE_BITS = (RMODE_MASK << RMODE_SHIFT);
static constexpr u32 EXCEPTION_MASK = (0x3Fu << 5);
__fi static FPControlRegister GetCurrent()
{
u64 value;
asm volatile("\tmrs %0, FPCR\n"
: "=r"(value));
return FPControlRegister{value};
}
__fi static void SetCurrent(FPControlRegister value)
{
asm volatile("\tmsr FPCR, %0\n" ::"r"(value.bitmask));
}
__fi static constexpr FPControlRegister GetDefault()
{
// 0x0 - all exceptions masked, nearest rounding
return FPControlRegister{0x0};
}
__fi constexpr FPControlRegister& EnableExceptions()
{
bitmask |= EXCEPTION_MASK;
return *this;
}
__fi constexpr FPControlRegister& DisableExceptions()
{
bitmask &= ~EXCEPTION_MASK;
return *this;
}
__fi constexpr FPRoundMode GetRoundMode() const
{
// Negative/Positive infinity rounding is flipped on A64.
const u64 RMode = (bitmask >> RMODE_SHIFT) & RMODE_MASK;
return static_cast<FPRoundMode>((RMode == 0b00 || RMode == 0b11) ? RMode : (RMode ^ 0b11));
}
__fi constexpr FPControlRegister& SetRoundMode(FPRoundMode mode)
{
const u64 RMode = ((mode == FPRoundMode::Nearest || mode == FPRoundMode::ChopZero) ? static_cast<u64>(mode) : (static_cast<u64>(mode) ^ 0b11));
bitmask = (bitmask & ~RMODE_BITS) | ((RMode & RMODE_MASK) << RMODE_SHIFT);
return *this;
}
__fi constexpr bool GetDenormalsAreZero() const
{
// Without FEAT_AFP, most ARM chips don't have separate DaZ/FtZ. This includes Apple Silicon, which
// implements x86-like behavior with a vendor-specific extension that we cannot access from usermode.
// The FZ bit causes both inputs and outputs to be flushed to zero.
return ((bitmask & FZ_BIT) != 0);
}
__fi constexpr FPControlRegister SetDenormalsAreZero(bool daz)
{
if (daz)
bitmask |= FZ_BIT;
else
bitmask &= ~FZ_BIT;
return *this;
}
__fi constexpr bool GetFlushToZero() const
{
// See note in GetDenormalsAreZero().
return ((bitmask & FZ_BIT) != 0);
}
__fi constexpr FPControlRegister SetFlushToZero(bool ftz)
{
if (ftz)
bitmask |= FZ_BIT;
else
bitmask &= ~FZ_BIT;
return *this;
}
__fi constexpr bool operator==(const FPControlRegister& rhs) const { return bitmask == rhs.bitmask; }
__fi constexpr bool operator!=(const FPControlRegister& rhs) const { return bitmask != rhs.bitmask; }
#else
#error Unknown architecture.
#endif
};
/// Helper to back up/restore FPCR.
class FPControlRegisterBackup
{
public:
__fi FPControlRegisterBackup(FPControlRegister new_value)
: m_prev_val(FPControlRegister::GetCurrent())
{
FPControlRegister::SetCurrent(new_value);
}
__fi ~FPControlRegisterBackup()
{
FPControlRegister::SetCurrent(m_prev_val);
}
FPControlRegisterBackup(const FPControlRegisterBackup&) = delete;
FPControlRegisterBackup& operator=(const FPControlRegisterBackup&) = delete;
private:
FPControlRegister m_prev_val;
};