From 602a6fd734fccedf9f6dfbc9904ba80221cbf846 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Fri, 16 Oct 2026 16:17:44 -0400 Subject: [PATCH] feat: auto drivers update on app open and roll back if a new pair never starts a session (#972) --- .../com/droiddeck/launcher/DriverMenus.kt | 70 ++++++++---- .../com/droiddeck/launcher/MainActivity.kt | 2 +- .../launcher/frontend/AutoComponents.kt | 2 +- .../droiddeck/launcher/gpu/AutoDriverTrial.kt | 101 ++++++++++++++++++ .../launcher/session/SessionEvents.kt | 1 + .../launcher/session/SessionService.kt | 6 ++ app/src/main/res/values/strings.xml | 4 + .../launcher/gpu/AutoDriverTrialTest.kt | 65 +++++++++++ 8 files changed, 228 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/gpu/AutoDriverTrialTest.kt diff --git a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt index f7b5717b6..631d55ccf 100644 --- a/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt +++ b/app/src/main/java/com/droiddeck/launcher/DriverMenus.kt @@ -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(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 } } diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index a4ea05c3e..41f192cf9 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -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()) diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt b/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt index ced1abfbe..fc3274427 100644 --- a/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt +++ b/app/src/main/java/com/droiddeck/launcher/frontend/AutoComponents.kt @@ -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 diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt b/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt new file mode 100644 index 000000000..751ab8298 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/gpu/AutoDriverTrial.kt @@ -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 = 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) { + 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) +} diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt index 86b027af6..c70845f6c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionEvents.kt @@ -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() } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 1eb5ba1a0..a09c54242 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -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") } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 296a9b71c..b9932ad22 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1285,6 +1285,10 @@ Downloading %1$s %2$s… Updated to %1$s %2$s. Updated to %1$s %2$s. The display driver applies after DroidDeck restarts. + Driver updates wait until the session ends. + %1$s %2$s will download once you\'re off mobile data and battery saver. + %1$s %2$s didn\'t start a session here, so Auto is staying on the drivers that did. + %1$s didn\'t start a session twice, so Auto went back to the drivers that worked. Switched to %1$s %2$s. Switched to %1$s %2$s. The display driver applies after DroidDeck restarts. Using %1$s %2$s. diff --git a/app/src/test/java/com/droiddeck/launcher/gpu/AutoDriverTrialTest.kt b/app/src/test/java/com/droiddeck/launcher/gpu/AutoDriverTrialTest.kt new file mode 100644 index 000000000..d38d96b3f --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/gpu/AutoDriverTrialTest.kt @@ -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) + } +}