aboutsummaryrefslogtreecommitdiff
path: root/src/dynarmic
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 11:46:04 -0400
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-10-27 11:46:04 -0400
commit11aac324010eb2fe3c0fc9dca3c196f33aaef10c (patch)
tree92a5fbba6b08ea26c3628b1b7991fcd5ec1b55fc /src/dynarmic
parenta05231f8782178d5a074de16aaba2ae0b118ccd4 (diff)
dynarmic: decode ARMv8.1-8.5 instructions used by Switch 2 code
Switch 2 titles are built for a Cortex-A78C class core. Scanning one title's .text with dynarmic's own decoder table found ~115k RETAB, ~37k LDAPR, ~25k LSE atomics, ~6.6k CAS, ~2.5k SWP plus a few CASP/XPAC/PAC/STG, none of which dynarmic decoded (only the declarations existed). - LSE atomics, SWP, CAS, CASP: exclusive load + op + exclusive store; on store failure no register is written and the instruction is retried (CheckBit terminal), so it stays atomic across cores. - LDAPR: ordered load, like LDAR. - Pointer authentication: identity (nothing is ever signed). RETAA/RETAB, BRA*/BLRA*, LDRAA/LDRAB, PAC*/AUT*/XPAC*, PACGA. - MTE: tags are always 0; STG/ST2G only write back, STZG/STZ2G zero the granules, STGP stores the pair, IRG/GMI/ADDG/SUBG ignore tags. tools/nx2/a64_coverage.py reports which encodings in an NSO's .text the decoder table does not match.
Diffstat (limited to 'src/dynarmic')
-rw-r--r--src/dynarmic/src/dynarmic/frontend/A64/decoder/a64.inc18
-rw-r--r--src/dynarmic/src/dynarmic/frontend/A64/translate/a64_translate.cpp3
-rw-r--r--src/dynarmic/src/dynarmic/frontend/A64/translate/impl/impl.h11
-rw-r--r--src/dynarmic/src/dynarmic/frontend/A64/translate/impl/load_store_atomic.cpp190
-rw-r--r--src/dynarmic/src/dynarmic/frontend/A64/translate/impl/memory_tagging.cpp98
-rw-r--r--src/dynarmic/src/dynarmic/frontend/A64/translate/impl/pointer_authentication.cpp73
6 files changed, 393 insertions, 0 deletions
diff --git a/src/dynarmic/src/dynarmic/frontend/A64/decoder/a64.inc b/src/dynarmic/src/dynarmic/frontend/A64/decoder/a64.inc
index 8a3fab7ac1..ee9f0faa18 100644
--- a/src/dynarmic/src/dynarmic/frontend/A64/decoder/a64.inc
+++ b/src/dynarmic/src/dynarmic/frontend/A64/decoder/a64.inc
@@ -2,6 +2,24 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// DO NOT REORDER
+// ARMv8.1+ encodings used by Switch 2 code. Kept first so they win over broader patterns.
+INST(LDAPR_sz, "LDAPRB, LDAPRH, LDAPR", "zz11100010111111110000nnnnnttttt")
+INST(AtomicMemOp, "LDADD/LDCLR/LDEOR/LDSET/LDSMAX/LDSMIN/LDUMAX/LDUMIN/SWP", "zz111000AR1sssssoooo00nnnnnttttt")
+INST(CompareAndSwap, "CASB, CASH, CAS", "zz0010001L1sssssO11111nnnnnttttt")
+INST(CompareAndSwapPair, "CASP", "0z0010000L1sssssO11111nnnnnttttt")
+INST(PACAUT_reg, "PACIA/PACIB/PACDA/PACDB/AUTIA/AUTIB/AUTDA/AUTDB", "110110101100000100Zooonnnnnddddd")
+INST(XPAC_1, "XPACI, XPACD", "110110101100000101000D11111ddddd")
+INST(PACGA, "PACGA", "10011010110mmmmm001100nnnnnddddd")
+INST(RETA, "RETAA, RETAB", "110101100101111100001M1111111111")
+INST(BRA_BLRA, "BRAA/BRAB/BLRAA/BLRAB (+Z)", "1101011Z00L1111100001Mnnnnnmmmmm")
+INST(LDRA, "LDRAA, LDRAB", "11111000MS1iiiiiiiiiW1nnnnnttttt")
+INST(STG_family, "STG/STZG/ST2G/STZ2G/LDG", "11011001oo1iiiiiiiiixxnnnnnttttt")
+INST(STGP, "STGP", "0110100xx0iiiiiiiuuuuunnnnnttttt")
+INST(IRG, "IRG", "10011010110mmmmm000100nnnnnddddd")
+INST(GMI, "GMI", "10011010110mmmmm000101nnnnnddddd")
+INST(ADDG, "ADDG", "1001000110iiiiii00ttttnnnnnddddd")
+INST(SUBG, "SUBG", "1101000110iiiiii00ttttnnnnnddddd")
+
INST(FMOV_3, "FMOV (vector, immediate)", "0Q00111100000abc111111defghddddd")
INST(FMOV_2, "FMOV (vector, immediate)", "0Qo0111100000abc111101defghddddd")
INST(MOVI, "MOVI, MVNI, ORR, BIC (vector, immediate)", "0Qo0111100000abcmmmm01defghddddd")
diff --git a/src/dynarmic/src/dynarmic/frontend/A64/translate/a64_translate.cpp b/src/dynarmic/src/dynarmic/frontend/A64/translate/a64_translate.cpp
index 7840c63d79..70d5098a65 100644
--- a/src/dynarmic/src/dynarmic/frontend/A64/translate/a64_translate.cpp
+++ b/src/dynarmic/src/dynarmic/frontend/A64/translate/a64_translate.cpp
@@ -82,6 +82,9 @@ bool TranslateSingleInstruction(IR::Block& block, LocationDescriptor descriptor,
#include "dynarmic/frontend/A64/translate/impl/impl.cpp"
#include "dynarmic/frontend/A64/translate/impl/impl.h"
#include "dynarmic/frontend/A64/translate/impl/load_store_exclusive.cpp"
+#include "dynarmic/frontend/A64/translate/impl/load_store_atomic.cpp"
+#include "dynarmic/frontend/A64/translate/impl/memory_tagging.cpp"
+#include "dynarmic/frontend/A64/translate/impl/pointer_authentication.cpp"
#include "dynarmic/frontend/A64/translate/impl/load_store_load_literal.cpp"
#include "dynarmic/frontend/A64/translate/impl/load_store_multiple_structures.cpp"
#include "dynarmic/frontend/A64/translate/impl/load_store_no_allocate_pair.cpp"
diff --git a/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/impl.h b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/impl.h
index c8afef3283..d8e6f2b739 100644
--- a/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/impl.h
+++ b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/impl.h
@@ -326,6 +326,17 @@ struct TranslatorVisitor final {
bool SWP(bool A, bool R, Reg Rs, Reg Rn, Reg Rt);
bool LDAPR(Reg Rn, Reg Rt);
+ // ARMv8.1+ forms decoded for Switch 2 code (load_store_atomic.cpp, pointer_authentication.cpp,
+ // memory_tagging.cpp)
+ bool AtomicMemOp(Imm<2> size, bool A, bool R, Reg Rs, Imm<4> op, Reg Rn, Reg Rt);
+ bool LDAPR_sz(Imm<2> size, Reg Rn, Reg Rt);
+ bool CompareAndSwap(Imm<2> size, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt);
+ bool CompareAndSwapPair(bool sz, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt);
+ bool PACAUT_reg(bool Z, Imm<3> op, Reg Rn, Reg Rd);
+ bool BRA_BLRA(bool Z, bool L, bool M, Reg Rn, Reg Rm);
+ bool STG_family(Imm<2> opc, Imm<9> imm9, Imm<2> mode, Reg Rn, Reg Rt);
+ bool STGP(Imm<2> mode, Imm<7> imm7, Reg Rt2, Reg Rn, Reg Rt);
+
// Loads and stores - Load/Store register (register offset)
bool STRx_reg(Imm<2> size, Imm<1> opc_1, Reg Rm, Imm<3> option, bool S, Reg Rn, Reg Rt);
bool LDRx_reg(Imm<2> size, Imm<1> opc_1, Reg Rm, Imm<3> option, bool S, Reg Rn, Reg Rt);
diff --git a/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/load_store_atomic.cpp b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/load_store_atomic.cpp
new file mode 100644
index 0000000000..eafc993988
--- /dev/null
+++ b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/load_store_atomic.cpp
@@ -0,0 +1,190 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// ARMv8.1 LSE atomics (LDADD/LDCLR/LDEOR/LDSET/LD{S,U}{MAX,MIN}/SWP, CAS, CASP) and
+// ARMv8.3 RCpc LDAPR.
+//
+// Each read-modify-write is built from an exclusive load and an exclusive store. If the
+// store fails (another core touched the monitor in between) no register is changed and the
+// same instruction is executed again, so the operation stays atomic with multiple cores.
+
+#include "dynarmic/frontend/A64/translate/impl/impl.h"
+
+namespace Dynarmic::A64 {
+
+static Reg AtomicNextReg(Reg r) {
+ return static_cast<Reg>(static_cast<size_t>(r) + 1);
+}
+
+static IR::U64 AtomicAddress(TranslatorVisitor& v, Reg Rn) {
+ return Rn == Reg::SP ? IR::U64{v.SP(64)} : IR::U64{v.X(64, Rn)};
+}
+
+// All-ones when value == 0, otherwise zero.
+static IR::U64 AtomicMaskIfZero(TranslatorVisitor& v, IR::U64 value) {
+ const IR::U64 nonzero = v.ir.LogicalShiftRight(v.ir.Or(value, v.ir.Sub(v.ir.Imm64(0), value)), v.ir.Imm8(63));
+ return v.ir.Sub(nonzero, v.ir.Imm64(1));
+}
+
+static IR::U64 AtomicSelect(TranslatorVisitor& v, IR::U64 mask, IR::U64 if_set, IR::U64 if_clear) {
+ return v.ir.Or(v.ir.And(if_set, mask), v.ir.AndNot(if_clear, mask));
+}
+
+static IR::UAny AtomicTruncate(TranslatorVisitor& v, IR::U64 value, size_t bytes) {
+ switch (bytes) {
+ case 1:
+ return v.ir.LeastSignificantByte(value);
+ case 2:
+ return v.ir.LeastSignificantHalf(value);
+ case 4:
+ return v.ir.LeastSignificantWord(value);
+ default:
+ return value;
+ }
+}
+
+static IR::U64 AtomicLoadZext(TranslatorVisitor& v, IR::U64 address, size_t bytes, IR::AccType acctype) {
+ const IR::UAny value = v.ExclusiveMem(address, bytes, acctype);
+ return bytes == 8 ? IR::U64{value} : v.ir.ZeroExtendToLong(value);
+}
+
+static IR::U64 AtomicSignExtend(TranslatorVisitor& v, IR::U64 value, size_t bytes) {
+ return bytes == 8 ? value : v.ir.SignExtendToLong(AtomicTruncate(v, value, bytes));
+}
+
+static IR::U64 AtomicZeroExtendReg(TranslatorVisitor& v, Reg r, size_t bytes) {
+ const IR::U64 value = v.X(64, r);
+ return bytes == 8 ? value : v.ir.ZeroExtendToLong(AtomicTruncate(v, value, bytes));
+}
+
+// Writes `value` to `r` only when the exclusive store succeeded (ok_mask all-ones).
+static void AtomicCommitReg(TranslatorVisitor& v, Reg r, IR::U64 ok_mask, IR::U64 value) {
+ if (r == Reg::ZR) {
+ return;
+ }
+ v.X(64, r, AtomicSelect(v, ok_mask, value, v.X(64, r)));
+}
+
+// Ends the block: continue with the next instruction on success, retry this one on failure.
+static bool AtomicFinish(TranslatorVisitor& v, IR::U32 status) {
+ v.ir.SetCheckBit(v.ir.IsZero(status));
+ v.ir.SetTerm(IR::Term::CheckBit{IR::Term::LinkBlock{v.ir.current_location->AdvancePC(4)},
+ IR::Term::LinkBlock{*v.ir.current_location}});
+ return false;
+}
+
+static IR::AccType AtomicAccType(bool acquire, bool release) {
+ return acquire || release ? IR::AccType::ORDERED : IR::AccType::ATOMIC;
+}
+
+bool TranslatorVisitor::AtomicMemOp(Imm<2> size, bool A, bool R, Reg Rs, Imm<4> op, Reg Rn, Reg Rt) {
+ const size_t bytes = size_t{1} << size.ZeroExtend<size_t>();
+ const size_t opv = op.ZeroExtend<size_t>();
+ if (opv > 0b1000) {
+ return UnallocatedEncoding();
+ }
+
+ const IR::U64 address = AtomicAddress(*this, Rn);
+ const IR::AccType acctype = AtomicAccType(A, R);
+ const IR::U64 old = AtomicLoadZext(*this, address, bytes, acctype);
+ const IR::U64 s = AtomicZeroExtendReg(*this, Rs, bytes);
+
+ IR::U64 result;
+ switch (opv) {
+ case 0b0000: // LDADD
+ result = ir.Add(old, s);
+ break;
+ case 0b0001: // LDCLR
+ result = ir.AndNot(old, s);
+ break;
+ case 0b0010: // LDEOR
+ result = ir.Eor(old, s);
+ break;
+ case 0b0011: // LDSET
+ result = ir.Or(old, s);
+ break;
+ case 0b0100: // LDSMAX
+ result = ir.MaxSigned(AtomicSignExtend(*this, old, bytes), AtomicSignExtend(*this, s, bytes));
+ break;
+ case 0b0101: // LDSMIN
+ result = ir.MinSigned(AtomicSignExtend(*this, old, bytes), AtomicSignExtend(*this, s, bytes));
+ break;
+ case 0b0110: // LDUMAX
+ result = ir.MaxUnsigned(old, s);
+ break;
+ case 0b0111: // LDUMIN
+ result = ir.MinUnsigned(old, s);
+ break;
+ default: // SWP
+ result = s;
+ break;
+ }
+
+ const IR::U32 status = ExclusiveMem(address, bytes, acctype, AtomicTruncate(*this, result, bytes));
+ const IR::U64 ok = AtomicMaskIfZero(*this, ir.ZeroExtendToLong(status));
+ AtomicCommitReg(*this, Rt, ok, old);
+ return AtomicFinish(*this, status);
+}
+
+bool TranslatorVisitor::LDAPR_sz(Imm<2> size, Reg Rn, Reg Rt) {
+ const size_t bytes = size_t{1} << size.ZeroExtend<size_t>();
+ const size_t regsize = bytes == 8 ? 64 : 32;
+ const IR::UAny data = Mem(AtomicAddress(*this, Rn), bytes, IR::AccType::ORDERED);
+ X(regsize, Rt, ZeroExtend(data, regsize));
+ return true;
+}
+
+bool TranslatorVisitor::CompareAndSwap(Imm<2> size, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt) {
+ const size_t bytes = size_t{1} << size.ZeroExtend<size_t>();
+ const IR::U64 address = AtomicAddress(*this, Rn);
+ const IR::AccType acctype = AtomicAccType(L, o0);
+
+ const IR::U64 old = AtomicLoadZext(*this, address, bytes, acctype);
+ const IR::U64 expected = AtomicZeroExtendReg(*this, Rs, bytes);
+ const IR::U64 desired = AtomicZeroExtendReg(*this, Rt, bytes);
+ const IR::U64 equal = AtomicMaskIfZero(*this, ir.Eor(old, expected));
+ const IR::U64 result = AtomicSelect(*this, equal, desired, old);
+
+ const IR::U32 status = ExclusiveMem(address, bytes, acctype, AtomicTruncate(*this, result, bytes));
+ const IR::U64 ok = AtomicMaskIfZero(*this, ir.ZeroExtendToLong(status));
+ AtomicCommitReg(*this, Rs, ok, old);
+ return AtomicFinish(*this, status);
+}
+
+bool TranslatorVisitor::CompareAndSwapPair(bool sz, bool L, Reg Rs, bool o0, Reg Rn, Reg Rt) {
+ if ((static_cast<size_t>(Rs) & 1) != 0 || (static_cast<size_t>(Rt) & 1) != 0) {
+ return UnallocatedEncoding();
+ }
+ const Reg Rs2 = AtomicNextReg(Rs);
+ const Reg Rt2 = AtomicNextReg(Rt);
+ const IR::U64 address = AtomicAddress(*this, Rn);
+ const IR::AccType acctype = AtomicAccType(L, o0);
+
+ if (!sz) {
+ // Two 32-bit words, accessed as one 64-bit exclusive.
+ const IR::U64 old = ExclusiveMem(address, 8, acctype);
+ const IR::U64 expected = ir.Pack2x32To1x64(X(32, Rs), X(32, Rs2));
+ const IR::U64 desired = ir.Pack2x32To1x64(X(32, Rt), X(32, Rt2));
+ const IR::U64 equal = AtomicMaskIfZero(*this, ir.Eor(old, expected));
+ const IR::U32 status = ExclusiveMem(address, 8, acctype, AtomicSelect(*this, equal, desired, old));
+ const IR::U64 ok = AtomicMaskIfZero(*this, ir.ZeroExtendToLong(status));
+ AtomicCommitReg(*this, Rs, ok, ir.ZeroExtendToLong(ir.LeastSignificantWord(old)));
+ AtomicCommitReg(*this, Rs2, ok, ir.LogicalShiftRight(old, ir.Imm8(32)));
+ return AtomicFinish(*this, status);
+ }
+
+ const IR::U128 old = ExclusiveMem(address, 16, acctype);
+ const IR::U64 old_lo = ir.VectorGetElement(64, old, 0);
+ const IR::U64 old_hi = ir.VectorGetElement(64, old, 1);
+ const IR::U64 diff = ir.Or(ir.Eor(old_lo, X(64, Rs)), ir.Eor(old_hi, X(64, Rs2)));
+ const IR::U64 equal = AtomicMaskIfZero(*this, diff);
+ const IR::U64 new_lo = AtomicSelect(*this, equal, X(64, Rt), old_lo);
+ const IR::U64 new_hi = AtomicSelect(*this, equal, X(64, Rt2), old_hi);
+ const IR::U32 status = ExclusiveMem(address, 16, acctype, ir.Pack2x64To1x128(new_lo, new_hi));
+ const IR::U64 ok = AtomicMaskIfZero(*this, ir.ZeroExtendToLong(status));
+ AtomicCommitReg(*this, Rs, ok, old_lo);
+ AtomicCommitReg(*this, Rs2, ok, old_hi);
+ return AtomicFinish(*this, status);
+}
+
+} // namespace Dynarmic::A64
diff --git a/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/memory_tagging.cpp b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/memory_tagging.cpp
new file mode 100644
index 0000000000..9c6bcb7872
--- /dev/null
+++ b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/memory_tagging.cpp
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// ARMv8.5 memory tagging, with tags ignored: every allocation tag is 0, tag stores only do
+// their address writeback, and the STZG forms still zero the granules they cover.
+
+#include "dynarmic/frontend/A64/translate/impl/impl.h"
+
+namespace Dynarmic::A64 {
+
+static IR::U64 TagBase(TranslatorVisitor& v, Reg Rn) {
+ return Rn == Reg::SP ? IR::U64{v.SP(64)} : IR::U64{v.X(64, Rn)};
+}
+
+static void TagSetBase(TranslatorVisitor& v, Reg Rn, IR::U64 value) {
+ if (Rn == Reg::SP) {
+ v.SP(64, value);
+ } else {
+ v.X(64, Rn, value);
+ }
+}
+
+bool TranslatorVisitor::STG_family(Imm<2> opc, Imm<9> imm9, Imm<2> mode, Reg Rn, Reg Rt) {
+ const size_t opcv = opc.ZeroExtend<size_t>();
+ const size_t modev = mode.ZeroExtend<size_t>();
+ if (modev == 0b00) {
+ // LDG (opc 01) leaves Xt as-is since tags are 0; STZGM/STGM/LDGM are EL1-only.
+ if (opcv == 0b01) {
+ return true;
+ }
+ return UnallocatedEncoding();
+ }
+
+ const s64 offset = imm9.SignExtend<s64>() << 4;
+ const IR::U64 base = TagBase(*this, Rn);
+ const IR::U64 offset_address = ir.Add(base, ir.Imm64(static_cast<u64>(offset)));
+ const IR::U64 address = modev == 0b01 ? base : offset_address; // 01 = post-index
+
+ const bool zero = (opcv & 1) != 0; // STZG, STZ2G
+ const size_t granules = (opcv & 2) ? 2 : 1; // ST2G, STZ2G
+ if (zero) {
+ const IR::U64 zero64 = ir.Imm64(0);
+ for (size_t i = 0; i < granules * 2; ++i) {
+ Mem(ir.Add(address, ir.Imm64(i * 8)), 8, IR::AccType::NORMAL, zero64);
+ }
+ }
+ (void)Rt;
+ if (modev != 0b10) {
+ TagSetBase(*this, Rn, offset_address);
+ }
+ return true;
+}
+
+bool TranslatorVisitor::STGP(Imm<2> mode, Imm<7> imm7, Reg Rt2, Reg Rn, Reg Rt) {
+ const size_t modev = mode.ZeroExtend<size_t>();
+ if (modev == 0b00) {
+ return UnallocatedEncoding();
+ }
+ const s64 offset = imm7.SignExtend<s64>() << 4;
+ const IR::U64 base = TagBase(*this, Rn);
+ const IR::U64 offset_address = ir.Add(base, ir.Imm64(static_cast<u64>(offset)));
+ const IR::U64 address = modev == 0b01 ? base : offset_address;
+ Mem(address, 8, IR::AccType::NORMAL, X(64, Rt));
+ Mem(ir.Add(address, ir.Imm64(8)), 8, IR::AccType::NORMAL, X(64, Rt2));
+ if (modev != 0b10) {
+ TagSetBase(*this, Rn, offset_address);
+ }
+ return true;
+}
+
+bool TranslatorVisitor::IRG(Reg Rm, Reg Rn, Reg Rd) {
+ (void)Rm;
+ TagSetBase(*this, Rd, TagBase(*this, Rn));
+ return true;
+}
+
+bool TranslatorVisitor::GMI(Reg Rm, Reg Rn, Reg Rd) {
+ (void)Rn;
+ // Tag of Xn is always 0, so the exclusion mask gains bit 0.
+ X(64, Rd, ir.Or(X(64, Rm), ir.Imm64(1)));
+ return true;
+}
+
+bool TranslatorVisitor::ADDG(Imm<6> offset_imm, Imm<4> tag_offset, Reg Rn, Reg Rd) {
+ (void)tag_offset;
+ const u64 offset = offset_imm.ZeroExtend<u64>() << 4;
+ TagSetBase(*this, Rd, ir.Add(TagBase(*this, Rn), ir.Imm64(offset)));
+ return true;
+}
+
+bool TranslatorVisitor::SUBG(Imm<6> offset_imm, Imm<4> tag_offset, Reg Rn, Reg Rd) {
+ (void)tag_offset;
+ const u64 offset = offset_imm.ZeroExtend<u64>() << 4;
+ TagSetBase(*this, Rd, ir.Sub(TagBase(*this, Rn), ir.Imm64(offset)));
+ return true;
+}
+
+} // namespace Dynarmic::A64
diff --git a/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/pointer_authentication.cpp b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/pointer_authentication.cpp
new file mode 100644
index 0000000000..e36942c942
--- /dev/null
+++ b/src/dynarmic/src/dynarmic/frontend/A64/translate/impl/pointer_authentication.cpp
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// ARMv8.3 pointer authentication, implemented as the identity: PAC* never adds a code and
+// AUT*/XPAC* have nothing to strip, so signed pointers are just plain pointers. The HINT-space
+// forms (PACIASP, AUTIBSP, ...) already decode as HINT/NOP.
+
+#include "dynarmic/frontend/A64/translate/impl/impl.h"
+
+namespace Dynarmic::A64 {
+
+bool TranslatorVisitor::PACAUT_reg(bool Z, Imm<3> op, Reg Rn, Reg Rd) {
+ (void)op;
+ (void)Rd;
+ if (Z && Rn != Reg::R31) {
+ return UnallocatedEncoding();
+ }
+ return true;
+}
+
+bool TranslatorVisitor::XPAC_1(bool D, Reg Rd) {
+ (void)D;
+ (void)Rd;
+ return true;
+}
+
+bool TranslatorVisitor::PACGA(Reg Rm, Reg Rn, Reg Rd) {
+ // Any deterministic value works, since the code is only ever compared to a recomputation.
+ const IR::U64 m = Rm == Reg::SP ? IR::U64{SP(64)} : IR::U64{X(64, Rm)};
+ X(64, Rd, ir.And(ir.Eor(X(64, Rn), m), ir.Imm64(0xFFFFFFFF00000000ULL)));
+ return true;
+}
+
+bool TranslatorVisitor::RETA(bool M) {
+ (void)M;
+ ir.SetPC(X(64, Reg::R30));
+ ir.SetTerm(IR::Term::PopRSBHint{});
+ return false;
+}
+
+bool TranslatorVisitor::BRA_BLRA(bool Z, bool L, bool M, Reg Rn, Reg Rm) {
+ (void)M;
+ if (!Z && Rm != Reg::R31) {
+ return UnallocatedEncoding();
+ }
+ const auto target = X(64, Rn);
+ if (L) {
+ X(64, Reg::R30, ir.Imm64(ir.PC() + 4));
+ ir.PushRSB(ir.current_location->AdvancePC(4));
+ }
+ ir.SetPC(target);
+ ir.SetTerm(IR::Term::FastDispatchHint{});
+ return false;
+}
+
+bool TranslatorVisitor::LDRA(bool M, bool S, Imm<9> imm9, bool W, Reg Rn, Reg Rt) {
+ (void)M;
+ const s64 offset = concatenate(Imm<1>{S ? 1u : 0u}, imm9).SignExtend<s64>() << 3;
+ const IR::U64 base = Rn == Reg::SP ? IR::U64{SP(64)} : IR::U64{X(64, Rn)};
+ const IR::U64 address = ir.Add(base, ir.Imm64(static_cast<u64>(offset)));
+ const IR::U64 data = Mem(address, 8, IR::AccType::NORMAL);
+ if (W) {
+ if (Rn == Reg::SP) {
+ SP(64, address);
+ } else {
+ X(64, Rn, address);
+ }
+ }
+ X(64, Rt, data);
+ return true;
+}
+
+} // namespace Dynarmic::A64