aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt11
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt108
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt13
-rw-r--r--src/android/app/src/main/jni/native.cpp410
-rw-r--r--src/android/app/src/main/res/layout/dialog_system_info.xml35
-rw-r--r--src/android/app/src/main/res/values/strings.xml21
7 files changed, 609 insertions, 1 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 1a56fe9e1b..18ace18393 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -201,6 +201,18 @@ object NativeLibrary {
external fun logSettings()
/**
+ * Returns Vulkan driver version / API version / GPU model
+ */
+ external fun getVulkanDriverVersion(): String
+ external fun getVulkanApiVersion(): String
+ external fun getGpuModel(): String
+
+ /**
+ * Returns a summary of detailed information about the CPU.
+ */
+ external fun getCpuSummary(): String
+
+ /**
* Checks for available updates.
*/
external fun checkForUpdate(): String?
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 1dbdd5e540..6cb35014b4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -232,6 +232,17 @@ class HomeSettingsFragment : Fragment() {
)
add(
HomeSetting(
+ R.string.system_information,
+ R.string.system_information_description,
+ R.drawable.ic_system,
+ {
+ SystemInfoDialogFragment.newInstance()
+ .show(parentFragmentManager, SystemInfoDialogFragment.TAG)
+ }
+ )
+ )
+ add(
+ HomeSetting(
R.string.about,
R.string.about_description,
R.drawable.ic_info_outline,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt
new file mode 100644
index 0000000000..5c914eeb16
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SystemInfoDialogFragment.kt
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.ActivityManager
+import android.app.Dialog
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogSystemInfoBinding
+
+class SystemInfoDialogFragment : DialogFragment() {
+ private var _binding: DialogSystemInfoBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ _binding = DialogSystemInfoBinding.inflate(layoutInflater)
+
+ populateSystemInfo()
+
+ val dialog = MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.system_information)
+ .setPositiveButton(android.R.string.ok, null)
+ .create()
+
+ dialog.setView(binding.root)
+
+ return dialog
+ }
+
+ private fun populateSystemInfo() {
+ val systemInfo = buildString {
+ // General Device Info
+ appendLine("=== ${getString(R.string.general_information)} ===")
+ appendLine("${getString(R.string.device_manufacturer)}: ${Build.MANUFACTURER}")
+ appendLine("${getString(R.string.device_model)}: ${Build.MODEL}")
+ appendLine("${getString(R.string.device_name)}: ${Build.DEVICE}")
+ appendLine("${getString(R.string.product)}: ${Build.PRODUCT}")
+ appendLine("${getString(R.string.hardware)}: ${Build.HARDWARE}")
+ appendLine("${getString(R.string.supported_abis)}: ${Build.SUPPORTED_ABIS.joinToString(", ")}")
+ appendLine("${getString(R.string.android_version)}: ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})")
+ appendLine("${getString(R.string.android_security_patch)}: ${Build.VERSION.SECURITY_PATCH}")
+ appendLine("${getString(R.string.build_id)}: ${Build.ID}")
+
+ appendLine()
+ appendLine("=== ${getString(R.string.cpu_info)} ===")
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && Build.SOC_MODEL.isNotBlank()) {
+ appendLine("${getString(R.string.soc)} ${Build.SOC_MODEL}")
+ }
+
+ val cpuSummary = NativeLibrary.getCpuSummary()
+ if (cpuSummary.isNotEmpty() && cpuSummary != "Unknown") {
+ appendLine(cpuSummary)
+ }
+
+ appendLine()
+
+ // GPU Info
+ appendLine("=== ${getString(R.string.gpu_information)} ===")
+ try {
+ val gpuModel = NativeLibrary.getGpuModel()
+ appendLine("${getString(R.string.gpu_model)}: $gpuModel")
+
+ val vulkanApi = NativeLibrary.getVulkanApiVersion()
+ appendLine("Vulkan API: $vulkanApi")
+
+ val vulkanDriver = NativeLibrary.getVulkanDriverVersion()
+ appendLine("${getString(R.string.vulkan_driver_version)}: $vulkanDriver")
+ } catch (e: Exception) {
+ appendLine("${getString(R.string.error_getting_emulator_info)}: ${e.message}")
+ }
+ appendLine()
+
+ // Memory Info
+ appendLine("=== ${getString(R.string.memory_info)} ===")
+
+ val activityManager =
+ requireContext().getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val memInfo = ActivityManager.MemoryInfo()
+ activityManager.getMemoryInfo(memInfo)
+ val totalDeviceRam = memInfo.totalMem / (1024 * 1024)
+
+ appendLine("${getString(R.string.total_memory)}: $totalDeviceRam MB")
+ }
+
+ binding.textSystemInfo.text = systemInfo
+ }
+
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ companion object {
+ const val TAG = "SystemInfoDialogFragment"
+
+ fun newInstance(): SystemInfoDialogFragment {
+ return SystemInfoDialogFragment()
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt
index aebe84b0f1..2f0f42093f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/Log.kt
@@ -1,7 +1,11 @@
+// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
+import org.yuzu.yuzu_emu.NativeLibrary
import android.os.Build
@@ -25,6 +29,15 @@ object Log {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
info("SoC Manufacturer - ${Build.SOC_MANUFACTURER}")
info("SoC Model - ${Build.SOC_MODEL}")
+ NativeLibrary.getCpuSummary().split('\n').forEach {
+ info("CPU Info - $it")
+ }
+ }
+ NativeLibrary.getVulkanDriverVersion().split('\n').forEach {
+ info("Vulkan Driver: - $it")
+ }
+ NativeLibrary.getVulkanApiVersion().split('\n').forEach {
+ info("Vulkan API Version: - $it")
}
info("Total System Memory - ${MemoryUtil.getDeviceRAM()}")
}
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 19260f60b9..ffef4f740c 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -8,13 +8,21 @@
#include "video_core/vulkan_common/vma.h"
#include <codecvt>
+#include <cstdio>
+#include <cstring>
#include <locale>
+#include <map>
+#include <set>
+#include <sstream>
#include <span>
#include <string>
#include <string_view>
+#include <thread>
#include <vector>
#include <dlfcn.h>
+#include <unistd.h>
+#include <iostream>
#ifdef ARCHITECTURE_arm64
#include <adrenotools/driver.h>
#endif
@@ -465,6 +473,205 @@ static Core::SystemResultStatus RunEmulation(const std::string& filepath,
return Core::SystemResultStatus::Success;
}
+namespace {
+
+struct CpuPartInfo {
+ u32 vendor;
+ u32 part;
+ const char* name;
+};
+
+constexpr CpuPartInfo s_cpu_list[] = {
+ // ARM - 0x41
+ {0x41, 0xd01, "Cortex-A32"},
+ {0x41, 0xd02, "Cortex-A34"},
+ {0x41, 0xd04, "Cortex-A35"},
+ {0x41, 0xd03, "Cortex-A53"},
+ {0x41, 0xd05, "Cortex-A55"},
+ {0x41, 0xd46, "Cortex-A510"},
+ {0x41, 0xd80, "Cortex-A520"},
+ {0x41, 0xd88, "Cortex-A520AE"},
+ {0x41, 0xd07, "Cortex-A57"},
+ {0x41, 0xd06, "Cortex-A65"},
+ {0x41, 0xd43, "Cortex-A65AE"},
+ {0x41, 0xd08, "Cortex-A72"},
+ {0x41, 0xd09, "Cortex-A73"},
+ {0x41, 0xd0a, "Cortex-A75"},
+ {0x41, 0xd0b, "Cortex-A76"},
+ {0x41, 0xd0e, "Cortex-A76AE"},
+ {0x41, 0xd0d, "Cortex-A77"},
+ {0x41, 0xd41, "Cortex-A78"},
+ {0x41, 0xd42, "Cortex-A78AE"},
+ {0x41, 0xd4b, "Cortex-A78C"},
+ {0x41, 0xd47, "Cortex-A710"},
+ {0x41, 0xd4d, "Cortex-A715"},
+ {0x41, 0xd81, "Cortex-A720"},
+ {0x41, 0xd89, "Cortex-A720AE"},
+ {0x41, 0xd87, "Cortex-A725"},
+ {0x41, 0xd44, "Cortex-X1"},
+ {0x41, 0xd4c, "Cortex-X1C"},
+ {0x41, 0xd48, "Cortex-X2"},
+ {0x41, 0xd4e, "Cortex-X3"},
+ {0x41, 0xd82, "Cortex-X4"},
+ {0x41, 0xd85, "Cortex-X925"},
+ {0x41, 0xd4a, "Neoverse E1"},
+ {0x41, 0xd0c, "Neoverse N1"},
+ {0x41, 0xd49, "Neoverse N2"},
+ {0x41, 0xd8e, "Neoverse N3"},
+ {0x41, 0xd40, "Neoverse V1"},
+ {0x41, 0xd4f, "Neoverse V2"},
+ {0x41, 0xd84, "Neoverse V3"},
+ {0x41, 0xd83, "Neoverse V3AE"},
+ // Qualcomm - 0x51
+ {0x51, 0x201, "Kryo"},
+ {0x51, 0x205, "Kryo"},
+ {0x51, 0x211, "Kryo"},
+ {0x51, 0x800, "Kryo 385 Gold"},
+ {0x51, 0x801, "Kryo 385 Silver"},
+ {0x51, 0x802, "Kryo 485 Gold"},
+ {0x51, 0x803, "Kryo 485 Silver"},
+ {0x51, 0x804, "Kryo 680 Prime"},
+ {0x51, 0x805, "Kryo 680 Gold"},
+ {0x51, 0x06f, "Krait"},
+ {0x51, 0xc00, "Falkor"},
+ {0x51, 0xc01, "Saphira"},
+ {0x51, 0x001, "Oryon"},
+};
+
+const char* find_cpu_name(u32 vendor, u32 part) {
+ for (const auto& cpu : s_cpu_list) {
+ if (cpu.vendor == vendor && cpu.part == part) {
+ return cpu.name;
+ }
+ }
+ return nullptr;
+}
+
+u64 read_midr_sysfs(u32 cpu_id) {
+ char path[128];
+ std::snprintf(path, sizeof(path), "/sys/devices/system/cpu/cpu%u/regs/identification/midr_el1", cpu_id);
+
+ FILE* f = std::fopen(path, "r");
+ if (!f) return 0;
+
+ char value[32];
+ if (!std::fgets(value, sizeof(value), f)) {
+ std::fclose(f);
+ return 0;
+ }
+ std::fclose(f);
+
+ return std::strtoull(value, nullptr, 16);
+}
+
+std::pair<u32, std::string> get_pretty_cpus() {
+ std::map<u64, int> core_layout;
+ u32 valid_cpus = 0;
+ for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) {
+ const auto midr = read_midr_sysfs(i);
+ if (midr == 0) break;
+
+ valid_cpus++;
+ core_layout[midr]++;
+ }
+
+ std::string cpus;
+
+ if (!core_layout.empty()) {
+ const CpuPartInfo* lowest_part = nullptr;
+ u32 lowest_part_id = 0xFFFFFFFF;
+
+ for (const auto& [midr, count] : core_layout) {
+ const auto vendor = (midr >> 24) & 0xff;
+ const auto part = (midr >> 4) & 0xfff;
+
+ if (!cpus.empty()) cpus += " + ";
+ cpus += fmt::format("{}x {}", count, find_cpu_name(vendor, part));
+ }
+ }
+
+ return {valid_cpus, cpus};
+}
+
+std::string get_arm_cpu_name() {
+ std::map<u64, int> core_layout;
+ for (u32 i = 0; i < std::thread::hardware_concurrency(); ++i) {
+ const auto midr = read_midr_sysfs(i);
+ if (midr == 0) break;
+
+ core_layout[midr]++;
+ }
+
+ if (!core_layout.empty()) {
+ const CpuPartInfo* lowest_part = nullptr;
+ u32 lowest_part_id = 0xFFFFFFFF;
+
+ for (const auto& [midr, count] : core_layout) {
+ const auto vendor = (midr >> 24) & 0xff;
+ const auto part = (midr >> 4) & 0xfff;
+
+ for (const auto& cpu : s_cpu_list) {
+ if (cpu.vendor == vendor && cpu.part == part) {
+ if (cpu.part < lowest_part_id) {
+ lowest_part_id = cpu.part;
+ lowest_part = &cpu;
+ }
+ break;
+ }
+ }
+ }
+
+ if (lowest_part) {
+ return lowest_part->name;
+ }
+ }
+
+ FILE* f = std::fopen("/proc/cpuinfo", "r");
+ if (!f) return "";
+
+ char buf[512];
+ std::string result;
+
+ auto trim = [](std::string& s) {
+ const auto start = s.find_first_not_of(" \t\r\n");
+ const auto end = s.find_last_not_of(" \t\r\n");
+ s = (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
+ };
+
+ while (std::fgets(buf, sizeof(buf), f)) {
+ std::string line(buf);
+ if (line.find("Hardware") == 0) {
+ auto pos = line.find(':');
+ if (pos != std::string::npos) {
+ result = line.substr(pos + 1);
+ trim(result);
+ break;
+ }
+ }
+ }
+ std::fclose(f);
+
+ if (!result.empty()) {
+ std::transform(result.begin(), result.end(), result.begin(), ::tolower);
+ }
+
+ return result;
+}
+
+const char* fallback_cpu_detection() {
+ static std::string s_result = []() -> std::string {
+ std::string result = get_arm_cpu_name();
+ if (result.empty()) {
+ return "Cortex-A34";
+ }
+ return result;
+ }();
+
+ return s_result.c_str();
+}
+
+} // namespace
+
extern "C" {
void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject instance,
@@ -694,6 +901,209 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuDriver(JNIEnv* env, jobject
env, EmulationSession::GetInstance().System().GPU().Renderer().GetDeviceVendor());
}
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject /*jobj*/) {
+ get_arm_cpu_name();
+ constexpr const char* CPUINFO_PATH = "/proc/cpuinfo";
+
+ auto trim = [](std::string& s) {
+ const auto start = s.find_first_not_of(" \t\r\n");
+ const auto end = s.find_last_not_of(" \t\r\n");
+ s = (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
+ };
+
+ auto to_lower = [](std::string s) {
+ for (auto& c : s) c = std::tolower(c);
+ return s;
+ };
+
+ try {
+ std::string result;
+ std::pair<u32, std::string> pretty_cpus = get_pretty_cpus();
+ u32 threads = pretty_cpus.first;
+ std::string cpus = pretty_cpus.second;
+
+ fmt::format_to(std::back_inserter(result), "CPUs: {}\n{} Threads",
+ cpus, threads);
+
+ FILE* f = std::fopen(CPUINFO_PATH, "r");
+ if (!f) return Common::Android::ToJString(env, result);
+
+ char buf[512];
+
+ if (f) {
+ std::set<std::string> feature_set;
+ while (std::fgets(buf, sizeof(buf), f)) {
+ std::string line(buf);
+ if (line.find("Features") == 0 || line.find("features") == 0) {
+ auto pos = line.find(':');
+ if (pos != std::string::npos) {
+ std::string feat_line = line.substr(pos + 1);
+ trim(feat_line);
+
+ std::istringstream iss(feat_line);
+ std::string feature;
+ while (iss >> feature) {
+ feature_set.insert(to_lower(feature));
+ }
+ }
+ }
+ }
+ std::fclose(f);
+
+ bool has_neon = feature_set.count("neon") || feature_set.count("asimd");
+ bool has_fp = feature_set.count("fp") || feature_set.count("vfp");
+ bool has_sve = feature_set.count("sve");
+ bool has_sve2 = feature_set.count("sve2");
+ bool has_crypto = feature_set.count("aes") || feature_set.count("sha1") ||
+ feature_set.count("sha2") || feature_set.count("pmull");
+ bool has_dotprod = feature_set.count("asimddp") || feature_set.count("dotprod");
+ bool has_i8mm = feature_set.count("i8mm");
+ bool has_bf16 = feature_set.count("bf16");
+ bool has_atomics = feature_set.count("atomics") || feature_set.count("lse");
+
+ std::string features;
+ if (has_neon || has_fp) {
+ features += "NEON";
+ if (has_dotprod) features += "+DP";
+ if (has_i8mm) features += "+I8MM";
+ if (has_bf16) features += "+BF16";
+ }
+
+ if (has_sve) {
+ if (!features.empty()) features += " | ";
+ features += "SVE";
+ if (has_sve2) features += "2";
+ }
+
+ if (has_crypto) {
+ if (!features.empty()) features += " | ";
+ features += "Crypto";
+ }
+
+ if (has_atomics) {
+ if (!features.empty()) features += " | ";
+ features += "LSE";
+ }
+
+ if (!features.empty()) {
+ result += "\nFeatures: " + features;
+ }
+ }
+
+ fmt::format_to(std::back_inserter(result), "\nLLVM CPU: {}", fallback_cpu_detection());
+
+ return Common::Android::ToJString(env, result);
+ } catch (...) {
+ return Common::Android::ToJString(env, "Unknown");
+ }
+}
+
+
+namespace {
+constexpr u32 VENDOR_QUALCOMM = 0x5143;
+constexpr u32 VENDOR_ARM = 0x13B5;
+
+VkPhysicalDeviceProperties GetVulkanDeviceProperties() {
+ Common::DynamicLibrary library;
+ if (!library.Open("libvulkan.so")) {
+ return {};
+ }
+
+ Vulkan::vk::InstanceDispatch dld;
+ // TODO: warn the user that Vulkan is unavailable rather than hard crash
+ const auto instance = Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_1);
+ const auto physical_devices = instance.EnumeratePhysicalDevices();
+ if (physical_devices.empty()) {
+ return {};
+ }
+
+ const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
+ return physical_device.GetProperties();
+}
+} // namespace
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanDriverVersion(JNIEnv* env, jobject jobj) {
+ try {
+ const auto props = GetVulkanDeviceProperties();
+ if (props.deviceID == 0) {
+ return Common::Android::ToJString(env, "N/A");
+ }
+
+ const u32 driver_version = props.driverVersion;
+ const u32 vendor_id = props.vendorID;
+
+ if (driver_version == 0) {
+ return Common::Android::ToJString(env, "N/A");
+ }
+
+ std::string version_str;
+
+ if (vendor_id == VENDOR_QUALCOMM) {
+ const u32 major = (driver_version >> 24) << 2;
+ const u32 minor = (driver_version >> 12) & 0xFFF;
+ const u32 patch = driver_version & 0xFFF;
+ version_str = fmt::format("{}.{}.{}", major, minor, patch);
+ }
+ else if (vendor_id == VENDOR_ARM) {
+ u32 major = VK_API_VERSION_MAJOR(driver_version);
+ u32 minor = VK_API_VERSION_MINOR(driver_version);
+ u32 patch = VK_API_VERSION_PATCH(driver_version);
+
+ // ARM custom encoding for newer drivers
+ if (major > 10) {
+ major = (driver_version >> 22) & 0x3FF;
+ minor = (driver_version >> 12) & 0x3FF;
+ patch = driver_version & 0xFFF;
+ }
+ version_str = fmt::format("{}.{}.{}", major, minor, patch);
+ }
+ // Standard Vulkan version encoding for other vendors
+ else {
+ const u32 major = VK_API_VERSION_MAJOR(driver_version);
+ const u32 minor = VK_API_VERSION_MINOR(driver_version);
+ const u32 patch = VK_API_VERSION_PATCH(driver_version);
+ version_str = fmt::format("{}.{}.{}", major, minor, patch);
+ }
+
+ return Common::Android::ToJString(env, version_str);
+ } catch (...) {
+ return Common::Android::ToJString(env, "N/A");
+ }
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanApiVersion(JNIEnv* env, jobject jobj) {
+ try {
+ const auto props = GetVulkanDeviceProperties();
+ if (props.deviceID == 0) {
+ return Common::Android::ToJString(env, "N/A");
+ }
+
+ const u32 api_version = props.apiVersion;
+ const u32 major = VK_API_VERSION_MAJOR(api_version);
+ const u32 minor = VK_API_VERSION_MINOR(api_version);
+ const u32 patch = VK_API_VERSION_PATCH(api_version);
+ const u32 variant = VK_API_VERSION_VARIANT(api_version);
+
+ // Include variant if non-zero (rare on Android)
+ const std::string version_str = variant > 0
+ ? fmt::format("{}.{}.{}.{}", variant, major, minor, patch)
+ : fmt::format("{}.{}.{}", major, minor, patch);
+
+ return Common::Android::ToJString(env, version_str);
+ } catch (...) {
+ return Common::Android::ToJString(env, "N/A");
+ }
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) {
+ const auto props = GetVulkanDeviceProperties();
+ if (props.deviceID == 0) {
+ return Common::Android::ToJString(env, "Unknown");
+ }
+
+ return Common::Android::ToJString(env, props.deviceName);
+}
+
void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
EmulationSession::GetInstance().System().ApplySettings();
EmulationSession::GetInstance().System().HIDCore().ReloadInputDevices();
diff --git a/src/android/app/src/main/res/layout/dialog_system_info.xml b/src/android/app/src/main/res/layout/dialog_system_info.xml
new file mode 100644
index 0000000000..ddf551e411
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_system_info.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:padding="16dp">
+
+ <com.google.android.material.card.MaterialCardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ app:cardElevation="0dp"
+ app:cardBackgroundColor="?attr/colorSurfaceVariant">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:maxHeight="400dp"
+ android:padding="12dp">
+
+ <TextView
+ android:id="@+id/text_system_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fontFamily="monospace"
+ android:textAppearance="?attr/textAppearanceBodySmall"
+ android:textColor="?attr/colorOnSurfaceVariant"
+ android:textIsSelectable="true" />
+
+ </ScrollView>
+
+ </com.google.android.material.card.MaterialCardView>
+
+</LinearLayout>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index fb80eaf053..6ea171ea2b 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -299,6 +299,25 @@
<string name="permission_denied_description">You denied this permission too many times and now you have to manually grant it in system settings.</string>
<string name="about">About</string>
<string name="about_description">Build version, credits, and more</string>
+ <string name="system_information">System Information</string>
+ <string name="system_information_description">View detailed device information</string>
+ <string name="device_manufacturer">Manufacturer</string>
+ <string name="device_model">Model</string>
+ <string name="product">Product</string>
+ <string name="android_version">Android Version</string>
+ <string name="android_security_patch">Security Patch</string>
+ <string name="build_id">Build ID</string>
+ <string name="general_information">General Information</string>
+ <string name="hardware">Hardware</string>
+ <string name="supported_abis">Supported ABIs</string>
+ <string name="cpu_info">CPU Information</string>
+ <string name="gpu_information">GPU Information</string>
+ <string name="vulkan_driver_version">Vulkan Driver Version</string>
+ <string name="error_getting_emulator_info">Error getting emulator info</string>
+ <string name="memory_info">Memory Information</string>
+ <string name="total_memory">Total Memory</string>
+ <string name="soc">SOC:</string>
+
<string name="warning_help">Help</string>
<string name="warning">Warning</string>
<string name="warning_skip">Skip</string>
@@ -656,7 +675,7 @@
<string name="installing">Installing…</string>
<string name="latest">Latest</string>
<string name="recommended_driver">Recommended Driver:</string>
- <string name="gpu_model">GPU Model:</string>
+ <string name="gpu_model">GPU Model</string>
<string name="unsupported_gpu">Unsupported GPU</string>
<string name="unsupported_gpu_warning">Your GPU does not support driver injection. Attempting to set custom drivers is not recommended.</string>