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 <the412banner@users.noreply.github.com>
This commit is contained in:
The412BannerandThe412Banner authored and GitHub committed 2026-10-05 20:32:51 -04:00
1 parent 3d16dc7ea4
commit 0bcb4a98fe
10 files changed
+552 -14

No files matched your search

+5
View File
@@ -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)
+112
View File
@@ -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 <dlfcn.h>
#include <jni.h>
#include <stdio.h>
#include <string.h>
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#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);
}
@@ -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<Pair<String, String>>)
fun collect(context: Context): List<Section> {
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<Section>): 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<Pair<String, String>>.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<Pair<String, String>> {
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<Pair<String, String>> {
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<Pair<String, String>> {
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<Pair<String, String>> {
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<Pair<String, String>> {
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<String, String>) = Section("Vulkan (the phone's own driver)", buildList<Pair<String, String>> {
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<Pair<String, String>> {
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")
})
}
@@ -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
@@ -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
}
}
}
@@ -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<String, String>? = 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<String, String> {
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<String, String>? = cached
}
@@ -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<DriverRow>, 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<List<DeviceInfo.Section>?>(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))
}
}
}
}
+6
View File
@@ -1044,4 +1044,10 @@
<string name="display_look_filter">%1$s Sets scaling to %2$s.</string>
<string name="display_match_screen">Match screen (%1$d×%2$d)</string>
<string name="mode_tab_steam">Steam</string>
<string name="device_info_show">Show device details</string>
<string name="device_info_hide">Hide device details</string>
<string name="device_info_loading">Reading the device…</string>
<string name="device_info_copy">Copy</string>
<string name="device_info_copy_hint">Copy these details into a bug report.</string>
<string name="device_info_copied">Copied.</string>
</resources>
@@ -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")))
}
}
@@ -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>): (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", ""))
}
}