mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
feat: auto driver picks come from a Drivers-CI manifest with per-GPU fallbacks (#977)
This commit is contained in:
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"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user