From fd092f3465d52f2e1040e9d6a29f057562292077 Mon Sep 17 00:00:00 2001 From: Rad0van Date: Sat, 6 Jun 2026 13:50:57 +0200 Subject: [PATCH 1/4] AArch64: identify Apple M2 Pro/Max and use a concrete -mcpu The CPU table lacked the Apple M2 Pro/Max cores (MIDR part 0x038/0x039), so they were reported as "Unidentified cores" and the JIT fell back to a generic -mcpu=cortex-a78. Add those parts and a new aarch64::get_cpu_llvm_name() that maps the detected Apple SoC family to the matching LLVM CPU (apple-m1..m4); fallback_cpu_detection() now uses it so the recompilers get the correct scheduling model on Apple silicon instead of a generic core. Co-Authored-By: Claude Opus 4.8 (1M context) --- Utilities/JITLLVM.cpp | 8 ++++ .../CPU/Backends/AArch64/AArch64Common.cpp | 46 +++++++++++++++++++ .../Emu/CPU/Backends/AArch64/AArch64Common.h | 4 ++ 3 files changed, 58 insertions(+) diff --git a/Utilities/JITLLVM.cpp b/Utilities/JITLLVM.cpp index 31fb769c5a..23eadc7cbd 100644 --- a/Utilities/JITLLVM.cpp +++ b/Utilities/JITLLVM.cpp @@ -1044,6 +1044,14 @@ const char * fallback_cpu_detection() return s_result.c_str(); #else + // Prefer a concrete host -mcpu (e.g. apple-m2) so LLVM uses the right scheduling model. + // MIDR_EL1 is exposed via sysfs on Linux; see aarch64::get_cpu_llvm_name(). + static const std::string s_mcpu = aarch64::get_cpu_llvm_name(); + if (!s_mcpu.empty()) + { + return s_mcpu.c_str(); + } + // TODO: Read the data from /proc/cpuinfo. ARM CPU registers are not accessible from usermode. // This will be a pain when supporting snapdragon on windows but we'll cross that bridge when we get there. // Require at least armv8-2a. Older chips are going to be useless anyway. diff --git a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp index e2468655a8..7b7d2c03d9 100644 --- a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp +++ b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp @@ -83,6 +83,8 @@ namespace aarch64 { 0x61, 0x25, "armv8.5-a", "M1 Pro", "Icestorm" }, { 0x61, 0x32, "armv8.5-a", "M2", "Avalanche" }, { 0x61, 0x33, "armv8.5-a", "M2", "Blizzard" }, + { 0x61, 0x38, "armv8.5-a+fp16+bf16+i8mm+dotprod", "M2 Pro/Max", "Avalanche" }, + { 0x61, 0x39, "armv8.5-a+fp16+bf16+i8mm+dotprod", "M2 Pro/Max", "Blizzard" }, // QUALCOMM { 0x51, 0x01, "armv8.5-a", "Snapdragon", "X-Elite" }, @@ -239,6 +241,40 @@ namespace aarch64 result += suffix; return result; } + + std::string get_cpu_llvm_name() + { + std::map core_layout; + for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) + { + const auto midr = read_MIDR_EL1(i); + if (midr == umax) + { + break; + } + core_layout[midr]++; + } + + for (const auto& [midr, count] : core_layout) + { + const auto implementer_id = (midr >> 24) & 0xff; + const auto part_id = (midr >> 4) & 0xfff; + const auto part_info = find_cpu_part(implementer_id, part_id); + if (!part_info || implementer_id != 0x61 || !part_info->family) + { + continue; + } + + // Map Apple SoC family to an LLVM -mcpu so the JIT schedules for the real microarchitecture. + const std::string family = part_info->family; + if (family.starts_with("M1")) return "apple-m1"; + if (family.starts_with("M2")) return "apple-m2"; + if (family.starts_with("M3")) return "apple-m3"; + if (family.starts_with("M4")) return "apple-m4"; + } + + return {}; + } #else static std::string sysctl_s(const std::string_view& variable_name) { @@ -297,5 +333,15 @@ namespace aarch64 return fmt::format("%s (%lluP+%lluE)", brand, pcores, ecores); } + + std::string get_cpu_llvm_name() + { + const auto brand = sysctl_s("machdep.cpu.brand_string"); + if (brand.find("M1") != umax) return "apple-m1"; + if (brand.find("M2") != umax) return "apple-m2"; + if (brand.find("M3") != umax) return "apple-m3"; + if (brand.find("M4") != umax) return "apple-m4"; + return {}; + } #endif } diff --git a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.h b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.h index 2ce4fa68b3..d0b525e5bf 100644 --- a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.h +++ b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.h @@ -39,4 +39,8 @@ namespace aarch64 std::string get_cpu_name(); std::string get_cpu_brand(); + + // Returns a concrete LLVM -mcpu name for the host (e.g. "apple-m2") or "" if unknown. + // Used to give the JIT the correct scheduling model instead of a generic fallback. + std::string get_cpu_llvm_name(); } From 9073c8b3b3648bc5212ada434d8af3bcc4369f6b Mon Sep 17 00:00:00 2001 From: Rad0van Date: Sun, 7 Jun 2026 18:37:28 +0200 Subject: [PATCH 2/4] AArch64: tighten Apple CPU matching and add a fallback Address review feedback: - macOS path: machdep.cpu.brand_string is "Apple " (e.g. "Apple M2 Max", "Apple M1 Pro", "Apple M4"). Strip the "Apple " vendor prefix and match the family token with starts_with instead of a substring-anywhere find. - Both paths: any chip recognized as Apple silicon (implementer 0x61 / "Apple"-branded brand_string) that isn't a known M1-M4 now falls back to a conservative apple-m1 baseline instead of dropping to the generic Cortex default. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CPU/Backends/AArch64/AArch64Common.cpp | 48 ++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp index 7b7d2c03d9..1824228228 100644 --- a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp +++ b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp @@ -255,24 +255,42 @@ namespace aarch64 core_layout[midr]++; } + bool is_apple_silicon = false; + for (const auto& [midr, count] : core_layout) { const auto implementer_id = (midr >> 24) & 0xff; const auto part_id = (midr >> 4) & 0xfff; + + // This helper only hand-picks an -mcpu for Apple silicon (implementer 0x61) for now. + if (implementer_id != 0x61) + { + continue; + } + + is_apple_silicon = true; + const auto part_info = find_cpu_part(implementer_id, part_id); - if (!part_info || implementer_id != 0x61 || !part_info->family) + if (!part_info || !part_info->family) { continue; } // Map Apple SoC family to an LLVM -mcpu so the JIT schedules for the real microarchitecture. - const std::string family = part_info->family; + const std::string_view family = part_info->family; if (family.starts_with("M1")) return "apple-m1"; if (family.starts_with("M2")) return "apple-m2"; if (family.starts_with("M3")) return "apple-m3"; if (family.starts_with("M4")) return "apple-m4"; } + // Recognized as Apple silicon but not a known M-series generation (newer M-series, A-series, etc.): + // fall back to a conservative Apple baseline instead of a generic Cortex core. + if (is_apple_silicon) + { + return "apple-m1"; + } + return {}; } #else @@ -336,11 +354,29 @@ namespace aarch64 std::string get_cpu_llvm_name() { + // machdep.cpu.brand_string on Apple silicon is "Apple ", e.g. + // "Apple M2 Max", "Apple M1 Pro", "Apple M4". Match the family token right after + // the "Apple " vendor prefix instead of a substring-anywhere find. const auto brand = sysctl_s("machdep.cpu.brand_string"); - if (brand.find("M1") != umax) return "apple-m1"; - if (brand.find("M2") != umax) return "apple-m2"; - if (brand.find("M3") != umax) return "apple-m3"; - if (brand.find("M4") != umax) return "apple-m4"; + + std::string_view family = brand; + if (family.starts_with("Apple ")) + { + family.remove_prefix(6); + } + + if (family.starts_with("M1")) return "apple-m1"; + if (family.starts_with("M2")) return "apple-m2"; + if (family.starts_with("M3")) return "apple-m3"; + if (family.starts_with("M4")) return "apple-m4"; + + // Unrecognized but Apple-branded silicon (newer M-series, A-series, etc.): pick a + // conservative Apple baseline instead of falling through to a generic Cortex core. + if (brand.starts_with("Apple")) + { + return "apple-m1"; + } + return {}; } #endif From 6872a0fe45dfc70cab5640087fc9ad274c7f355d Mon Sep 17 00:00:00 2001 From: Rad0van Date: Tue, 9 Jun 2026 23:40:29 +0200 Subject: [PATCH 3/4] AArch64: dedup CPU identification, derive -mcpu from the family token Per review (@kd-11, @Megamouse): extract the shared MIDR->part lookup into get_cpu_part_info(); get_cpu_name() becomes get_cpu_part_info()->name. get_cpu_llvm_name() now derives the LLVM target from the family token instead of a hardcoded M1-M4 list: "apple-" + to_lower(split(family, " ")[0]). So "M2 Pro/Max" -> apple-m2, and future Apple generations map automatically once added to the part table (the macOS sysctl path uses the same idiom on the brand string, so A-series -> apple-aN etc.). Non-Apple parts return empty. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../CPU/Backends/AArch64/AArch64Common.cpp | 102 ++++++------------ 1 file changed, 34 insertions(+), 68 deletions(-) diff --git a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp index 1824228228..a77b85034d 100644 --- a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp +++ b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp @@ -138,7 +138,9 @@ namespace aarch64 #endif } - std::string get_cpu_name() + // Resolve the representative CPU part entry for this machine (the "lowest" core in a + // big.LITTLE layout), or nullptr if the layout can't be fully identified. + static const cpu_entry_t* get_cpu_part_info() { std::map core_layout; for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) @@ -154,10 +156,10 @@ namespace aarch64 if (core_layout.empty()) { - return {}; + return nullptr; } - const cpu_entry_t* lowest_part_info = nullptr; + const cpu_entry_t* lowest_part_info = nullptr; for (const auto& [midr, count] : core_layout) { const auto implementer_id = (midr >> 24) & 0xff; @@ -166,7 +168,7 @@ namespace aarch64 const auto part_info = find_cpu_part(implementer_id, part_id); if (!part_info) { - return {}; + return nullptr; } if (lowest_part_info == nullptr || lowest_part_info > part_info) @@ -175,7 +177,13 @@ namespace aarch64 } } - return lowest_part_info ? lowest_part_info->name : ""; + return lowest_part_info; + } + + std::string get_cpu_name() + { + const auto part_info = get_cpu_part_info(); + return part_info ? part_info->name : ""; } std::string get_cpu_brand() @@ -244,54 +252,17 @@ namespace aarch64 std::string get_cpu_llvm_name() { - std::map core_layout; - for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) + const auto part_info = get_cpu_part_info(); + + // Only Apple silicon (implementer 0x61) gets a hand-picked -mcpu for now. The family + // field holds the SoC generation ("M2", "M2 Pro/Max", ...) whose first token maps + // directly onto the LLVM "apple-mN" targets (and auto-extends to future generations). + if (!part_info || part_info->vendor != 0x61 || !part_info->family || !part_info->family[0]) { - const auto midr = read_MIDR_EL1(i); - if (midr == umax) - { - break; - } - core_layout[midr]++; + return {}; } - bool is_apple_silicon = false; - - for (const auto& [midr, count] : core_layout) - { - const auto implementer_id = (midr >> 24) & 0xff; - const auto part_id = (midr >> 4) & 0xfff; - - // This helper only hand-picks an -mcpu for Apple silicon (implementer 0x61) for now. - if (implementer_id != 0x61) - { - continue; - } - - is_apple_silicon = true; - - const auto part_info = find_cpu_part(implementer_id, part_id); - if (!part_info || !part_info->family) - { - continue; - } - - // Map Apple SoC family to an LLVM -mcpu so the JIT schedules for the real microarchitecture. - const std::string_view family = part_info->family; - if (family.starts_with("M1")) return "apple-m1"; - if (family.starts_with("M2")) return "apple-m2"; - if (family.starts_with("M3")) return "apple-m3"; - if (family.starts_with("M4")) return "apple-m4"; - } - - // Recognized as Apple silicon but not a known M-series generation (newer M-series, A-series, etc.): - // fall back to a conservative Apple baseline instead of a generic Cortex core. - if (is_apple_silicon) - { - return "apple-m1"; - } - - return {}; + return fmt::format("apple-%s", fmt::to_lower(fmt::split(part_info->family, {" "}).front())); } #else static std::string sysctl_s(const std::string_view& variable_name) @@ -354,30 +325,25 @@ namespace aarch64 std::string get_cpu_llvm_name() { - // machdep.cpu.brand_string on Apple silicon is "Apple ", e.g. - // "Apple M2 Max", "Apple M1 Pro", "Apple M4". Match the family token right after - // the "Apple " vendor prefix instead of a substring-anywhere find. + // machdep.cpu.brand_string on Apple silicon is "Apple ", e.g. "Apple M2 Max", + // "Apple M1 Pro", "Apple M4". The generation token after the "Apple " vendor prefix maps + // directly onto the LLVM "apple-mN"/"apple-aN" targets. const auto brand = sysctl_s("machdep.cpu.brand_string"); std::string_view family = brand; - if (family.starts_with("Apple ")) + if (!family.starts_with("Apple ")) { - family.remove_prefix(6); + return {}; + } + family.remove_prefix(6); + + const auto tokens = fmt::split(family, {" "}); + if (tokens.empty()) + { + return {}; } - if (family.starts_with("M1")) return "apple-m1"; - if (family.starts_with("M2")) return "apple-m2"; - if (family.starts_with("M3")) return "apple-m3"; - if (family.starts_with("M4")) return "apple-m4"; - - // Unrecognized but Apple-branded silicon (newer M-series, A-series, etc.): pick a - // conservative Apple baseline instead of falling through to a generic Cortex core. - if (brand.starts_with("Apple")) - { - return "apple-m1"; - } - - return {}; + return fmt::format("apple-%s", fmt::to_lower(tokens.front())); } #endif } From 7ef71465026d0ab80d2c33121b4ef58f9deac290 Mon Sep 17 00:00:00 2001 From: Rad0van Date: Wed, 10 Jun 2026 10:16:54 +0200 Subject: [PATCH 4/4] AArch64: use fmt::split_sv for the family token Apply review suggestions (@Megamouse): split_sv returns string_views (no per-token std::string allocation), and to_lower takes a string_view, so the generation token is extracted without heap churn. Both the Linux and macOS paths guard against an empty token list. Co-Authored-By: Claude Opus 4.8 (1M context) --- rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp index a77b85034d..7350aa7926 100644 --- a/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp +++ b/rpcs3/Emu/CPU/Backends/AArch64/AArch64Common.cpp @@ -262,7 +262,13 @@ namespace aarch64 return {}; } - return fmt::format("apple-%s", fmt::to_lower(fmt::split(part_info->family, {" "}).front())); + const auto tokens = fmt::split_sv(part_info->family, {" "}); + if (tokens.empty()) + { + return {}; + } + + return fmt::format("apple-%s", fmt::to_lower(tokens.front())); } #else static std::string sysctl_s(const std::string_view& variable_name) @@ -337,7 +343,7 @@ namespace aarch64 } family.remove_prefix(6); - const auto tokens = fmt::split(family, {" "}); + const auto tokens = fmt::split_sv(family, {" "}); if (tokens.empty()) { return {};