diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt index 48c58d01b..9a5fb1182 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DeviceInfo.kt @@ -57,10 +57,11 @@ object DeviceInfo { 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)" + "platform" -> "the platform or SoC (the kernel and Vulkan names have no model)" else -> "unknown" }) add("Kernel name", gpu.kgslName) + add("Kernel chip id", gpu.kgslChipId) 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 %" }) 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 adfa2589a..652d33fbe 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -14,6 +14,8 @@ data class GpuInfo( val oneUi8Gen2: Boolean, val kgslName: String = "", val modelSource: String = "", + val kgslChipId: String = "", + val platformGpuModel: Int = 0, ) { enum class Family(val label: String) { A8XX("Adreno 8xx"), @@ -28,9 +30,12 @@ data class GpuInfo( enum class Support { TESTED, UNTESTED, UNSUPPORTED } + private val isA850: Boolean get() = model == 850 || platformGpuModel == 850 + val support: Support get() = when { family == Family.NOT_ADRENO -> Support.UNSUPPORTED + isA850 -> Support.UNTESTED family == Family.A6XX && model == 650 -> Support.TESTED family == Family.A8XX -> Support.TESTED family == Family.A7XX && model >= 725 -> Support.TESTED @@ -40,14 +45,19 @@ data class GpuInfo( val supportText: String get() = when (support) { Support.TESTED -> "Supported" - Support.UNTESTED -> if (family == Family.A7XX_LOW) "Experimental: its drivers are test builds" + Support.UNTESTED -> if (isA850) "Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device" + else if (family == Family.A7XX_LOW) "Experimental: its drivers are test builds" else "Outside tested hardware (Adreno 650, 725 and newer): it may not run" Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU" } fun supportText(context: Context): String = context.getString(when (support) { Support.TESTED -> R.string.gpuinfo_supported - Support.UNTESTED -> if (family == Family.A7XX_LOW) R.string.gpuinfo_experimental else R.string.gpuinfo_below_tested + Support.UNTESTED -> when { + isA850 -> R.string.gpuinfo_a850_experimental + family == Family.A7XX_LOW -> R.string.gpuinfo_experimental + else -> R.string.gpuinfo_below_tested + } Support.UNSUPPORTED -> R.string.gpuinfo_unsupported }) @@ -58,38 +68,49 @@ data class GpuInfo( fun detect(): 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 raw = FileUtils.readString(File("/sys/class/kgsl/kgsl-3d0/gpu_model"))?.trim() + val chipId = FileUtils.readString(File("/sys/class/kgsl/kgsl-3d0/gpu_chipid"))?.trim().orEmpty() val soc = if (adreno) SocNames.label() else "" - val fromKernel = raw?.let { threeDigits(it) } ?: 0 - 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 platformGpuModel = if (adreno) platformModel() else 0 + val (model, source) = if (adreno) modelFromNames( + raw, + { VulkanInfo.cachedOrNull()?.get("device") }, + { platformGpuModel }, + ) else 0 to "" val family = familyOf(adreno, model) val samsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true) return GpuInfo( name = if (!adreno) Build.HARDWARE.ifBlank { UNNAMED } else if (model > 0) "Adreno $model" else "Adreno", model = model, family = family, soc = soc, oneUi8Gen2 = samsung && model == 740, - kgslName = raw.orEmpty(), modelSource = source, + kgslName = raw.orEmpty(), modelSource = source, kgslChipId = chipId, + platformGpuModel = platformGpuModel, ) } - internal fun threeDigits(text: String): Int = Regex("""(\d{3})""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 + internal fun threeDigits(text: String): Int = Regex("""(?i)\badreno\s*(?:\(TM\)\s*)?(\d{3})(?!\d)""").find(text)?.groupValues?.get(1)?.toIntOrNull() ?: 0 + + internal fun modelFromNames( + kernelName: String?, + vulkanName: () -> String?, + fallback: () -> Int, + ): Pair { + kernelName?.let { threeDigits(it) }?.takeIf { it > 0 }?.let { return it to "kernel" } + vulkanName()?.let { threeDigits(it) }?.takeIf { it > 0 }?.let { return it to "vulkan" } + return fallback().let { if (it > 0) it to "platform" else 0 to "" } + } 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 + internal fun platformModel( + platform: String = systemProperty("ro.board.platform"), + qtiSocName: String = systemProperty("ro.vendor.qti.soc_name"), + socModel: String = SocNames.model(), + ): Int = if (SocNames.normalizedModel(socModel) == "SM8975" || SocNames.normalizedModel(qtiSocName) == "SM8975") 850 + else PLATFORMS[platform.lowercase()] ?: PLATFORMS[qtiSocName.lowercase()] ?: 0 fun systemProperty(name: String): String = try { Class.forName("android.os.SystemProperties").getMethod("get", String::class.java).invoke(null, name) as? String ?: "" diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt index 39ad3f2ab..c1f2966b6 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/SocNames.kt @@ -8,6 +8,7 @@ object SocNames { "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", + "SM8975" to "Snapdragon 8 Elite Extreme Gen 6", "SM7325" to "Snapdragon 778G", "SM7450" to "Snapdragon 7 Gen 1", "SM7550" to "Snapdragon 7 Gen 3", "SM6375" to "Snapdragon 695", ) @@ -25,8 +26,10 @@ object SocNames { .firstOrNull { it.isNotEmpty() && !Regex("(?i)^msm_\\d").containsMatchIn(it) }.orEmpty() } + internal fun normalizedModel(model: String): String = model.trim().uppercase().removePrefix("QTI ").trim() + fun name(model: String, platform: String = GpuInfo.systemProperty("ro.board.platform")): String? = - MODELS[model.uppercase()] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" } + MODELS[normalizedModel(model)] ?: PLATFORMS[platform.lowercase()]?.let { "$it family" } fun label(model: String = model(), platform: String = GpuInfo.systemProperty("ro.board.platform")): String { val name = name(model, platform) diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index 6fb7360f3..9978409c5 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -284,27 +284,15 @@ public final class TurnipDriver { } String quest = QuestExperimental.INSTANCE.defaultAndroidDriver(); if (quest != null) return quest; - String model = gpuModel(); - Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model)); - if (model != null) { - java.util.regex.Matcher m = java.util.regex.Pattern.compile("(\\d)\\d\\d").matcher(model); - if (m.find()) { - char generation = m.group(1).charAt(0); - if (generation >= '8') return DRIVER_A8XX; - if (generation == '7') return DRIVER_A7XX; - if (generation == '6') return DRIVER_A6XX; - } - } - return android.os.Build.VERSION.SDK_INT >= 36 ? DRIVER_A8XX : DRIVER_A7XX; + int model = GpuInfo.Companion.detect().getModel(); + Log.i(TAG, "gpu model: " + (model == 0 ? "unknown" : model)); + return chooseByModel(model, android.os.Build.VERSION.SDK_INT); } - private static String gpuModel() { - for (String path : new String[]{ - "/sys/class/kgsl/kgsl-3d0/gpu_model", - "/sys/class/kgsl/kgsl-3d0/gpu_chipid"}) { - String value = FileUtils.readString(new File(path)); - if (value != null && !value.trim().isEmpty()) return value.trim(); - } - return null; + static String chooseByModel(int model, int sdk) { + if (model >= 800) return DRIVER_A8XX; + if (model >= 700) return DRIVER_A7XX; + if (model >= 600) return DRIVER_A6XX; + return sdk >= 36 ? DRIVER_A8XX : DRIVER_A7XX; } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 960fdd825..92966d30f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1877,6 +1877,7 @@ 不是 Adreno GPU 受支持 实验性:其驱动为测试构建 + Adreno 850 实验支持:需要配套的实验版显示驱动和游戏驱动;尚未在此设备上验证 低于已测试的硬件(Adreno 725 及更新):可能无法运行 不受支持:DroidDeck 需要 Adreno(骁龙)GPU diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 00c99dd55..2b7b254d1 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1801,6 +1801,7 @@ 不是 Adreno GPU 受支援 實驗性:其驅動程式為測試版本 + Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證 低於已測試的硬件(Adreno 725 或更新):可能無法執行 不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 941437a04..47a76574f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1801,6 +1801,7 @@ 不是 Adreno GPU 受支援 實驗性:其驅動程式為測試版本 + Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證 低於已測試的硬體(Adreno 725 及更新機型):可能無法執行 不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9bce6977..0ea43eba0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1886,6 +1886,7 @@ Not an Adreno GPU Supported Experimental: its drivers are test builds + Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device Below tested hardware (Adreno 725 and newer): it may not run Not supported: DroidDeck needs an Adreno (Snapdragon) GPU diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt index 6d64307e3..bee21343d 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuDetectionTest.kt @@ -9,7 +9,7 @@ class GpuDetectionTest { private fun info(adreno: Boolean, model: Int) = GpuInfo(name = "x", model = model, family = GpuInfo.familyOf(adreno, model), soc = "", oneUi8Gen2 = false) - @Test fun modelIsTheFirstThreeDigitsOfWhateverTheKernelOrVulkanReports() { + @Test fun modelIsParsedOnlyFromAnAdrenoName() { assertEquals(650, GpuInfo.threeDigits("Adreno650")) assertEquals(830, GpuInfo.threeDigits("Adreno (TM) 830")) assertEquals(740, GpuInfo.threeDigits("Adreno740v2")) @@ -19,6 +19,12 @@ class GpuDetectionTest { assertEquals(0, GpuInfo.threeDigits(" ")) assertEquals(0, GpuInfo.threeDigits("Adreno")) assertEquals(0, GpuInfo.threeDigits("Adreno 65")) + assertEquals(850, GpuInfo.threeDigits("Adreno850v2")) + assertEquals(850, GpuInfo.threeDigits("Adreno (TM) 850")) + assertEquals(0, GpuInfo.threeDigits("44051431")) + assertEquals(0, GpuInfo.threeDigits("0x2a02be7")) + assertEquals(0, GpuInfo.threeDigits("Adreno 8500")) + assertEquals(0, GpuInfo.threeDigits("Mali-G850")) } @Test fun nonAdrenoRenderersAreNeverAdrenoWhateverNumberTheyCarry() { @@ -53,12 +59,14 @@ class GpuDetectionTest { assertEquals(Support.TESTED, info(true, 740).support) assertEquals(Support.TESTED, info(true, 750).support) assertEquals(Support.TESTED, info(true, 830).support) + assertEquals(Support.UNTESTED, info(true, 850).support) assertEquals(Support.UNSUPPORTED, info(false, 0).support) assertEquals(Support.UNSUPPORTED, info(false, 830).support) } @Test fun userFacingTextPerTier() { assertEquals("Supported", info(true, 830).supportText) + assertEquals("Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device", info(true, 850).supportText) assertEquals("Experimental: its drivers are test builds", info(true, 720).supportText) assertEquals("Outside tested hardware (Adreno 650, 725 and newer): it may not run", info(true, 618).supportText) assertEquals("Not supported: DroidDeck needs an Adreno (Snapdragon) GPU", info(false, 0).supportText) diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt index 0ce3bdc6c..24a38bd75 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/GpuModelFallbackTest.kt @@ -11,4 +11,61 @@ class GpuModelFallbackTest { assertEquals(0, GpuInfo.platformModel("somethingelse")) assertEquals(GpuInfo.Family.A7XX, GpuInfo.familyOf(true, GpuInfo.platformModel("pineapple"))) } + @Test fun sm8975IsTheOnlyNewSocFallback() { + assertEquals(850, GpuInfo.platformModel("unknown", "", "SM8975")) + assertEquals(850, GpuInfo.platformModel("unknown", "", "QTI SM8975")) + assertEquals(850, GpuInfo.platformModel("unknown", "QTI SM8975", "")) + assertEquals(850, GpuInfo.platformModel("unknown", "", " qti sm8975 ")) + assertEquals(0, GpuInfo.platformModel("unknown", "", "SM89750")) + assertEquals(0, GpuInfo.platformModel("unknown", "", "SM8975-PRO")) + assertEquals(0, GpuInfo.platformModel("unknown", "", "Qualcomm SM8975")) + assertEquals(0, GpuInfo.platformModel("unknown", "", "SM8850")) + assertEquals(850, GpuInfo.platformModel("sun", "", "SM8975")) + assertEquals(830, GpuInfo.platformModel("sun", "", "unknown")) + } + + @Test fun aKernelModelDoesNotNeedVulkanOrSocFallback() { + assertEquals(850 to "kernel", GpuInfo.modelFromNames("Adreno850v2", + { error("Vulkan must not be queried when the kernel identifies the GPU") }, + { error("SoC must not override the kernel model") })) + } + + @Test fun aMissingKernelModelUsesVulkanBeforeSocFallback() { + assertEquals(830 to "vulkan", GpuInfo.modelFromNames("Adreno", { "Adreno (TM) 830" }, + { error("SoC must not override the Vulkan model") })) + assertEquals(850 to "vulkan", GpuInfo.modelFromNames(null, { "Adreno850v2" }, { 0 })) + } + + @Test fun numericChipIdsCannotHideTheSocFallback() { + for (name in listOf(null, "", "Adreno", "44051431", "0x2a02be7")) { + assertEquals(850 to "platform", GpuInfo.modelFromNames(name, { "Adreno" }, + { GpuInfo.platformModel("unknown", "", "QTI SM8975") })) + } + assertEquals(0 to "", GpuInfo.modelFromNames(null, { null }, { 0 })) + } + + @Test fun sm8975StaysExperimentalWhenTheDriverReportsA840() { + val fallback = GpuInfo.platformModel("sun", "", "SM8975") + for (kernel in listOf("Adreno", "Adreno (TM) 840")) { + val (model, source) = GpuInfo.modelFromNames(kernel, { "Adreno (TM) 840" }, { fallback }) + val gpu = GpuInfo("Adreno $model", model, GpuInfo.familyOf(true, model), "SM8975", false, + modelSource = source, platformGpuModel = fallback) + assertEquals(840, gpu.model) + assertEquals(if (kernel == "Adreno") "vulkan" else "kernel", gpu.modelSource) + assertEquals(GpuInfo.Support.UNTESTED, gpu.support) + assertEquals("Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device", gpu.supportText) + } + } + + @Test fun anA840WithoutSm8975KeepsItsExistingSupportStatus() { + for (fallback in listOf(0, 830, 840)) { + val gpu = GpuInfo("Adreno 840", 840, GpuInfo.Family.A8XX, "", false, + platformGpuModel = fallback) + assertEquals(GpuInfo.Support.TESTED, gpu.support) + assertEquals("Supported", gpu.supportText) + } + val otherGpu = GpuInfo("Mali", 0, GpuInfo.Family.NOT_ADRENO, "", false, + platformGpuModel = 850) + assertEquals(GpuInfo.Support.UNSUPPORTED, otherGpu.support) + } } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt index 8919b79a7..963f9f739 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/SocNamesTest.kt @@ -21,4 +21,13 @@ class SocNamesTest { assertEquals("Snapdragon 8 Gen 3 family (SG8350P)", SocNames.label("SG8350P", "pineapple")) assertEquals("XYZ123", SocNames.label("XYZ123", "")) } + @Test fun qtiPrefixIsNormalizedWithoutGuessingOtherModelVariants() { + assertEquals("Snapdragon 8 Elite Extreme Gen 6", SocNames.name("SM8975", "")) + assertEquals("Snapdragon 8 Elite Extreme Gen 6", SocNames.name("QTI SM8975", "")) + assertEquals("Snapdragon 8 Elite Extreme Gen 6", SocNames.name(" qti sm8975 ", "")) + assertEquals("Snapdragon 8 Elite Extreme Gen 6 (QTI SM8975)", SocNames.label("QTI SM8975", "")) + assertNull(SocNames.name("SM89750", "")) + assertNull(SocNames.name("SM8975-PRO", "")) + assertNull(SocNames.name("Qualcomm SM8975", "")) + } } diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/TurnipDriverAutoTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/TurnipDriverAutoTest.kt index b15e19630..95713589e 100644 --- a/app/src/test/java/com/droiddeck/launcher/gpu/TurnipDriverAutoTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/gpu/TurnipDriverAutoTest.kt @@ -30,4 +30,17 @@ class TurnipDriverAutoTest { @Test fun otherDevicesRetainTheGenericFallback() { assertEquals("turnip25.1.0", TurnipDriver(RuntimeEnvironment.getApplication()).autoId()) } + + @Test fun normalizedGpuModelsSelectExistingGenerationDrivers() { + for (sdk in listOf(28, 36)) { + assertEquals("turnip26.3.0-a6xx", TurnipDriver.chooseByModel(650, sdk)) + assertEquals("turnip25.1.0", TurnipDriver.chooseByModel(730, sdk)) + assertEquals("turnip-sdk36", TurnipDriver.chooseByModel(850, sdk)) + } + } + + @Test fun unknownGpuModelsRetainTheSdkFallback() { + assertEquals("turnip25.1.0", TurnipDriver.chooseByModel(0, 28)) + assertEquals("turnip-sdk36", TurnipDriver.chooseByModel(0, 36)) + } } diff --git a/docs/adreno-850-experimental.md b/docs/adreno-850-experimental.md new file mode 100644 index 000000000..28c1dd3f6 --- /dev/null +++ b/docs/adreno-850-experimental.md @@ -0,0 +1,80 @@ +# Experimental Adreno 850 detection and driver imports + +Reports [#342](https://github.com/Droid-Deck/DroidDeck/issues/342) and +[#723](https://github.com/Droid-Deck/DroidDeck/issues/723) describe Turnip failing +on Adreno 850, including an SM8975 / Adreno850v2 device reporting +`VK_ERROR_INCOMPATIBLE_DRIVER`. These reports motivate the detection changes; +they are not proof that the application's existing Turnip builds support A850. + +## Detection + +The application parses a GPU model only from an Adreno device name. The raw +KGSL `gpu_chipid` is recorded separately in device diagnostics: a decimal or +hexadecimal chip ID is not an Adreno model number. + +Detection prefers the KGSL name, then an already cached Vulkan device name, +then a platform fallback. An explicit `SM8975` or `QTI SM8975` SoC identifies +Adreno 850 when neither device name is available, including when the board +property still uses an older platform name. Driver auto selection uses the +same detection, with existing external-driver and Quest overrides retained. +This does not start a new Vulkan query during driver selection. + +A850 is labelled experimental. The SM8975 platform identity retains that +classification even when the selected driver reports Adreno 840; the displayed +model still follows the KGSL/Vulkan name. Identifying the hardware does not add native +A850 support to Turnip or prove that Steam and games will run. + +## Optional community driver bundle + +The community attachments uploaded to #342 on 2026-10-12 contain experimental +Android display and Linux game drivers. Their metadata describes a binary +patch that treats A850 as A840 and reuses some Adreno X2 entries. These are +not native A850 drivers; they may fail to start, hang, or render incorrectly, +and have not been validated across A850 revisions. + +Download the original attachments separately: + +| Platform | Original attachment | SHA-256 | +| --- | --- | --- | +| Android | [AdrenoTools-A850-experimental.zip](https://github.com/user-attachments/files/33319268/AdrenoTools-A850-experimental.zip) | `0cf849d068c875f6c469380ca1f74da1f757575bce53d4ef9c1f6d38677cbe8b` | +| Linux | [A850-experimental.zip](https://github.com/user-attachments/files/33319267/A850-experimental.zip) | `40fc59506620001531b9bdd1c9152e104c6fb1f4c07773c4743b9d70deb87eec` | + +Prepare a bundle accepted by DroidDeck's existing DriverBundle importer: + +```sh +mkdir -p build/a850-experimental +python3 tools/prepare_a850_driver_bundle.py \ + --android /absolute/path/AdrenoTools-A850-experimental.zip \ + --linux /absolute/path/A850-experimental.zip \ + --output build/a850-experimental/A850-community-experimental.zip +``` + +The tool accepts only the pinned originals, validates their ZIP entries, +metadata and ELF platform, and preserves the original files byte for byte. +It does not download, execute or rebuild drivers. Neither binary is included +in the source tree or installed automatically by these changes. + +To try the bundle, stop the session and wait for any automatic driver check +or download to finish. Note the existing driver selections, then use +**Components → GPU drivers → Mix and match → Import .zip**. This is a GPU +driver bundle, not a Decky plugin ZIP. Confirm that both the Android display +and Linux game drivers are selected, then fully exit and reopen the app. +Keep **Session → Performance → Session repair → Turnip: sysmem rendering** +enabled as advised by the original Linux driver metadata. + +Test entering Steam separately from game rendering. If it fails, restore the +previous drivers and restart the app. Preserve the **Share logs** ZIP, +including `device.txt`, `session.log`, the exact chip ID and Android version. + +## Validation + +```sh +python3 -m unittest discover -s tools/tests -p test_a850_driver_bundle.py -v +./gradlew -PskipRust=true :app:testDebugUnitTest --tests 'com.droiddeck.launcher.gpu.*' +``` + +The tests cover detection priority, chip-ID rejection, SoC normalization, +experimental status, driver-generation selection and bundle integrity. They +do not replace Vulkan, Steam or game testing on the target phone. A user +confirmed that importing the experimental bundle succeeded; target-device +Steam and game compatibility remain unverified. diff --git a/tools/prepare_a850_driver_bundle.py b/tools/prepare_a850_driver_bundle.py new file mode 100644 index 000000000..a24b848bd --- /dev/null +++ b/tools/prepare_a850_driver_bundle.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Package pinned local community A850 drivers; never download or execute drivers.""" + +import argparse +import hashlib +import io +import json +import os +from pathlib import Path +import stat +import struct +import tempfile +import zipfile + +ANDROID_SHA256 = '0cf849d068c875f6c469380ca1f74da1f757575bce53d4ef9c1f6d38677cbe8b' +LINUX_SHA256 = '40fc59506620001531b9bdd1c9152e104c6fb1f4c07773c4743b9d70deb87eec' +LIBRARY = 'libvulkan_freedreno.so' +VERSION = '20261012-a850exp1' +SOURCE = 'https://github.com/Droid-Deck/DroidDeck/issues/342' +README = f'''# Adreno 850 community experimental + +Manual opt-in experiment for DroidDeck; not native Adreno 850 support. +Source: community attachments to {SOURCE} +Android original: AdrenoTools-A850-experimental.zip, SHA256 {ANDROID_SHA256} +Linux original: A850-experimental.zip, SHA256 {LINUX_SHA256} + +These community binaries are patched to treat Adreno 850 as Adreno 840. +They have not been validated on the target phone by this project. They may +black-screen, hang, glitch, or fail to boot. They do not establish native 850 +support. Import and select this bundle manually; global auto selection is +unchanged. Use sysmem mode as advised in the original Linux metadata. + +Both original meta.json files, libraries, and Linux ICD JSON are preserved +byte-for-byte. No drivers are downloaded, executed, or rebuilt by this tool. +''' +NOTICE = f'''Community experimental binaries, distributed without a compatibility guarantee. +Original author attribution and release descriptions are in android/meta.json +and linux/meta.json. The412Banner is the original driver author; the community +attachment describes an experimental binary patch. Source: {SOURCE} +This wrapper makes no new license grant for the enclosed third-party binaries. +''' + + +def validate_elf(data, platform): + if (len(data) < 64 or data[:6] != b'\x7fELF\x02\x01' + or struct.unpack_from('> 16 + if entry.is_dir() or stat.S_IFMT(mode) not in (0, stat.S_IFREG) or entry.flag_bits & 1: + raise ValueError(f'{platform}: entry must be an unencrypted regular file') + files = {name: archive.read(name) for name in names} + meta = json.loads(files['meta.json']) + if not isinstance(meta, dict): + raise ValueError(f'{platform}: meta.json must be an object') + if platform == 'android' and meta.get('libraryName') != LIBRARY: + raise ValueError('android: incorrect meta.json libraryName') + if platform == 'linux': + if meta.get('libc') != 'glibc' or meta.get('kind') != 'linux-vulkan-icd': + raise ValueError('linux: incorrect meta.json libc/kind') + icd = json.loads(files['freedreno_icd.aarch64.json']) + if (not isinstance(icd, dict) or not isinstance(icd.get('ICD'), dict) + or icd['ICD'].get('library_path') != './' + LIBRARY): + raise ValueError('linux: incorrect ICD library_path') + validate_elf(files[LIBRARY], platform) + return files + + +def prepare_bundle(android, linux, output): + android, linux, output = map(Path, (android, linux, output)) + for source in (android, linux): + if source.resolve() == output.resolve() or (output.exists() and source.samefile(output)): + raise ValueError('output must differ from both input archives') + files = {} + for platform, source, digest in (('android', android, ANDROID_SHA256), ('linux', linux, LINUX_SHA256)): + files.update({f'{platform}/{name}': data for name, data in read_driver(source, platform, digest).items()}) + manifest = {'package': 'Adreno 850 community experimental', 'version': VERSION, 'tag': VERSION, + 'drivers': {'android': {'path': 'android'}, 'linux': {'path': 'linux'}}} + files['manifest.json'] = (json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode() + files['README.md'] = README.encode() + files['NOTICE'] = NOTICE.encode() + temporary = None + try: + with tempfile.NamedTemporaryFile(dir=output.parent, prefix='.' + output.name + '.', delete=False) as target: + temporary = Path(target.name) + with zipfile.ZipFile(target, 'w', compression=zipfile.ZIP_STORED) as archive: + for name, data in sorted(files.items()): + info = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0)) + info.create_system = 3 + info.external_attr = (stat.S_IFREG | 0o644) << 16 + archive.writestr(info, data) + target.flush() + os.fsync(target.fileno()) + os.replace(temporary, output) + finally: + if temporary is not None: + temporary.unlink(missing_ok=True) + return output + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--android', type=Path, required=True, help='Original AdrenoTools-A850-experimental.zip') + parser.add_argument('--linux', type=Path, required=True, help='Original A850-experimental.zip') + parser.add_argument('--output', type=Path, required=True, help='Destination DroidDeck DriverBundle zip') + args = parser.parse_args() + try: + result = prepare_bundle(args.android, args.linux, args.output) + except (OSError, ValueError, zipfile.BadZipFile, RuntimeError) as error: + parser.exit(1, f'error: {error}\n') + print(f'Created {result} (manual opt-in; experimental, no target-phone validation)') + + +if __name__ == '__main__': + main() diff --git a/tools/tests/test_a850_driver_bundle.py b/tools/tests/test_a850_driver_bundle.py new file mode 100644 index 000000000..d207a63d2 --- /dev/null +++ b/tools/tests/test_a850_driver_bundle.py @@ -0,0 +1,164 @@ +import hashlib +import importlib.util +import json +from pathlib import Path +import struct +import stat +import tempfile +import unittest +from unittest import mock +import zipfile + +spec = importlib.util.spec_from_file_location( + 'prepare_a850_driver_bundle', Path(__file__).resolve().parents[1] / 'prepare_a850_driver_bundle.py') +prepare = importlib.util.module_from_spec(spec) +spec.loader.exec_module(prepare) + + +class A850BundleTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.android = self.root / 'android.zip' + self.linux = self.root / 'linux.zip' + self.output = self.root / 'bundle.zip' + self.android_files = self.files('android') + self.linux_files = self.files('linux') + self.write(self.android, self.android_files) + self.write(self.linux, self.linux_files) + + def files(self, platform): + elf = bytearray(64) + elf[:6] = b'\x7fELF\x02\x01' + struct.pack_into('