mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
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:
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>
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user