feat: auto drivers update on app open and roll back if a new pair never starts a session (#972)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-16 16:17:44 -04:00
1 parent 4b2537a36b
commit 602a6fd734
8 files changed
+228 -23

No files matched your search

@@ -11,6 +11,7 @@ import androidx.compose.runtime.getValue
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.DriverBundle
import com.droiddeck.launcher.gpu.DriverPairs
import com.droiddeck.launcher.gpu.GpuInfo
@@ -47,7 +48,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
var pairPercent by mutableIntStateOf(-1)
var autoStatus by mutableStateOf("")
var activeBundle by mutableStateOf<String?>(null)
private var autoCheckedThisProcess = false
private var lastAutoCheck = 0L
private var pairModeOpen by mutableStateOf(false)
var restartPending by mutableStateOf(false)
private set
@@ -211,11 +212,32 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
if (auto) ensureAuto(force = false)
}
fun checkOnOpen() {
if (SessionPrefs.gpuDriverMode(activity) == SessionPrefs.GPU_DRIVERS_AUTO) ensureAuto(force = false)
else if (!releaseChecking && releasesStale() && com.droiddeck.launcher.frontend.AutoComponents.isOnline(activity)) checkLatestTurnip()
}
private fun releasesStale(): Boolean {
val cached = TurnipReleases.cached(activity)
return cached == null || System.currentTimeMillis() - cached.checkedAt > RELEASE_CHECK_INTERVAL_MS ||
cached.assets.none { it.pair.isNotEmpty() }
}
fun ensureAuto(force: Boolean) {
mode = SessionPrefs.gpuDriverMode(activity)
if (mode != SessionPrefs.GPU_DRIVERS_AUTO || pairBusy != null || releaseChecking) return
if (!force && autoCheckedThisProcess) return
autoCheckedThisProcess = true
if (SessionState.running) {
if (force) autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
return
}
AutoDriverTrial.rollbackDue(activity)?.let { trial ->
AutoDriverTrial.rollBack(activity, trial)
autoStatus = activity.getString(R.string.drivers_rolled_back, trial.name)
refreshDrivers()
return
}
if (!force && System.currentTimeMillis() - lastAutoCheck < RELEASE_CHECK_INTERVAL_MS) return
lastAutoCheck = System.currentTimeMillis()
val displayChanged = SessionPrefs.androidDriver(activity) != QuestExperimental.defaultAndroidDriver()
if (QuestExperimental.applyAutoDrivers(activity)) {
autoStatus = activity.getString(
@@ -231,10 +253,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
releaseChecking = true
autoStatus = activity.getString(R.string.drivers_checking_latest)
Thread({
val cached = TurnipReleases.cached(activity)
val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L ||
cached.assets.none { it.pair.isNotEmpty() }
val problem = if (force || stale) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
val problem = if (force || releasesStale()) runCatching { TurnipReleases.refresh(activity) }.exceptionOrNull()?.message else null
val holdBack = !force && com.droiddeck.launcher.frontend.AutoComponents.constrained(activity)
ui.post {
releaseChecking = false
refreshReleaseRows()
@@ -248,6 +268,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
?: activity.getString(R.string.upd_current_status)
pair == null || !pair.complete ->
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
AutoDriverTrial.release(pair) in AutoDriverTrial.bad(activity) ->
autoStatus = activity.getString(R.string.drivers_skipping_bad, pair.name, pair.version)
SessionState.running -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
holdBack && row?.installed != true -> autoStatus = activity.getString(R.string.drivers_auto_waits_for_network, pair.name, pair.version)
else -> installPair(pair, auto = true)
}
}
@@ -268,8 +292,6 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
pairPercent = 0
if (auto) autoStatus = activity.getString(R.string.drivers_downloading_pair, pair.name, pair.version)
Thread({
val lm = LinuxVulkanDriverManager(activity)
val td = TurnipDriver(activity)
var downloaded = false
val result = runCatching {
val zips = pair.assets
@@ -283,22 +305,27 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
pairBusy = null
pairPercent = -1
result.onSuccess { (displayId, linuxId) ->
val displayChanged = SessionPrefs.androidDriver(activity) != displayId
if (auto && SessionState.running) {
lastAutoCheck = 0L
autoStatus = activity.getString(R.string.drivers_auto_waits_for_session)
return@onSuccess
}
val previousDisplay = SessionPrefs.androidDriver(activity)
val previousLinux = SessionPrefs.linuxDriver(activity)
val displayChanged = previousDisplay != displayId
SessionPrefs.setAndroidDriver(activity, displayId)
SessionPrefs.setLinuxDriver(activity, linuxId)
val restart = displayChanged && CompositorHost.isStarted
if (auto) {
val previous = SessionPrefs.gpuAutoInstalled(activity)
for (id in previous - setOf(displayId, linuxId)) {
val bundle = DriverBundle.containing(activity, id, lm.isInstalled(id))
when {
bundle != null -> { DriverBundle.remove(activity, bundle); TurnipReleases.forget(activity, bundle.id) }
lm.isInstalled(id) -> lm.removeDriver(id)
else -> td.remove(id)
}
TurnipReleases.forget(activity, id)
}
SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId))
val trial = AutoDriverTrial.current(activity)
val lastGood = trial?.let { setOf(it.previousDisplay, it.previousLinux) } ?: setOf(previousDisplay, previousLinux)
val changed = previousDisplay != displayId || previousLinux != linuxId
SessionPrefs.setGpuAutoInstalled(activity, SessionPrefs.gpuAutoInstalled(activity) + setOf(displayId, linuxId))
if (changed) AutoDriverTrial.begin(
activity, AutoDriverTrial.release(pair), "${pair.name} ${pair.version}", displayId, linuxId,
trial?.previousDisplay ?: previousDisplay, trial?.previousLinux ?: previousLinux,
)
if (!SessionState.running) AutoDriverTrial.prune(activity, lastGood + setOf(displayId, linuxId))
autoStatus = activity.getString(
if (downloaded) { if (restart) R.string.drivers_updated_to_restart else R.string.drivers_updated_to }
else { if (restart) R.string.drivers_switched_to_restart else R.string.drivers_switched_to },
@@ -506,6 +533,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
private companion object {
const val TAG = "MainActivity"
const val RELEASE_CHECK_INTERVAL_MS = 3_600_000L
}
}
@@ -1051,7 +1051,7 @@ class MainActivity : ComponentActivity() {
refreshPhantomStatus()
syncLossless()
updates.onResume()
drivers.ensureAuto(force = false)
drivers.checkOnOpen()
protons.checkUpdates()
if (!SessionState.running) Thread({
val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList())
@@ -332,7 +332,7 @@ object AutoComponents {
caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
private fun constrained(context: Context): Boolean {
fun constrained(context: Context): Boolean {
val power = context.getSystemService(PowerManager::class.java)
if (power?.isPowerSaveMode == true) return true
if (Build.VERSION.SDK_INT >= 29 && (power?.currentThermalStatus ?: 0) >= PowerManager.THERMAL_STATUS_MODERATE) return true
@@ -0,0 +1,101 @@
package com.droiddeck.launcher.gpu
import android.content.Context
import android.util.Log
import com.droiddeck.launcher.session.SessionPrefs
object AutoDriverTrial {
private const val TAG = "AutoDriverTrial"
private const val PREFS = "gpu_auto_trial"
const val UNREADY_STARTS_BEFORE_ROLLBACK = 2
class Trial(
val release: String, val name: String,
val display: String, val linux: String,
val previousDisplay: String, val previousLinux: String,
val starts: Int,
)
@Volatile private var app: Context? = null
@Volatile private var watching: Trial? = null
fun release(pair: DriverPairs.DriverPair) = "${pair.key}@${pair.version}"
fun current(context: Context): Trial? {
val p = prefs(context)
val release = p.getString("release", null) ?: return null
return Trial(
release, p.getString("name", "").orEmpty(),
p.getString("display", "").orEmpty(), p.getString("linux", "").orEmpty(),
p.getString("previousDisplay", "").orEmpty(), p.getString("previousLinux", "").orEmpty(),
p.getInt("starts", 0),
)
}
fun begin(context: Context, release: String, name: String, display: String, linux: String, previousDisplay: String, previousLinux: String) {
prefs(context).edit()
.putString("release", release).putString("name", name)
.putString("display", display).putString("linux", linux)
.putString("previousDisplay", previousDisplay).putString("previousLinux", previousLinux)
.putInt("starts", 0)
.commit()
}
fun clear(context: Context) {
val bad = bad(context)
prefs(context).edit().clear().putStringSet("bad", bad).commit()
}
fun bad(context: Context): Set<String> = prefs(context).getStringSet("bad", emptySet()).orEmpty()
fun sessionStarting(context: Context, display: String, linux: String) {
val trial = current(context)
watching = null
if (trial == null || trial.display != display || trial.linux != linux) return
app = context.applicationContext
watching = trial
prefs(context).edit().putInt("starts", trial.starts + 1).commit()
}
fun sessionReady() {
val trial = watching ?: return
val context = app ?: return
watching = null
if (current(context)?.release != trial.release) return
Log.i(TAG, "${trial.release} reached a ready session; keeping it")
clear(context)
prune(context, setOf(trial.display, trial.linux))
}
fun rollbackDue(context: Context): Trial? = current(context)?.takeIf { it.starts >= UNREADY_STARTS_BEFORE_ROLLBACK }
fun rollBack(context: Context, trial: Trial) {
Log.w(TAG, "${trial.release} never reached a ready session in ${trial.starts} starts; restoring the previous drivers")
if (SessionPrefs.androidDriver(context) == trial.display) SessionPrefs.setAndroidDriver(context, trial.previousDisplay)
if (SessionPrefs.linuxDriver(context) == trial.linux) SessionPrefs.setLinuxDriver(context, trial.previousLinux)
val bad = bad(context) + trial.release
prefs(context).edit().clear().putStringSet("bad", bad).commit()
prune(context, setOf(trial.previousDisplay, trial.previousLinux, SessionPrefs.androidDriver(context), SessionPrefs.linuxDriver(context)))
}
fun prune(context: Context, keep: Set<String>) {
val lm = LinuxVulkanDriverManager(context)
val td = TurnipDriver(context)
val owned = SessionPrefs.gpuAutoInstalled(context)
for (id in owned - keep) {
val bundle = DriverBundle.containing(context, id, lm.isInstalled(id))
when {
bundle != null -> {
if (bundle.androidId in keep || bundle.linuxId in keep) continue
DriverBundle.remove(context, bundle); TurnipReleases.forget(context, bundle.id)
}
lm.isInstalled(id) -> lm.removeDriver(id)
else -> td.remove(id)
}
TurnipReleases.forget(context, id)
}
SessionPrefs.setGpuAutoInstalled(context, owned.intersect(keep))
}
private fun prefs(context: Context) = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
}
@@ -103,6 +103,7 @@ object SessionEvents {
if (!SessionState.running || SessionState.guestPid <= 1 || !SessionState.firstFrameSeen || SessionState.suspended) return
if (SessionState.phase != SessionPhase.READY) {
transition(SessionPhase.READY, "session.ready")
Thread({ com.droiddeck.launcher.gpu.AutoDriverTrial.sessionReady() }, "gpu-driver-trial").start()
}
}
@@ -539,6 +539,12 @@ class SessionService : Service() {
LinuxRuntime.vulkanIcd(this)?.let { guest.add("VK_ICD_FILENAMES=" + it.path) }
val linuxDriverId = SessionPrefs.linuxDriver(this)
SessionState.runtimeDriver = linuxDriverId
com.droiddeck.launcher.gpu.AutoDriverTrial.sessionStarting(
this,
com.droiddeck.launcher.wayland.CompositorHost.displayChoice.takeIf { com.droiddeck.launcher.wayland.CompositorHost.isStarted }
?: SessionPrefs.androidDriver(this),
linuxDriverId,
)
LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId)
?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) }
tuDebug(linuxDriverId)?.let { guest.add("TU_DEBUG=$it") }
+4
View File
@@ -1285,6 +1285,10 @@
<string name="drivers_downloading_pair">Downloading %1$s %2$s…</string>
<string name="drivers_updated_to">Updated to %1$s %2$s.</string>
<string name="drivers_updated_to_restart">Updated to %1$s %2$s. The display driver applies after DroidDeck restarts.</string>
<string name="drivers_auto_waits_for_session">Driver updates wait until the session ends.</string>
<string name="drivers_auto_waits_for_network">%1$s %2$s will download once you\'re off mobile data and battery saver.</string>
<string name="drivers_skipping_bad">%1$s %2$s didn\'t start a session here, so Auto is staying on the drivers that did.</string>
<string name="drivers_rolled_back">%1$s didn\'t start a session twice, so Auto went back to the drivers that worked.</string>
<string name="drivers_switched_to">Switched to %1$s %2$s.</string>
<string name="drivers_switched_to_restart">Switched to %1$s %2$s. The display driver applies after DroidDeck restarts.</string>
<string name="drivers_using">Using %1$s %2$s.</string>
@@ -0,0 +1,65 @@
package com.droiddeck.launcher.gpu
import android.content.Context
import com.droiddeck.launcher.session.SessionPrefs
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class AutoDriverTrialTest {
private val context: Context get() = RuntimeEnvironment.getApplication()
@Before fun reset() {
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
context.getSharedPreferences("gpu_auto_trial", Context.MODE_PRIVATE).edit().clear().commit()
SessionPrefs.setAndroidDriver(context, "new-display")
SessionPrefs.setLinuxDriver(context, "new-linux")
SessionPrefs.setGpuAutoInstalled(context, setOf("old-display", "old-linux", "new-display", "new-linux"))
AutoDriverTrial.begin(context, "banner@r5", "Banners r5", "new-display", "new-linux", "old-display", "old-linux")
}
@Test fun aReadySessionKeepsTheNewDriversAndDropsTheOldOnes() {
AutoDriverTrial.sessionStarting(context, "new-display", "new-linux")
AutoDriverTrial.sessionReady()
assertNull(AutoDriverTrial.current(context))
assertEquals(setOf("new-display", "new-linux"), SessionPrefs.gpuAutoInstalled(context))
assertEquals("new-linux", SessionPrefs.linuxDriver(context))
}
@Test fun startsThatNeverGetReadyRollBackAndMarkTheReleaseBad() {
AutoDriverTrial.sessionStarting(context, "new-display", "new-linux")
assertNull(AutoDriverTrial.rollbackDue(context))
AutoDriverTrial.sessionStarting(context, "new-display", "new-linux")
val trial = AutoDriverTrial.rollbackDue(context)!!
AutoDriverTrial.rollBack(context, trial)
assertEquals("old-display", SessionPrefs.androidDriver(context))
assertEquals("old-linux", SessionPrefs.linuxDriver(context))
assertEquals(setOf("old-display", "old-linux"), SessionPrefs.gpuAutoInstalled(context))
assertTrue("banner@r5" in AutoDriverTrial.bad(context))
assertNull(AutoDriverTrial.current(context))
}
@Test fun rollingBackKeepsADriverTheUserSelectedSince() {
SessionPrefs.setGpuAutoInstalled(context, setOf("old-display", "old-linux", "new-display", "new-linux", "chosen-linux"))
SessionPrefs.setLinuxDriver(context, "chosen-linux")
AutoDriverTrial.rollBack(context, AutoDriverTrial.current(context)!!)
assertEquals("chosen-linux", SessionPrefs.linuxDriver(context))
assertTrue("chosen-linux" in SessionPrefs.gpuAutoInstalled(context))
}
@Test fun sessionsOnOtherDriversDoNotCountTowardsTheTrial() {
AutoDriverTrial.sessionStarting(context, "old-display", "new-linux")
AutoDriverTrial.sessionStarting(context, "old-display", "new-linux")
AutoDriverTrial.sessionReady()
assertNull(AutoDriverTrial.rollbackDue(context))
assertEquals("banner@r5", AutoDriverTrial.current(context)?.release)
}
}