fix: recognize SM8975 and mark Adreno 850 support experimental (#1249)

* fix: recognize SM8975 and mark Adreno 850 support experimental

* fix: retain A850 experimental status with spoofed Vulkan names
This commit is contained in:
Danbro007 authored and GitHub committed 2026-10-22 10:19:23 -04:00
1 parent 0577a22459
commit d2abaef2a3
15 files changed
+522 -41

No files matched your search

@@ -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 %" })
@@ -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<Int, String> {
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 ?: ""
@@ -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)
@@ -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;
}
}
@@ -1877,6 +1877,7 @@
<string name="gpuinfo_not_adreno">不是 Adreno GPU</string>
<string name="gpuinfo_supported">受支持</string>
<string name="gpuinfo_experimental">实验性:其驱动为测试构建</string>
<string name="gpuinfo_a850_experimental">Adreno 850 实验支持:需要配套的实验版显示驱动和游戏驱动;尚未在此设备上验证</string>
<string name="gpuinfo_below_tested">低于已测试的硬件(Adreno 725 及更新):可能无法运行</string>
<string name="gpuinfo_unsupported">不受支持:DroidDeck 需要 Adreno(骁龙)GPU</string>
@@ -1801,6 +1801,7 @@
<string name="gpuinfo_not_adreno">不是 Adreno GPU</string>
<string name="gpuinfo_supported">受支援</string>
<string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string>
<string name="gpuinfo_a850_experimental">Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證</string>
<string name="gpuinfo_below_tested">低於已測試的硬件(Adreno 725 或更新):可能無法執行</string>
<string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU</string>
@@ -1801,6 +1801,7 @@
<string name="gpuinfo_not_adreno">不是 Adreno GPU</string>
<string name="gpuinfo_supported">受支援</string>
<string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string>
<string name="gpuinfo_a850_experimental">Adreno 850 實驗性支援:需要相容的實驗版顯示驅動程式及遊戲驅動程式;尚未在此裝置上驗證</string>
<string name="gpuinfo_below_tested">低於已測試的硬體(Adreno 725 及更新機型):可能無法執行</string>
<string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU</string>
+1
View File
@@ -1886,6 +1886,7 @@
<string name="gpuinfo_not_adreno">Not an Adreno GPU</string>
<string name="gpuinfo_supported">Supported</string>
<string name="gpuinfo_experimental">Experimental: its drivers are test builds</string>
<string name="gpuinfo_a850_experimental">Experimental Adreno 850 support: requires compatible experimental display and game drivers; not verified on this device</string>
<string name="gpuinfo_below_tested">Below tested hardware (Adreno 725 and newer): it may not run</string>
<string name="gpuinfo_unsupported">Not supported: DroidDeck needs an Adreno (Snapdragon) GPU</string>
@@ -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)
@@ -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)
}
}
@@ -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", ""))
}
}
@@ -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))
}
}
+80
View File
@@ -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.
+133
View File
@@ -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('<H', data, 18)[0] != 183):
raise ValueError(f'{platform}: expected ELF64 little-endian AArch64 library')
if platform == 'linux' and b'libc.so.6\x00' not in data:
raise ValueError('linux: expected glibc library')
if platform == 'android' and b'libc.so.6\x00' in data:
raise ValueError('android: glibc library is not an AdrenoTools driver')
def read_driver(path, platform, expected_hash):
original = Path(path).read_bytes()
if hashlib.sha256(original).hexdigest() != expected_hash:
raise ValueError(f'{platform}: SHA256 mismatch; only the pinned issue #342 attachment is accepted')
allowed = {'meta.json', LIBRARY}
if platform == 'linux':
allowed.add('freedreno_icd.aarch64.json')
with zipfile.ZipFile(io.BytesIO(original)) as archive:
entries = archive.infolist()
names = [entry.filename for entry in entries]
if len(names) != len(set(names)) or set(names) != allowed:
raise ValueError(f'{platform}: expected only {sorted(allowed)}; unsafe, extra or missing entries')
for entry in entries:
mode = entry.external_attr >> 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()
+164
View File
@@ -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('<H', elf, 18, 183)
elf += b'libc.so.6\x00' if platform == 'linux' else b'libc.so\x00'
meta = ({'libc': 'glibc', 'kind': 'linux-vulkan-icd'} if platform == 'linux'
else {'libraryName': prepare.LIBRARY})
files = {'meta.json': json.dumps(meta).encode(), prepare.LIBRARY: bytes(elf)}
if platform == 'linux':
files['freedreno_icd.aarch64.json'] = json.dumps({'ICD': {'library_path': './' + prepare.LIBRARY}}).encode()
return files
def write(self, path, files):
with zipfile.ZipFile(path, 'w') as archive:
for name, data in files.items():
archive.writestr(name, data)
def pinned_fixture(self):
return mock.patch.multiple(prepare,
ANDROID_SHA256=hashlib.sha256(self.android.read_bytes()).hexdigest(),
LINUX_SHA256=hashlib.sha256(self.linux.read_bytes()).hexdigest())
def test_manifest_roundtrip_preserves_both_original_halves_and_is_deterministic(self):
with self.pinned_fixture():
prepare.prepare_bundle(self.android, self.linux, self.output)
first = self.output.read_bytes()
prepare.prepare_bundle(self.android, self.linux, self.output)
self.assertEqual(first, self.output.read_bytes())
with zipfile.ZipFile(self.output) as archive:
manifest = json.loads(archive.read('manifest.json'))
self.assertEqual('Adreno 850 community experimental', manifest['package'])
self.assertEqual('20261012-a850exp1', manifest['version'])
self.assertEqual(manifest['version'], manifest['tag'])
for platform, originals in (('android', self.android_files), ('linux', self.linux_files)):
directory = manifest['drivers'][platform]['path']
self.assertEqual(platform, directory)
for name, data in originals.items():
self.assertEqual(data, archive.read(directory + '/' + name))
self.assertIn(b'not native Adreno 850 support', archive.read('README.md'))
self.assertIn(b'issues/342', archive.read('NOTICE'))
def test_hash_mismatch_refuses_and_preserves_old_output(self):
self.output.write_bytes(b'previous bundle')
with self.assertRaisesRegex(ValueError, 'SHA256 mismatch'):
prepare.prepare_bundle(self.android, self.linux, self.output)
self.assertEqual(b'previous bundle', self.output.read_bytes())
def test_unsafe_extra_and_missing_entries_refused_before_output_write(self):
for entry in ('../escape', '/absolute', 'sub/../meta.json', 'sub\\meta.json', 'extra.so', None):
with self.subTest(entry=entry):
files = self.android_files.copy()
if entry is None:
files.pop(prepare.LIBRARY)
else:
files[entry] = b'unexpected'
self.write(self.android, files)
self.output.write_bytes(b'old')
with self.pinned_fixture(), self.assertRaisesRegex(ValueError, 'unsafe, extra or missing'):
prepare.prepare_bundle(self.android, self.linux, self.output)
self.assertEqual(b'old', self.output.read_bytes())
def test_bad_linux_half_does_not_replace_output(self):
self.linux_files['meta.json'] = b'{"libc":"bionic","kind":"linux-vulkan-icd"}'
self.write(self.linux, self.linux_files)
self.output.write_bytes(b'old')
with self.pinned_fixture(), self.assertRaisesRegex(ValueError, 'libc/kind'):
prepare.prepare_bundle(self.android, self.linux, self.output)
self.assertEqual(b'old', self.output.read_bytes())
def test_bad_android_library_name_refused(self):
self.android_files['meta.json'] = b'{"libraryName":"../evil.so"}'
self.write(self.android, self.android_files)
with self.pinned_fixture(), self.assertRaisesRegex(ValueError, 'libraryName'):
prepare.prepare_bundle(self.android, self.linux, self.output)
def test_wrong_elf_class_endian_architecture_and_libc_refused(self):
original = self.linux_files[prepare.LIBRARY]
for offset, replacement in ((4, 1), (5, 2), (18, 62)):
elf = bytearray(original)
elf[offset] = replacement
with self.subTest(offset=offset), self.assertRaisesRegex(ValueError, 'ELF64'):
prepare.validate_elf(elf, 'linux')
with self.assertRaisesRegex(ValueError, 'glibc'):
prepare.validate_elf(self.android_files[prepare.LIBRARY], 'linux')
with self.assertRaisesRegex(ValueError, 'AdrenoTools'):
prepare.validate_elf(original, 'android')
def test_output_cannot_overwrite_either_input_or_alias(self):
for source in (self.android, self.linux):
with self.subTest(source=source), self.assertRaisesRegex(ValueError, 'differ'):
prepare.prepare_bundle(self.android, self.linux, source)
alias = self.root / 'alias.zip'
alias.symlink_to(self.android)
with self.assertRaisesRegex(ValueError, 'differ'):
prepare.prepare_bundle(self.android, self.linux, alias)
def test_duplicate_and_symlink_entries_refused(self):
for duplicate in (True, False):
with self.subTest(duplicate=duplicate):
with zipfile.ZipFile(self.android, 'w') as archive:
for name, data in self.android_files.items():
info = zipfile.ZipInfo(name)
if not duplicate and name == prepare.LIBRARY:
info.create_system = 3
info.external_attr = (stat.S_IFLNK | 0o777) << 16
archive.writestr(info, data)
if duplicate:
import warnings
with warnings.catch_warnings():
warnings.simplefilter('ignore', UserWarning)
archive.writestr('meta.json', b'{}')
with self.pinned_fixture(), self.assertRaises(ValueError):
prepare.prepare_bundle(self.android, self.linux, self.output)
self.assertFalse(self.output.exists())
def test_malformed_icd_refused(self):
for icd in ({'ICD': []}, {'ICD': {'library_path': '../evil.so'}}):
with self.subTest(icd=icd):
self.linux_files['freedreno_icd.aarch64.json'] = json.dumps(icd).encode()
self.write(self.linux, self.linux_files)
with self.pinned_fixture(), self.assertRaisesRegex(ValueError, 'ICD library_path'):
prepare.prepare_bundle(self.android, self.linux, self.output)
def test_failed_replace_cleans_temporary_and_preserves_previous_output(self):
self.output.write_bytes(b'old')
with self.pinned_fixture(), mock.patch.object(prepare.os, 'replace', side_effect=OSError('fixture failure')):
with self.assertRaisesRegex(OSError, 'fixture failure'):
prepare.prepare_bundle(self.android, self.linux, self.output)
self.assertEqual(b'old', self.output.read_bytes())
self.assertEqual([], list(self.root.glob('.bundle.zip.*')))
if __name__ == '__main__':
unittest.main()