From 0bcb4a98feda234d89913e09c3d34c1ff54b02bc Mon Sep 17 00:00:00 2001 From: The412Banner <205237651+The412Banner@users.noreply.github.com> Date: Mon, 5 Oct 2026 20:32:51 -0400 Subject: [PATCH] Device details on the GPU card; Adreno model and SoC name when the kernel leaves them out (#295) * Device details on the GPU card, and the Adreno model when the kernel leaves it out The Components page called an AYANEO Pocket FIT (Adreno 750) "Adreno" and warned it was "Below tested hardware (Adreno 725 and newer)": its kernel names the GPU "Adreno33v2", GpuInfo wanted three digits, and the SoC model is blank. When KGSL's name has no model, take it from the phone's Vulkan driver (once this process has asked it) or from the platform code name (pineapple -> 750, kalama -> 740, sun -> 830, ...). The GPU card now opens with a tap or A onto the device's details, gathered once off the main thread (DeviceInfo): SoC (maker, model, platform, soc0 id), GPU (model and where it came from, the kernel's name, clock, load, temperature, support), CPU clusters, memory, system, the phone's own Vulkan driver - device, API, driver version and build, conformance, limits, the extensions DXVK and VKD3D lean on, Android's certified version and level - and the drivers DroidDeck uses. A button copies it all for a bug report; each section takes pad focus so the d-pad walks the list. Vulkan is asked through a new small native library (deviceinfo, vkinfo.c) that opens the system loader with dlopen and keeps nothing open. * Device details: the chip's model and name, CPU clusters, GPU clock range Ideas from ClusterTune (GPL-2.0, its approach, not its code), which tunes clocks on AYN, Retroid and other handhelds: - The SoC model is read where vendors put it - ro.soc.model, ro.vendor.qti.soc_model, Android's SOC_MODEL, ro.fota.platform (unless it is a branch name like "MSM_13.0") - since handhelds often leave Android's blank (Pocket FIT: SG8350P). - SocNames names it: model codes we are sure of (QCS8550 -> Snapdragon 8 Gen 2, SM8750 -> Snapdragon 8 Elite, ...), else the platform as a family (pineapple -> "Snapdragon 8 Gen 3 family"). The GPU card's subtitle shows it: "Snapdragon 8 Gen 3 family (SG8350P)". - CPU clusters come from the cpufreq policies (the cores each covers, min-max speed) rather than grouping cores by top speed. - GPU clock range from KGSL's step list, or a devfreq GPU node on non-Adreno chips (Mali), not only the top clock. * Device details: drop Qualcomm's own "Driver Build:" label from the driver build line --------- Co-authored-by: The412Banner --- app/src/main/cpp/CMakeLists.txt | 5 + app/src/main/cpp/deviceinfo/vkinfo.c | 112 ++++++++++++ .../com/droiddeck/launcher/gpu/DeviceInfo.kt | 166 ++++++++++++++++++ .../com/droiddeck/launcher/gpu/GpuInfo.kt | 42 ++++- .../com/droiddeck/launcher/gpu/SocNames.kt | 51 ++++++ .../com/droiddeck/launcher/gpu/VulkanInfo.kt | 36 ++++ .../droiddeck/launcher/ui/GpuDriversPanel.kt | 108 ++++++++++-- app/src/main/res/values/strings.xml | 6 + .../launcher/gpu/GpuModelFallbackTest.kt | 15 ++ .../droiddeck/launcher/gpu/SocNamesTest.kt | 25 +++ 10 files changed, 552 insertions(+), 14 deletions(-) create mode 100644 app/src/main/cpp/deviceinfo/vkinfo.c create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index ccc3c12c2..cc765a205 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -44,6 +44,11 @@ add_subdirectory(waylandcomp) add_library(sdnative SHARED sdnative/sdnative.c) target_link_libraries(sdnative log) +# The device info window's Vulkan section: the phone's own driver, asked once through the system +# loader (opened with dlopen, so a phone without Vulkan still loads the app). +add_library(deviceinfo SHARED deviceinfo/vkinfo.c) +target_link_libraries(deviceinfo dl) + # Rebuild Termux terminal-emulator's PTY JNI helper with the app's NDK instead of carrying its # prebuilt libtermux.so with a redundant libstdc++.so dependency. add_library(termux SHARED termux/termux.c) diff --git a/app/src/main/cpp/deviceinfo/vkinfo.c b/app/src/main/cpp/deviceinfo/vkinfo.c new file mode 100644 index 000000000..a8d769818 --- /dev/null +++ b/app/src/main/cpp/deviceinfo/vkinfo.c @@ -0,0 +1,112 @@ +// What the phone's own Vulkan driver says about its GPU, for the app's device info window: one +// instance on the system loader, the first physical device's properties, a few limits and the +// extensions DXVK and VKD3D-Proton lean on. Returned as "key=value" lines; nothing is kept open. +#include +#include +#include +#include +#define VK_NO_PROTOTYPES +#include + +#define LINE(...) do { if (used < sizeof(out)) used += snprintf(out + used, sizeof(out) - used, __VA_ARGS__); } while (0) + +static const char *const EXTENSIONS[] = { + "VK_EXT_robustness2", "VK_EXT_transform_feedback", "VK_EXT_custom_border_color", + "VK_KHR_dynamic_rendering", "VK_EXT_extended_dynamic_state3", "VK_EXT_descriptor_indexing", + "VK_KHR_timeline_semaphore", "VK_EXT_mesh_shader", "VK_KHR_ray_query", +}; + +static const char *type_name(VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return "integrated GPU"; + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return "discrete GPU"; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return "virtual GPU"; + case VK_PHYSICAL_DEVICE_TYPE_CPU: return "CPU"; + default: return "other"; + } +} + +JNIEXPORT jstring JNICALL +Java_com_droiddeck_launcher_gpu_VulkanInfo_nativeQuery(JNIEnv *env, jclass clazz) { + static char out[4096]; + size_t used = 0; + out[0] = 0; + void *lib = dlopen("libvulkan.so", RTLD_NOW | RTLD_LOCAL); + if (!lib) { LINE("error=no Vulkan loader\n"); return (*env)->NewStringUTF(env, out); } + PFN_vkGetInstanceProcAddr gipa = (PFN_vkGetInstanceProcAddr) dlsym(lib, "vkGetInstanceProcAddr"); + PFN_vkCreateInstance create = gipa ? (PFN_vkCreateInstance) gipa(NULL, "vkCreateInstance") : NULL; + PFN_vkEnumerateInstanceVersion instanceVersion = gipa ? (PFN_vkEnumerateInstanceVersion) gipa(NULL, "vkEnumerateInstanceVersion") : NULL; + if (!create) { LINE("error=no vkCreateInstance\n"); dlclose(lib); return (*env)->NewStringUTF(env, out); } + uint32_t loader = VK_API_VERSION_1_0; + if (instanceVersion) instanceVersion(&loader); + LINE("loader=%u.%u.%u\n", VK_API_VERSION_MAJOR(loader), VK_API_VERSION_MINOR(loader), VK_API_VERSION_PATCH(loader)); + + VkApplicationInfo app = { .sType = VK_STRUCTURE_TYPE_APPLICATION_INFO, .pApplicationName = "DroidDeck device info", + .apiVersion = loader >= VK_API_VERSION_1_1 ? VK_API_VERSION_1_1 : VK_API_VERSION_1_0 }; + VkInstanceCreateInfo info = { .sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, .pApplicationInfo = &app }; + VkInstance instance = VK_NULL_HANDLE; + if (create(&info, NULL, &instance) != VK_SUCCESS) { LINE("error=vkCreateInstance failed\n"); dlclose(lib); return (*env)->NewStringUTF(env, out); } + +#define GET(name) PFN_##name name = (PFN_##name) gipa(instance, #name) + GET(vkDestroyInstance); GET(vkEnumeratePhysicalDevices); GET(vkGetPhysicalDeviceProperties); + GET(vkGetPhysicalDeviceProperties2); GET(vkGetPhysicalDeviceMemoryProperties); + GET(vkEnumerateDeviceExtensionProperties); GET(vkGetPhysicalDeviceFeatures); + uint32_t count = 1; + VkPhysicalDevice device = VK_NULL_HANDLE; + VkResult listed = vkEnumeratePhysicalDevices ? vkEnumeratePhysicalDevices(instance, &count, &device) : VK_ERROR_INITIALIZATION_FAILED; + if ((listed != VK_SUCCESS && listed != VK_INCOMPLETE) || count == 0 || !device || !vkGetPhysicalDeviceProperties) { + LINE("error=no Vulkan device\n"); + } else { + VkPhysicalDeviceProperties p; + vkGetPhysicalDeviceProperties(device, &p); + LINE("device=%s\n", p.deviceName); + LINE("type=%s\n", type_name(p.deviceType)); + LINE("api=%u.%u.%u\n", VK_API_VERSION_MAJOR(p.apiVersion), VK_API_VERSION_MINOR(p.apiVersion), VK_API_VERSION_PATCH(p.apiVersion)); + // Qualcomm packs its driver version as 10.10.12 bits, the same layout as VK_MAKE_VERSION. + LINE("driver_version=%u.%u.%u\n", p.driverVersion >> 22, (p.driverVersion >> 12) & 0x3ff, p.driverVersion & 0xfff); + LINE("vendor_id=0x%04x\ndevice_id=0x%08x\n", p.vendorID, p.deviceID); + LINE("max_image_2d=%u\n", p.limits.maxImageDimension2D); + + // Driver name and build string (Vulkan 1.2 or VK_KHR_driver_properties). + uint32_t extCount = 0; + if (vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, NULL); + static VkExtensionProperties exts[512]; + if (extCount > 512) extCount = 512; + if (extCount && vkEnumerateDeviceExtensionProperties) vkEnumerateDeviceExtensionProperties(device, NULL, &extCount, exts); + int driverProps = p.apiVersion >= VK_API_VERSION_1_2; + for (uint32_t i = 0; i < extCount; i++) + if (!strcmp(exts[i].extensionName, "VK_KHR_driver_properties")) driverProps = 1; + if (driverProps && vkGetPhysicalDeviceProperties2) { + VkPhysicalDeviceDriverProperties driver = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES }; + VkPhysicalDeviceProperties2 p2 = { .sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, .pNext = &driver }; + vkGetPhysicalDeviceProperties2(device, &p2); + LINE("driver_name=%s\n", driver.driverName); + LINE("driver_info=%s\n", driver.driverInfo); + LINE("conformance=%u.%u.%u.%u\n", driver.conformanceVersion.major, driver.conformanceVersion.minor, + driver.conformanceVersion.subminor, driver.conformanceVersion.patch); + } + LINE("extension_count=%u\n", extCount); + for (size_t e = 0; e < sizeof(EXTENSIONS) / sizeof(*EXTENSIONS); e++) { + int has = 0; + for (uint32_t i = 0; i < extCount && !has; i++) has = !strcmp(exts[i].extensionName, EXTENSIONS[e]); + LINE("ext.%s=%s\n", EXTENSIONS[e], has ? "yes" : "no"); + } + if (vkGetPhysicalDeviceFeatures) { + VkPhysicalDeviceFeatures f; + vkGetPhysicalDeviceFeatures(device, &f); + LINE("geometry_shader=%s\ntessellation_shader=%s\ntexture_bc=%s\n", + f.geometryShader ? "yes" : "no", f.tessellationShader ? "yes" : "no", f.textureCompressionBC ? "yes" : "no"); + } + if (vkGetPhysicalDeviceMemoryProperties) { + VkPhysicalDeviceMemoryProperties m; + vkGetPhysicalDeviceMemoryProperties(device, &m); + VkDeviceSize largest = 0; + for (uint32_t i = 0; i < m.memoryHeapCount; i++) + if ((m.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) && m.memoryHeaps[i].size > largest) largest = m.memoryHeaps[i].size; + LINE("device_heap_mb=%llu\n", (unsigned long long) (largest >> 20)); + } + } + if (vkDestroyInstance) vkDestroyInstance(instance, NULL); + dlclose(lib); + return (*env)->NewStringUTF(env, out); +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt new file mode 100644 index 000000000..5e30ebb4a --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt @@ -0,0 +1,166 @@ +package com.droiddeck.launcher.gpu + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import com.droiddeck.launcher.core.FileUtils +import com.droiddeck.launcher.session.SessionPrefs +import java.io.File +import java.util.Locale + +/** + * Everything the device info window shows about the phone's chip: SoC, GPU, CPU, memory, system, + * the phone's own Vulkan driver and the drivers DroidDeck uses. Reads files and asks Vulkan once + * ([VulkanInfo]), so [collect] belongs off the main thread. Values the phone does not give are + * left out rather than guessed. + */ +object DeviceInfo { + class Section(val title: String, val rows: List>) + + fun collect(context: Context): List
{ + val vk = VulkanInfo.query() + // After Vulkan has been asked, detection can use its name too. + val gpu = GpuInfo.detect() + return listOf( + chip(), gpu(gpu), cpu(), memory(context), system(), vulkan(context, vk), drivers(context, gpu), + ).filter { it.rows.isNotEmpty() } + } + + /** The whole window as plain text, for a bug report. */ + fun text(sections: List
): String = buildString { + append("DroidDeck device info\n") + for (s in sections) { + append('\n').append(s.title).append('\n') + for ((k, v) in s.rows) append(" ").append(k).append(": ").append(v).append('\n') + } + } + + private fun MutableList>.add(key: String, value: String?) { + if (!value.isNullOrBlank() && value != Build.UNKNOWN) add(key to value.trim()) + } + + private fun read(path: String): String? = FileUtils.readString(File(path))?.trim()?.ifEmpty { null } + + private fun prop(name: String): String = GpuInfo.systemProperty(name) + + private fun chip() = Section("Chip (SoC)", buildList> { + val model = SocNames.model() + val platform = prop("ro.board.platform") + add("Name", SocNames.name(model, platform)) + add("Model", model) + if (Build.VERSION.SDK_INT >= 31) add("Maker", Build.SOC_MANUFACTURER) + add("Platform", platform) + add("Hardware", Build.HARDWARE) + add("Board", Build.BOARD) + val id = read("/sys/devices/soc0/soc_id") + val machine = read("/sys/devices/soc0/machine") + add("SoC id", listOfNotNull(id, machine).joinToString(" · ")) + add("Family", read("/sys/devices/soc0/family")) + }) + + private fun gpu(gpu: GpuInfo) = Section("GPU", buildList> { + add("GPU", gpu.name) + add("Family", gpu.family.label) + add("Model found from", when (gpu.modelSource) { + "kernel" -> "the kernel's GPU name" + "vulkan" -> "the Vulkan driver (the kernel's name has no model)" + "platform" -> "the platform (the kernel's name has no model)" + else -> "unknown" + }) + add("Kernel name", gpu.kgslName) + val kgsl = "/sys/class/kgsl/kgsl-3d0" + add("Clock range", gpuRange()) + add("Load", read("$kgsl/gpu_busy_percentage")?.let { if (it.endsWith("%")) it else "$it %" }) + add("Temperature", read("$kgsl/temp")?.toLongOrNull()?.let { if (it > 1000) "${it / 1000} °C" else "$it °C" }) + add("Support", gpu.supportText) + }) + + private fun cpu() = Section("CPU", buildList> { + add("Cores", Runtime.getRuntime().availableProcessors().toString()) + // Each cpufreq policy is one cluster: the cores it covers and their speed range. + val policies = File("/sys/devices/system/cpu/cpufreq").listFiles { f -> f.name.matches(Regex("policy\\d+")) } + ?.sortedBy { it.name.removePrefix("policy").toInt() }.orEmpty() + for (p in policies) { + val cpus = (read("${p.path}/related_cpus") ?: read("${p.path}/affected_cpus"))?.split(Regex("\\s+"))?.filter { it.isNotEmpty() }.orEmpty() + val min = read("${p.path}/cpuinfo_min_freq")?.toLongOrNull() + val max = read("${p.path}/cpuinfo_max_freq")?.toLongOrNull() ?: continue + val cores = if (cpus.size > 1) "cores ${cpus.first()}-${cpus.last()}" else "core ${cpus.firstOrNull() ?: p.name.removePrefix("policy")}" + add("Cluster ${p.name.removePrefix("policy")}", "${cpus.size.coerceAtLeast(1)} × $cores · " + + (if (min != null) ghz(min) + "–" else "") + ghz(max)) + } + add("ABI", Build.SUPPORTED_ABIS.joinToString(", ")) + }) + + private fun ghz(khz: Long) = String.format(Locale.US, "%.2f GHz", khz / 1e6) + + /** The GPU's slowest and fastest step: KGSL's list, else a devfreq GPU node (Mali and others). */ + private fun gpuRange(): String? { + fun mhz(hz: Long) = "${hz / 1_000_000} MHz" + val kgsl = read("/sys/class/kgsl/kgsl-3d0/gpu_available_frequencies") + ?.split(Regex("\\s+"))?.mapNotNull { it.toLongOrNull() }?.filter { it > 0 }.orEmpty() + if (kgsl.isNotEmpty()) return "${mhz(kgsl.min())}–${mhz(kgsl.max())} (${kgsl.size} steps)" + read("/sys/class/kgsl/kgsl-3d0/max_gpuclk")?.toLongOrNull()?.let { return "up to ${mhz(it)}" } + val node = File("/sys/class/devfreq").listFiles()?.firstOrNull { f -> + val n = f.name.lowercase() + (n.contains("gpu") || n.contains("mali") || n.contains("kgsl-3d")) && listOf("bus", "bw", "memlat").none { n.contains(it) } + } ?: return null + val steps = read("${node.path}/available_frequencies")?.split(Regex("\\s+"))?.mapNotNull { it.toLongOrNull() }?.filter { it > 0 }.orEmpty() + if (steps.isNotEmpty()) return "${mhz(steps.min())}–${mhz(steps.max())} (${steps.size} steps)" + val min = read("${node.path}/min_freq")?.toLongOrNull() + val max = read("${node.path}/max_freq")?.toLongOrNull() ?: return null + return (if (min != null) "${mhz(min)}–" else "up to ") + mhz(max) + } + + private fun memory(context: Context) = Section("Memory", buildList> { + val am = context.getSystemService(ActivityManager::class.java) ?: return@buildList + val mi = ActivityManager.MemoryInfo().also(am::getMemoryInfo) + add("Total", FileUtils.sizeToString(mi.totalMem)) + add("Available now", FileUtils.sizeToString(mi.availMem)) + }) + + private fun system() = Section("System", buildList> { + add("Device", "${Build.MANUFACTURER} ${Build.MODEL}") + add("Android", "${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})") + add("Security patch", Build.VERSION.SECURITY_PATCH) + add("Kernel", System.getProperty("os.version")) + add("Build", Build.DISPLAY) + }) + + private fun vulkan(context: Context, vk: Map) = Section("Vulkan (the phone's own driver)", buildList> { + vk["error"]?.let { add("Error", it) } + add("Device", vk["device"]) + add("Type", vk["type"]) + add("API version", vk["api"]) + add("Loader version", vk["loader"]) + add("Driver", listOfNotNull(vk["driver_name"], vk["driver_version"]).joinToString(" ")) + // Qualcomm's build string starts with its own label ("Driver Build: 43c70540de, ..."). + add("Driver build", vk["driver_info"]?.replace(Regex("(?i)^driver build:\\s*"), "")) + add("Conformance", vk["conformance"]) + add("Vendor / device id", listOfNotNull(vk["vendor_id"], vk["device_id"]).joinToString(" / ")) + add("Max texture size", vk["max_image_2d"]) + add("GPU memory heap", vk["device_heap_mb"]?.let { "$it MB" }) + add("Extensions", vk["extension_count"]) + val features = listOf("geometry_shader" to "Geometry shaders", "tessellation_shader" to "Tessellation", "texture_bc" to "BC textures") + for ((key, label) in features) add(label, vk[key]) + for ((key, value) in vk) if (key.startsWith("ext.")) add(key.removePrefix("ext."), value) + // What Android certifies the phone for. + val pm = context.packageManager + val hw = pm.systemAvailableFeatures.associate { it.name to it.version } + hw[PackageManager.FEATURE_VULKAN_HARDWARE_VERSION]?.let { + add("Android-certified version", "${it shr 22}.${(it shr 12) and 0x3ff}.${it and 0xfff}") + } + hw[PackageManager.FEATURE_VULKAN_HARDWARE_LEVEL]?.let { add("Android hardware level", it.toString()) } + }) + + private fun drivers(context: Context, gpu: GpuInfo) = Section("Drivers DroidDeck uses", buildList> { + add("Driver mode", SessionPrefs.gpuDriverMode(context)) + val turnip = TurnipDriver(context) + val choice = SessionPrefs.androidDriver(context).ifEmpty { turnip.autoId() } + add("Display driver", "${turnip.displayName(choice)} ${turnip.driverVersion(choice)}".trim()) + val linux = LinuxVulkanDriverManager(context) + val id = SessionPrefs.linuxDriver(context) + add("Game driver", if (id.isEmpty()) "the runtime's own Turnip" else "${linux.getDriverName(id)} ${linux.getDriverVersion(id)}".trim()) + if (gpu.oneUi8Gen2) add("Note", "One UI on an 8 Gen 2: needs the OneUI Turnip build") + }) +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt index 3054f0733..c4a05a526 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -16,10 +16,14 @@ data class GpuInfo( /** The three-digit Adreno model (740, 825), or 0 when KGSL does not say. */ val model: Int, val family: Family, - /** The SoC as the device reports it ("SM8550", "QCS8550"), or "" when it does not. */ + /** The SoC as a person would say it ("Snapdragon 8 Gen 2 (QCS8550)"), or "" when the phone does not say. */ val soc: String, /** Samsung's One UI on an 8 Gen 2: its Turnip needs the OneUI build, or frames tear and flicker. */ val oneUi8Gen2: Boolean, + /** KGSL's own name for the GPU, as the kernel spells it ("Adreno740v2", "Adreno33v2"). */ + val kgslName: String = "", + /** Where [model] came from: "kernel", "vulkan" or "platform", "" when unknown. */ + val modelSource: String = "", ) { enum class Family(val label: String) { A8XX("Adreno 8xx"), @@ -56,17 +60,49 @@ data class GpuInfo( val adreno = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() val raw = listOf("/sys/class/kgsl/kgsl-3d0/gpu_model", "/sys/class/kgsl/kgsl-3d0/gpu_chipid") .firstNotNullOfOrNull { FileUtils.readString(File(it))?.trim()?.takeIf(String::isNotEmpty) } - val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN }.orEmpty() else "" - val model = raw?.let { Regex("""(\d{3})""").find(it)?.groupValues?.get(1)?.toIntOrNull() } ?: 0 + // Where vendors put the chip's model, named when it is known: "Snapdragon 8 Gen 2 (QCS8550)". + val soc = if (adreno) SocNames.label() else "" + val fromKernel = raw?.let { threeDigits(it) } ?: 0 + // Some kernels name the GPU without its model (AYANEO's Pocket FIT: "Adreno33v2"). The + // Vulkan driver's own name, when this process has asked it, then the platform's code + // name stand in - the GPU is the same on every phone of a platform. + val fromVulkan = if (fromKernel > 0 || !adreno) 0 else VulkanInfo.cachedOrNull()?.get("device")?.let { threeDigits(it) } ?: 0 + val fromPlatform = if (fromKernel > 0 || fromVulkan > 0 || !adreno) 0 else platformModel() + val model = maxOf(fromKernel, fromVulkan, fromPlatform) + val source = when { + fromKernel > 0 -> "kernel" + fromVulkan > 0 -> "vulkan" + fromPlatform > 0 -> "platform" + else -> "" + } val family = familyOf(adreno, model) val samsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true) return GpuInfo( name = if (!adreno) Build.HARDWARE.ifBlank { "this GPU" } else if (model > 0) "Adreno $model" else "Adreno", model = model, family = family, soc = soc, oneUi8Gen2 = samsung && model == 740, + kgslName = raw.orEmpty(), modelSource = source, ) } + private fun threeDigits(text: String): Int = Regex("""(\d{3})""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 + + /** Qualcomm platform code names whose GPU is known for certain. */ + private val PLATFORMS = mapOf( + "msmnile" to 640, "kona" to 650, "lahaina" to 660, "taro" to 730, "cape" to 730, + "kalama" to 740, "pineapple" to 750, "sun" to 830, + ) + + internal fun platformModel(platform: String = systemProperty("ro.board.platform")): Int = + PLATFORMS[platform.lowercase()] ?: PLATFORMS[systemProperty("ro.vendor.qti.soc_name").lowercase()] ?: 0 + + /** android.os.SystemProperties.get, which apps may read but not call directly. */ + fun systemProperty(name: String): String = try { + Class.forName("android.os.SystemProperties").getMethod("get", String::class.java).invoke(null, name) as? String ?: "" + } catch (_: Exception) { + "" + } + internal fun familyOf(adreno: Boolean, model: Int): Family = when { !adreno -> Family.NOT_ADRENO model == 0 -> Family.ADRENO_UNKNOWN diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt new file mode 100644 index 000000000..76adb4bf2 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt @@ -0,0 +1,51 @@ +package com.droiddeck.launcher.gpu + +import android.os.Build + +/** + * Which chip this is, as a person would say it. Handhelds often leave Android's SoC model blank + * (the AYANEO Pocket FIT does), so the model is looked for where vendors put it, the same order + * ClusterTune uses; then it is named from a table of models we are sure of, or from the platform + * code name as a family ("Snapdragon 8 Gen 3 family") when the model itself is a vendor variant. + */ +object SocNames { + /** Model codes whose product name is certain. */ + private val MODELS = mapOf( + "SM8150" to "Snapdragon 855", "SM8250" to "Snapdragon 865", "SM8250-AC" to "Snapdragon 870", + "SM8350" to "Snapdragon 888", "SM8450" to "Snapdragon 8 Gen 1", "SM8475" to "Snapdragon 8+ Gen 1", + "SM8550" to "Snapdragon 8 Gen 2", "QCS8550" to "Snapdragon 8 Gen 2", "SM8650" to "Snapdragon 8 Gen 3", + "SM8750" to "Snapdragon 8 Elite", "SM8850" to "Snapdragon 8 Elite Gen 5", + "SM7325" to "Snapdragon 778G", "SM7450" to "Snapdragon 7 Gen 1", "SM7550" to "Snapdragon 7 Gen 3", + "SM6375" to "Snapdragon 695", + ) + + /** Platform code names: every chip of one platform shares its CPU and GPU design. */ + private val PLATFORMS = mapOf( + "msmnile" to "Snapdragon 855/860", "kona" to "Snapdragon 865/870", "lahaina" to "Snapdragon 888", + "taro" to "Snapdragon 8 Gen 1", "cape" to "Snapdragon 8+ Gen 1", "kalama" to "Snapdragon 8 Gen 2", + "pineapple" to "Snapdragon 8 Gen 3", "sun" to "Snapdragon 8 Elite", + ) + + /** The chip's model code ("QCS8550", "SG8350P"), or "" when the phone does not say. */ + fun model(prop: (String) -> String = GpuInfo::systemProperty): String { + val sdk = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it != Build.UNKNOWN }.orEmpty() else "" + return listOf(prop("ro.soc.model"), prop("ro.vendor.qti.soc_model"), sdk, prop("ro.fota.platform")) + .map { it.trim() } + // ro.fota.platform is often a branch name ("MSM_13.0"), not a chip. + .firstOrNull { it.isNotEmpty() && !Regex("(?i)^msm_\\d").containsMatchIn(it) }.orEmpty() + } + + /** "Snapdragon 8 Gen 2", "Snapdragon 8 Gen 3 family", or null when neither the model nor the platform is known. */ + fun name(model: String, platform: String = GpuInfo.systemProperty("ro.board.platform")): String? = + MODELS[model.uppercase()] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" } + + /** For the GPU card: "Snapdragon 8 Gen 2 (QCS8550)", the bare model, or "". */ + fun label(model: String = model(), platform: String = GpuInfo.systemProperty("ro.board.platform")): String { + val name = name(model, platform) + return when { + name != null && model.isNotEmpty() -> "$name ($model)" + name != null -> name + else -> model + } + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt new file mode 100644 index 000000000..ee4ca1a69 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/VulkanInfo.kt @@ -0,0 +1,36 @@ +package com.droiddeck.launcher.gpu + +import android.util.Log + +/** + * The phone's own Vulkan driver, as it describes itself (native/deviceinfo): device name, API and + * driver versions, a few limits and the extensions DXVK/VKD3D lean on. Asked once per process and + * kept; the first [query] creates a Vulkan instance, so it belongs off the main thread. + */ +object VulkanInfo { + @Volatile private var cached: Map? = null + + @JvmStatic private external fun nativeQuery(): String + + /** Blocking on first use. Empty-ish map with an "error" key when Vulkan cannot be asked. */ + @Synchronized + fun query(): Map { + cached?.let { return it } + val text = try { + System.loadLibrary("deviceinfo") + nativeQuery() + } catch (t: Throwable) { + Log.w("VulkanInfo", "query failed", t) + "error=" + (t.message ?: t.javaClass.simpleName) + } + val map = text.lineSequence().mapNotNull { line -> + val eq = line.indexOf('=') + if (eq <= 0) null else line.substring(0, eq) to line.substring(eq + 1) + }.toMap(LinkedHashMap()) + cached = map + return map + } + + /** What [query] found, if it has run in this process; never blocks. */ + fun cachedOrNull(): Map? = cached +} diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt index f3f9dd631..b4c7e23ff 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GpuDriversPanel.kt @@ -1,6 +1,17 @@ package com.droiddeck.launcher.ui import com.droiddeck.launcher.R +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.focusable +import androidx.compose.material.icons.outlined.ExpandLess +import androidx.compose.material.icons.outlined.ExpandMore +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import com.droiddeck.launcher.gpu.DeviceInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import androidx.compose.ui.res.stringResource import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -180,29 +191,104 @@ private fun DriverDropdown(rows: List, selected: String, fallback: St } } +/** + * This GPU and how far it is supported. A tap (or A) opens the card onto everything about the chip - + * SoC, GPU, CPU, memory, system, the phone's own Vulkan driver, the drivers in use - gathered once, + * off the main thread, with a button that copies it for a bug report. + */ @Composable private fun DeviceCard(s: GpuDriversState) { val colors = MaterialTheme.colorScheme val pal = LocalPalette.current + val context = LocalContext.current + val clipboard = LocalClipboardManager.current val tint = when { s.unsupported -> pal.error s.supported -> pal.good else -> AttentionAmber } - Row( - verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp), - modifier = Modifier.fillMaxWidth().clip(Shape14).background(colors.surface).border(1.dp, pal.line, Shape14).padding(14.dp), + var open by rememberSaveable { mutableStateOf(false) } + var details by remember { mutableStateOf?>(null) } + var copied by remember { mutableStateOf(false) } + LaunchedEffect(open) { + if (open && details == null) details = withContext(Dispatchers.IO) { DeviceInfo.collect(context) } + } + val src = remember { MutableInteractionSource() } + val hot = rememberHot(src) + val toggle = { open = !open; copied = false } + Column( + Modifier.fillMaxWidth().clip(Shape14).background(colors.surface) + .background(if (hot) pal.signal.copy(alpha = 0.10f) else Color.Transparent) + .glideBorder(hot, Shape14, pal.signal, pal.line), ) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.size(44.dp).clip(Shape12).background(tint.copy(alpha = 0.14f))) { - Icon(Icons.Outlined.Memory, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier.fillMaxWidth().paneItem("gpu-device") + .hoverable(src).clickable(interactionSource = src, indication = null, role = Role.Button, onClick = toggle) + .controllerConfirm(onClick = toggle) + .padding(14.dp), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.size(44.dp).clip(Shape12).background(tint.copy(alpha = 0.14f))) { + Icon(Icons.Outlined.Memory, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp)) + } + Column(Modifier.weight(1f)) { + Text(s.gpuName, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground) + Text(listOf(s.soc, s.gpuFamily).filter { it.isNotEmpty() }.joinToString(" · "), fontSize = 13.sp, color = colors.onSurfaceVariant) + } + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Box(Modifier.size(7.dp).clip(CircleShape).background(tint)) + Text(s.supportText, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = tint, maxLines = 2) + } + Icon( + if (open) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore, + contentDescription = stringResource(if (open) R.string.device_info_hide else R.string.device_info_show), + tint = colors.onSurfaceVariant, modifier = Modifier.size(24.dp), + ) } - Column(Modifier.weight(1f)) { - Text(s.gpuName, fontSize = 18.sp, fontWeight = FontWeight.Bold, color = colors.onBackground) - Text(listOf(s.soc, s.gpuFamily).filter { it.isNotEmpty() }.joinToString(" · "), fontSize = 13.sp, color = colors.onSurfaceVariant) + AnimatedVisibility(open) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth().padding(start = 14.dp, end = 14.dp, bottom = 14.dp)) { + HorizontalDivider(color = pal.line) + val sections = details + if (sections == null) { + Text(stringResource(R.string.device_info_loading), fontSize = 13.sp, color = colors.onSurfaceVariant) + LinearProgressIndicator(Modifier.fillMaxWidth()) + } else { + for (section in sections) DeviceInfoSection(section) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + stringResource(if (copied) R.string.device_info_copied else R.string.device_info_copy_hint), + fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.weight(1f), + ) + SecondaryButton(stringResource(R.string.device_info_copy), compact = true) { + clipboard.setText(AnnotatedString(DeviceInfo.text(sections))) + copied = true + } + } + } + } } - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) { - Box(Modifier.size(7.dp).clip(CircleShape).background(tint)) - Text(s.supportText, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = tint, maxLines = 2) + } +} + +/** One part of the device details: its title, then label/value rows. Focusable, so the d-pad walks the list. */ +@Composable +private fun DeviceInfoSection(section: DeviceInfo.Section) { + val colors = MaterialTheme.colorScheme + val pal = LocalPalette.current + val src = remember { MutableInteractionSource() } + val hot = rememberHot(src) + Column( + verticalArrangement = Arrangement.spacedBy(3.dp), + modifier = Modifier.fillMaxWidth().clip(Shape12) + .glideBorder(hot, Shape12, pal.signal) + .focusable(interactionSource = src).padding(horizontal = 8.dp, vertical = 6.dp), + ) { + Text(section.title.uppercase(), fontSize = 11.5.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.2.sp, color = colors.onSurfaceVariant) + for ((label, value) in section.rows) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text(label, fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.width(190.dp)) + Text(value, fontSize = 13.sp, fontWeight = FontWeight.Medium, color = colors.onBackground, modifier = Modifier.weight(1f)) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d3594e894..30372589a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1044,4 +1044,10 @@ %1$s Sets scaling to %2$s. Match screen (%1$d×%2$d) Steam + Show device details + Hide device details + Reading the device… + Copy + Copy these details into a bug report. + Copied. diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt new file mode 100644 index 000000000..10a0fd971 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt @@ -0,0 +1,15 @@ +package com.droiddeck.launcher.gpu + +import org.junit.Assert.assertEquals +import org.junit.Test + +class GpuModelFallbackTest { + @Test fun aPlatformCodeNameGivesItsAdreno() { + // AYANEO's Pocket FIT: KGSL says "Adreno33v2", the platform says pineapple. + assertEquals(750, GpuInfo.platformModel("pineapple")) + assertEquals(740, GpuInfo.platformModel("KALAMA")) + assertEquals(830, GpuInfo.platformModel("sun")) + assertEquals(0, GpuInfo.platformModel("somethingelse")) + assertEquals(GpuInfo.Family.A7XX, GpuInfo.familyOf(true, GpuInfo.platformModel("pineapple"))) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt new file mode 100644 index 000000000..d90e261d0 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt @@ -0,0 +1,25 @@ +package com.droiddeck.launcher.gpu + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class SocNamesTest { + private fun props(vararg pairs: Pair): (String) -> String = { pairs.toMap()[it].orEmpty() } + + @Test fun theModelIsFoundWhereVendorsPutIt() { + // AYANEO Pocket FIT: ro.soc.model blank, the vendor property set, ro.fota.platform a branch name. + assertEquals("SG8350P", SocNames.model(props("ro.vendor.qti.soc_model" to "SG8350P", "ro.fota.platform" to "MSM_13.0"))) + assertEquals("QCS8550", SocNames.model(props("ro.soc.model" to "QCS8550", "ro.vendor.qti.soc_model" to "other"))) + assertEquals("", SocNames.model(props("ro.fota.platform" to "MSM_13.0"))) + } + + @Test fun knownModelsAndPlatformsGetTheirNames() { + assertEquals("Snapdragon 8 Gen 2", SocNames.name("QCS8550", "")) + assertEquals("Snapdragon 8 Gen 3 family", SocNames.name("SG8350P", "pineapple")) + assertNull(SocNames.name("XYZ123", "nowhere")) + assertEquals("Snapdragon 8 Gen 2 (QCS8550)", SocNames.label("QCS8550", "")) + assertEquals("Snapdragon 8 Gen 3 family (SG8350P)", SocNames.label("SG8350P", "pineapple")) + assertEquals("XYZ123", SocNames.label("XYZ123", "")) + } +}