feat: auto driver picks come from a Drivers-CI manifest with per-GPU fallbacks (#977)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-18 14:23:41 -04:00
1 parent 632f13d1b2
commit 1fbe103ffe
7 files changed
+162 -10

No files matched your search

@@ -12,6 +12,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableIntStateOf
import com.droiddeck.launcher.gpu.AutoDriverTrial
import com.droiddeck.launcher.gpu.AutoPicks
import com.droiddeck.launcher.gpu.DriverBundle
import com.droiddeck.launcher.gpu.DriverPairs
import com.droiddeck.launcher.gpu.GpuInfo
@@ -179,7 +180,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
val lm = LinuxVulkanDriverManager(activity)
val td = TurnipDriver(activity)
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
val recommended = if (QuestExperimental.isSupported()) null else DriverPairs.recommendedKey(gpu, pairs)
val recommended = if (QuestExperimental.isSupported()) null else recommendedKey(pairs)
pairRows = pairs.map { p ->
val bundle = p.bundle?.let { installedBundle(it) }
val displayId = if (p.bundle != null) bundle?.androidId else p.display?.let { TurnipReleases.installedId(activity, it, td::isInstalled) }
@@ -196,6 +197,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
}.sortedByDescending { it.recommended }
}
private fun recommendedKey(pairs: List<DriverPairs.DriverPair>) = DriverPairs.recommendedKey(
gpu, pairs, TurnipReleases.autoPicks(activity), AutoDriverTrial.bad(activity),
blob = AutoPicks.blobVersion(com.droiddeck.launcher.gpu.VulkanInfo.cachedOrNull()?.get("driver_version")),
)
fun chooseMode(next: String) {
pairModeOpen = next == GPU_MODE_PAIR
setMode(auto = next == GPU_MODE_AUTO, mix = next == GPU_MODE_MIX)
@@ -255,12 +261,13 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
Thread({
val problem = if (force || releasesStale()) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
val holdBack = !force && com.droiddeck.launcher.frontend.AutoComponents.constrained(activity)
runCatching { com.droiddeck.launcher.gpu.VulkanInfo.query() }
ui.post {
releaseChecking = false
refreshReleaseRows()
refreshPairs()
val pairs = DriverPairs.from(TurnipReleases.cached(activity))
val key = DriverPairs.recommendedKey(gpu, pairs)
val key = recommendedKey(pairs)
val pair = pairs.firstOrNull { it.key == key }
val row = pairRows.firstOrNull { it.key == key }
when {
@@ -202,7 +202,8 @@ object DeviceReport {
k("GPU", "${gpu.name} · ${gpu.family.label} · ${gpu.supportText}" + if (gpu.oneUi8Gen2) " · One UI 8 Gen 2" else "")
k("Driver mode", SessionPrefs.gpuDriverMode(context) +
(com.droiddeck.launcher.gpu.DriverPairs.recommendedKey(gpu, com.droiddeck.launcher.gpu.DriverPairs.from(
com.droiddeck.launcher.gpu.TurnipReleases.cached(context)))?.let { " (recommended pair: $it)" } ?: ""))
com.droiddeck.launcher.gpu.TurnipReleases.cached(context)), com.droiddeck.launcher.gpu.TurnipReleases.autoPicks(context),
com.droiddeck.launcher.gpu.AutoDriverTrial.bad(context))?.let { " (recommended pair: $it)" } ?: ""))
val turnip = TurnipDriver(context)
val androidChoice = SessionPrefs.androidDriver(context)
k("Display driver (chosen)", if (androidChoice.isEmpty()) "Auto -> ${turnip.autoId()}" else androidChoice)
@@ -38,6 +38,7 @@ object DownloadSource {
private val CATALOGS = listOf(
"https://raw.githubusercontent.com/Droid-Deck/DroidDeck-CI/",
"https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/",
"https://raw.githubusercontent.com/Droid-Deck/Drivers-CI/",
)
data class Mirror(val host: String, val api: Boolean)
@@ -0,0 +1,64 @@
package com.droiddeck.launcher.gpu
import com.droiddeck.launcher.gpu.GpuInfo.Family
import org.json.JSONArray
import org.json.JSONObject
object AutoPicks {
const val URL = "https://raw.githubusercontent.com/Droid-Deck/Drivers-CI/main/auto-drivers.json"
const val SCHEMA = 1
const val NEWEST_DD_TURNIP = DriverPairs.DD_TURNIP
class Rule(
val families: Set<Family>,
val models: Set<Int> = emptySet(),
val oneUi8Gen2: Boolean? = null,
val minSdk: Int = 0,
val blobMin: Int = 0,
val blobMax: Int = Int.MAX_VALUE,
val prefer: List<String>,
) {
fun matches(gpu: GpuInfo, sdk: Int, blob: Int?): Boolean =
gpu.family in families &&
(models.isEmpty() || gpu.model in models) &&
(oneUi8Gen2 == null || gpu.oneUi8Gen2 == oneUi8Gen2) &&
sdk >= minSdk &&
(blobMin == 0 && blobMax == Int.MAX_VALUE || blob != null && blob in blobMin..blobMax)
}
val BUILT_IN = listOf(
Rule(setOf(Family.A8XX), models = setOf(840), prefer = listOf(NEWEST_DD_TURNIP, DriverPairs.WN_BALANCED)),
Rule(setOf(Family.A8XX), prefer = listOf(DriverPairs.WN_BALANCED)),
Rule(setOf(Family.A7XX_LOW), prefer = listOf(DriverPairs.BANNER_710)),
Rule(setOf(Family.A7XX), oneUi8Gen2 = true, prefer = listOf(DriverPairs.BANNER_ONEUI)),
Rule(setOf(Family.A7XX, Family.A6XX), prefer = listOf(DriverPairs.BANNER)),
Rule(setOf(Family.ADRENO_UNKNOWN), minSdk = 36, prefer = listOf(DriverPairs.WN_BALANCED)),
Rule(setOf(Family.ADRENO_UNKNOWN), prefer = listOf(DriverPairs.BANNER)),
)
fun parse(text: String?): List<Rule>? = runCatching {
val json = JSONObject(text ?: return null)
if (json.optInt("schemaVersion") != SCHEMA) return null
val rules = json.getJSONArray("rules")
(0 until rules.length()).mapNotNull { i -> rule(rules.getJSONObject(i)) }.takeIf { it.isNotEmpty() }
}.getOrNull()
private fun rule(o: JSONObject): Rule? {
val families = strings(o.optJSONArray("families")).mapNotNull { name -> Family.values().firstOrNull { it.name == name } }.toSet()
val prefer = strings(o.optJSONArray("prefer"))
if (families.isEmpty() || prefer.isEmpty()) return null
val models = o.optJSONArray("models")?.let { a -> (0 until a.length()).map { a.getInt(it) }.toSet() } ?: emptySet()
return Rule(
families, models,
if (o.has("oneUi8Gen2")) o.getBoolean("oneUi8Gen2") else null,
o.optInt("minSdk", 0), o.optInt("blobMin", 0), o.optInt("blobMax", Int.MAX_VALUE), prefer,
)
}
private fun strings(a: JSONArray?): List<String> = a?.let { (0 until it.length()).map { i -> it.getString(i) } }.orEmpty()
fun preferences(gpu: GpuInfo, rules: List<Rule>, sdk: Int, blob: Int?): List<String> =
rules.firstOrNull { it.matches(gpu, sdk, blob) }?.prefer.orEmpty()
fun blobVersion(driverVersion: String?): Int? = driverVersion?.split('.')?.getOrNull(1)?.toIntOrNull()
}
@@ -61,13 +61,17 @@ object DriverPairs {
}
}
fun recommendedKey(gpu: GpuInfo, pairs: List<DriverPair>): String? = when (gpu.family) {
Family.NOT_ADRENO -> null
Family.A8XX -> if (gpu.model == 840) newestDdTurnip(pairs) ?: WN_BALANCED else WN_BALANCED
Family.A7XX_LOW -> BANNER_710
Family.A7XX -> if (gpu.oneUi8Gen2) BANNER_ONEUI else BANNER
Family.A6XX -> BANNER
Family.ADRENO_UNKNOWN -> if (android.os.Build.VERSION.SDK_INT >= 36) WN_BALANCED else BANNER
fun recommendedKey(
gpu: GpuInfo, pairs: List<DriverPair>,
rules: List<AutoPicks.Rule> = AutoPicks.BUILT_IN, bad: Set<String> = emptySet(),
sdk: Int = android.os.Build.VERSION.SDK_INT, blob: Int? = null,
): String? {
if (gpu.family == Family.NOT_ADRENO) return null
val keys = AutoPicks.preferences(gpu, rules, sdk, blob)
.ifEmpty { AutoPicks.preferences(gpu, AutoPicks.BUILT_IN, sdk, blob) }
.mapNotNull { if (it == AutoPicks.NEWEST_DD_TURNIP) newestDdTurnip(pairs) else it }
return keys.firstOrNull { key -> pairs.any { it.key == key && it.complete && AutoDriverTrial.release(it) !in bad } }
?: keys.firstOrNull()
}
fun newestDdTurnip(pairs: List<DriverPair>): String? = pairs
@@ -19,6 +19,7 @@ object TurnipReleases {
private const val PREFS = "turnip_releases"
private const val KEY_RELEASE = "latest"
private const val KEY_DOWNLOADS = "downloads"
private const val KEY_PICKS = "autoPicks"
class Asset(val source: String, val tag: String, val name: String, val url: String, val size: Long, val linux: Boolean, val label: String,
val sha256: String = "", val pair: String = "", val bundle: Boolean = false)
@@ -57,6 +58,9 @@ object TurnipReleases {
},
)
fun autoPicks(context: Context): List<AutoPicks.Rule> =
AutoPicks.parse(prefs(context).getString(KEY_PICKS, null)) ?: AutoPicks.BUILT_IN
fun cached(context: Context): Check? {
val raw = prefs(context).getString(KEY_RELEASE, null) ?: return null
return runCatching { parse(JSONObject(raw)) }.getOrNull()
@@ -100,6 +104,10 @@ object TurnipReleases {
}
}
if (failed.size == SOURCES.size) throw lastError ?: IOException("no source answered")
runCatching { get(context, AutoPicks.URL, AutoPicks.URL) }
.onFailure { Log.i(TAG, "auto picks: ${it.message}") }
.getOrNull()?.takeIf { AutoPicks.parse(it) != null }
?.let { prefs(context).edit().putString(KEY_PICKS, it).apply() }
val stored = JSONObject().put("assets", assets).put("latest", latest)
.put("failed", JSONArray(failed)).put("checkedAt", System.currentTimeMillis())
prefs(context).edit().putString(KEY_RELEASE, stored.toString()).apply()
@@ -0,0 +1,67 @@
package com.droiddeck.launcher.gpu
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class AutoPicksTest {
private fun gpu(model: Int) = GpuInfo("Adreno $model", model, GpuInfo.familyOf(true, model), "", false)
private fun bundle(tag: String) = TurnipReleases.Asset("DroidDeck", tag, "$tag.zip", "https://example/$tag", 6_000_000,
false, "Android + Linux", "0".repeat(64), DriverPairs.ddTurnip(tag), bundle = true)
private fun half(name: String, tag: String, linux: Boolean, pair: String) =
TurnipReleases.Asset("src", tag, name, "https://example/$name", 3_000_000, linux, "label", "0".repeat(64), pair)
private val pairs = DriverPairs.from(TurnipReleases.Check(
listOf(
bundle("DD-Turnip-v0.3.0"),
half("Turnip-r6.zip", "r6", false, DriverPairs.BANNER),
half("Turnip-r6-Linux.zip", "r6", true, DriverPairs.BANNER),
),
emptyList(), emptyList(), 0L,
))
private val manifest = """
{"schemaVersion": 1, "rules": [
{"families": ["A7XX"], "models": [740], "blobMin": 700, "prefer": ["banner"]},
{"families": ["A7XX", "A6XX"], "prefer": ["dd-turnip", "banner"]},
{"families": ["A9XX"], "prefer": ["banner"]}
]}
""".trimIndent()
@Test fun theManifestOrdersPreferencesAndTheNewestDdTurnipResolves() {
val rules = AutoPicks.parse(manifest)!!
assertEquals(2, rules.size)
assertEquals(DriverPairs.ddTurnip("DD-Turnip-v0.3.0"), DriverPairs.recommendedKey(gpu(650), pairs, rules))
}
@Test fun aReleaseMarkedBadFallsThroughToTheNextPreference() {
val rules = AutoPicks.parse(manifest)!!
val dd = pairs.first { it.key.startsWith(DriverPairs.DD_TURNIP) }
assertEquals(DriverPairs.BANNER, DriverPairs.recommendedKey(gpu(650), pairs, rules, bad = setOf(AutoDriverTrial.release(dd))))
}
@Test fun systemDriverRulesOnlyMatchAKnownBlob() {
val rules = AutoPicks.parse(manifest)!!
assertEquals(DriverPairs.BANNER, DriverPairs.recommendedKey(gpu(740), pairs, rules, blob = 744))
assertEquals(DriverPairs.ddTurnip("DD-Turnip-v0.3.0"), DriverPairs.recommendedKey(gpu(740), pairs, rules, blob = 676))
assertEquals(DriverPairs.ddTurnip("DD-Turnip-v0.3.0"), DriverPairs.recommendedKey(gpu(740), pairs, rules, blob = null))
assertEquals(744, AutoPicks.blobVersion("512.744.0"))
}
@Test fun aGpuTheManifestDoesNotCoverUsesTheBuiltInTable() {
assertEquals(DriverPairs.WN_BALANCED, DriverPairs.recommendedKey(gpu(830), pairs, AutoPicks.parse(manifest)!!))
}
@Test fun anotherSchemaOrNoUsableRulesIsIgnored() {
assertNull(AutoPicks.parse("""{"schemaVersion": 2, "rules": [{"families": ["A7XX"], "prefer": ["banner"]}]}"""))
assertNull(AutoPicks.parse("""{"schemaVersion": 1, "rules": [{"families": ["A9XX"], "prefer": ["banner"]}]}"""))
assertNull(AutoPicks.parse("not json"))
}
}