mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Added support for Adreno 6xx devices (#320)
This commit is contained in:
1 parent
b44235c495
commit
a1b01a04a8
5 files changed
+354
-1
No files matched your search
@@ -15,7 +15,7 @@ DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big
|
||||
|
||||
## Requirements and install
|
||||
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts.
|
||||
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts.
|
||||
|
||||
Before Steam launches, you must turn off **Restrict child processes** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.Downloader
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import com.github.luben.zstd.ZstdInputStream
|
||||
import com.github.luben.zstd.ZstdOutputStream
|
||||
@@ -660,6 +661,107 @@ object ComponentsManager {
|
||||
return Catalog(items, o.optLong("fetchedAt"))
|
||||
}
|
||||
|
||||
/**
|
||||
* A package Proton does not ship, required on the GPUs listed here. An empty [models] set
|
||||
* means every model in [families]. A row the user already chose in Components is left alone;
|
||||
* a Steam update that puts the Proton's own files back is replaced on the next session.
|
||||
*/
|
||||
internal data class ForcedPackage(
|
||||
val families: Set<GpuInfo.Family>,
|
||||
val comp: String,
|
||||
val file: String,
|
||||
val release: String,
|
||||
val models: Set<Int> = emptySet(),
|
||||
) {
|
||||
fun matches(gpu: GpuInfo) = gpu.family in families && (models.isEmpty() || gpu.model in models)
|
||||
}
|
||||
|
||||
internal val FORCED_PACKAGES = listOf(
|
||||
// DXVK 3 refuses to start unless the device exposes storageBuffer8BitAccess. Turnip on
|
||||
// Adreno 6xx does not. 2.7.1 is the last DXVK 2 package on the Dxvk-Linux tag.
|
||||
ForcedPackage(setOf(GpuInfo.Family.A6XX), "dxvk", "dxvk-2.7.1-linux.wcp", "Dxvk-Linux"),
|
||||
)
|
||||
|
||||
/**
|
||||
* True when this Proton should receive the forced package. No active swap means the Proton's
|
||||
* own copy. A record for a different Proton build means a Steam update put that copy back,
|
||||
* whichever package was recorded. A record for this build is a Components choice and stays.
|
||||
*/
|
||||
internal fun forcedPackageWanted(activeFile: String?, protonVersionMatches: Boolean): Boolean =
|
||||
activeFile.isNullOrEmpty() || !protonVersionMatches
|
||||
|
||||
/** The catalog row for this file and release, or null. */
|
||||
internal fun catalogMatch(items: List<CatalogItem>, file: String, release: String): CatalogItem? =
|
||||
items.firstOrNull { it.file == file && it.release == release }
|
||||
|
||||
/**
|
||||
* Install every [FORCED_PACKAGES] row this GPU matches into each Proton that
|
||||
* [forcedPackageWanted] selects. Returns a line for the log, or null when this GPU or these
|
||||
* Protons need nothing. A failed fetch throws and the session logs it without stopping.
|
||||
*/
|
||||
fun ensureForcedPackages(context: Context): String? = ensureForcedPackages(
|
||||
context, GpuInfo.detect(),
|
||||
{ file, release -> catalogItem(context, file, release) },
|
||||
{ download(context, it) {} },
|
||||
{ id, file -> swap(context, id, file) },
|
||||
)
|
||||
|
||||
internal fun ensureForcedPackages(
|
||||
context: Context,
|
||||
gpu: GpuInfo,
|
||||
lookup: (file: String, release: String) -> CatalogItem?,
|
||||
fetch: (CatalogItem) -> Unit,
|
||||
apply: (protonId: String, file: String) -> String,
|
||||
): String? {
|
||||
val rules = FORCED_PACKAGES.filter { it.matches(gpu) }
|
||||
if (rules.isEmpty()) return null
|
||||
val installed = protons(context)
|
||||
val lines = mutableListOf<String>()
|
||||
for (rule in rules) {
|
||||
val state = loadState(context)
|
||||
val targets = installed.filter { p ->
|
||||
val active = state.optJSONObject("active")?.optJSONObject(p.id)?.optJSONObject(rule.comp)
|
||||
forcedPackageWanted(active?.optString("file"), active?.optString("protonVersion") == p.version)
|
||||
}
|
||||
if (targets.isEmpty()) continue
|
||||
val missing = missingPackage(context, rule, lookup, fetch)
|
||||
if (missing != null) {
|
||||
lines.add(missing)
|
||||
continue
|
||||
}
|
||||
lines.add(targets.joinToString("; ") { apply(it.id, rule.file) })
|
||||
}
|
||||
return lines.joinToString("; ").ifEmpty { null }
|
||||
}
|
||||
|
||||
/** Null when [rule]'s package is stored and readable. Otherwise the log line, after a fetch. */
|
||||
private fun missingPackage(
|
||||
context: Context,
|
||||
rule: ForcedPackage,
|
||||
lookup: (file: String, release: String) -> CatalogItem?,
|
||||
fetch: (CatalogItem) -> Unit,
|
||||
): String? {
|
||||
val wcp = File(packagesDir(context), safeName(rule.file))
|
||||
if (wcp.isFile && runCatching { packageInfo(wcp) }.isSuccess) return null
|
||||
wcp.delete()
|
||||
val item = lookup(rule.file, rule.release)
|
||||
?: return "${rule.file} is not in the Nightlies listing; ${LABEL.getValue(rule.comp)} stays on the Proton's copy"
|
||||
fetch(item)
|
||||
return null
|
||||
}
|
||||
|
||||
/** The cached Nightlies row for this file, fetching the listing when the cache does not have it. */
|
||||
internal fun catalogItem(context: Context, file: String, release: String): CatalogItem? =
|
||||
resolveCatalogItem(file, release, { catalog(context, false).items }, { catalog(context, true).items })
|
||||
|
||||
/** [cached] when it has the row, otherwise [refresh]. [refresh] is not called on a hit. */
|
||||
internal fun resolveCatalogItem(
|
||||
file: String,
|
||||
release: String,
|
||||
cached: () -> List<CatalogItem>,
|
||||
refresh: () -> List<CatalogItem>,
|
||||
): CatalogItem? = catalogMatch(cached(), file, release) ?: catalogMatch(refresh(), file, release)
|
||||
|
||||
/** Downloads a catalog item into storage, verifying the release's sha256 and the package type. */
|
||||
fun download(context: Context, item: CatalogItem, progress: (Int) -> Unit): Package {
|
||||
require(item.url.startsWith("https://github.com/$NIGHTLIES/releases/download/")) { "Downloads come only from the Nightlies releases" }
|
||||
|
||||
@@ -309,6 +309,14 @@ class SessionService : Service() {
|
||||
// The desktop's Steam launchers start the client there (droiddeck-steam-launch), through the
|
||||
// same set-up as a Steam session: it gets what the client and its games are started with.
|
||||
val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP
|
||||
// Some GPUs need a component Proton does not ship. Apply those before the guest, so the
|
||||
// next game launch copies them into the prefix. A download failure is logged and the
|
||||
// session still starts; the next session tries again.
|
||||
if (steamHere) {
|
||||
runCatching { ComponentsManager.ensureForcedPackages(this) }
|
||||
.onSuccess { if (it != null) Log.i(TAG, it) }
|
||||
.onFailure { Log.w(TAG, "forced components", it) }
|
||||
}
|
||||
addClientEnvironment(guest, steamHere)
|
||||
// Where the fast path's description of proot's view goes, once the binds are known.
|
||||
val fastPathAt = guest.size
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.junit.Assert.*
|
||||
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
|
||||
import java.io.File
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class ForcedPackageTest {
|
||||
private val dxvk2 = "dxvk-2.7.1-linux.wcp"
|
||||
private lateinit var context: Context
|
||||
|
||||
@Before fun setUp() {
|
||||
context = RuntimeEnvironment.getApplication()
|
||||
File(context.filesDir, "components").deleteRecursively()
|
||||
LinuxRuntime.rootDir(context).deleteRecursively()
|
||||
}
|
||||
|
||||
@Test fun adreno6xxForcesDxvk2AndNothingElseDoes() {
|
||||
val rule = ComponentsManager.FORCED_PACKAGES.single { GpuInfo.Family.A6XX in it.families }
|
||||
assertEquals("dxvk", rule.comp)
|
||||
assertEquals(dxvk2, rule.file)
|
||||
assertEquals("Dxvk-Linux", rule.release)
|
||||
assertTrue(rule.models.isEmpty())
|
||||
assertTrue(rule.matches(gpu(650, GpuInfo.Family.A6XX)))
|
||||
assertTrue(rule.matches(gpu(610, GpuInfo.Family.A6XX)))
|
||||
assertTrue(rule.matches(gpu(690, GpuInfo.Family.A6XX)))
|
||||
assertFalse(rule.matches(gpu(740, GpuInfo.Family.A7XX)))
|
||||
assertFalse(rule.matches(gpu(830, GpuInfo.Family.A8XX)))
|
||||
assertTrue(ComponentsManager.FORCED_PACKAGES.none { GpuInfo.Family.A7XX in it.families })
|
||||
}
|
||||
|
||||
@Test fun aModelListOnlyMatchesThoseChips() {
|
||||
val rule = ComponentsManager.ForcedPackage(
|
||||
setOf(GpuInfo.Family.A7XX_LOW), "dxvk", dxvk2, "Dxvk-Linux", setOf(710),
|
||||
)
|
||||
assertTrue(rule.matches(gpu(710, GpuInfo.Family.A7XX_LOW)))
|
||||
assertFalse(rule.matches(gpu(720, GpuInfo.Family.A7XX_LOW)))
|
||||
assertFalse(rule.matches(gpu(710, GpuInfo.Family.A6XX)))
|
||||
assertFalse(ComponentsManager.ForcedPackage(emptySet(), "dxvk", dxvk2, "Dxvk-Linux").matches(gpu(650, GpuInfo.Family.A6XX)))
|
||||
}
|
||||
|
||||
@Test fun aShippedComponentIsReplacedUntilTheUserChoosesOne() {
|
||||
assertTrue(ComponentsManager.forcedPackageWanted(null, false))
|
||||
assertTrue(ComponentsManager.forcedPackageWanted(null, true))
|
||||
assertTrue(ComponentsManager.forcedPackageWanted("", false))
|
||||
assertFalse(ComponentsManager.forcedPackageWanted(dxvk2, true))
|
||||
assertTrue(ComponentsManager.forcedPackageWanted(dxvk2, false))
|
||||
assertFalse(ComponentsManager.forcedPackageWanted("dxvk-3.1-linux.wcp", true))
|
||||
assertTrue(ComponentsManager.forcedPackageWanted("dxvk-3.1-linux.wcp", false))
|
||||
assertTrue(ComponentsManager.forcedPackageWanted("dxvk-gplasync-2.7.1-1-linux.wcp", false))
|
||||
}
|
||||
|
||||
@Test fun catalogMatchPicksTheReleaseAndIgnoresAnother() {
|
||||
val wanted = item(dxvk2, "Dxvk-Linux")
|
||||
val otherRelease = item(dxvk2, "Dxvk-gplasync-Linux")
|
||||
val otherFile = item("dxvk-3.1-linux.wcp", "Dxvk-Linux")
|
||||
assertEquals(wanted, ComponentsManager.catalogMatch(listOf(otherFile, otherRelease, wanted), dxvk2, "Dxvk-Linux"))
|
||||
assertNull(ComponentsManager.catalogMatch(listOf(otherFile, otherRelease), dxvk2, "Dxvk-Linux"))
|
||||
assertNull(ComponentsManager.catalogMatch(emptyList(), dxvk2, "Dxvk-Linux"))
|
||||
}
|
||||
|
||||
@Test fun thisMachineIsNotForced() {
|
||||
assertNull(ComponentsManager.ensureForcedPackages(context))
|
||||
}
|
||||
|
||||
@Test fun anotherGpuIsLeftAlone() {
|
||||
proton("proton-exp")
|
||||
assertNull(install(gpu(740, GpuInfo.Family.A7XX)))
|
||||
}
|
||||
|
||||
@Test fun noInstalledProtonNeedsNothing() {
|
||||
assertNull(install(gpu(650, GpuInfo.Family.A6XX)))
|
||||
}
|
||||
|
||||
@Test fun aChosenPackageIsNotReplaced() {
|
||||
proton("proton-exp")
|
||||
writeState("proton-exp", "dxvk-3.1-linux.wcp", "experimental-11")
|
||||
assertNull(install(gpu(650, GpuInfo.Family.A6XX)))
|
||||
}
|
||||
|
||||
@Test fun theSamePackageOnThisProtonBuildIsNotFetchedAgain() {
|
||||
proton("proton-exp")
|
||||
writeState("proton-exp", dxvk2, "experimental-11")
|
||||
assertNull(install(gpu(650, GpuInfo.Family.A6XX)))
|
||||
}
|
||||
|
||||
@Test fun aMissingListingLeavesTheShippedCopy() {
|
||||
proton("proton-exp")
|
||||
val result = install(gpu(650, GpuInfo.Family.A6XX), lookup = { _, _ -> null })
|
||||
assertEquals("$dxvk2 is not in the Nightlies listing; DXVK stays on the Proton's copy", result)
|
||||
}
|
||||
|
||||
@Test fun aShippedCopyAndAProtonUpdateAreBothSwapped() {
|
||||
proton("kept")
|
||||
proton("proton-exp", "experimental-12")
|
||||
proton("other", "experimental-12")
|
||||
writeState("kept", "dxvk-3.1-linux.wcp", "experimental-11")
|
||||
writeState("proton-exp", dxvk2, "experimental-11")
|
||||
writeState("other", "dxvk-3.1-linux.wcp", "experimental-11")
|
||||
val fetched = mutableListOf<String>()
|
||||
val applied = mutableListOf<String>()
|
||||
val result = install(
|
||||
gpu(650, GpuInfo.Family.A6XX),
|
||||
lookup = { file, release -> item(file, release) },
|
||||
fetch = { fetched += it.file },
|
||||
apply = { id, file -> applied += "$id $file"; "DXVK 2.7.1 is now in $id. It applies the next time a game starts." },
|
||||
)
|
||||
assertEquals(listOf(dxvk2), fetched)
|
||||
assertEquals(listOf("other $dxvk2", "proton-exp $dxvk2"), applied)
|
||||
assertEquals(
|
||||
"DXVK 2.7.1 is now in other. It applies the next time a game starts.; " +
|
||||
"DXVK 2.7.1 is now in proton-exp. It applies the next time a game starts.",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun everyProtonStillOnItsShippedCopyIsSwapped() {
|
||||
proton("one")
|
||||
proton("two")
|
||||
val applied = mutableListOf<String>()
|
||||
val result = install(
|
||||
gpu(650, GpuInfo.Family.A6XX),
|
||||
lookup = { file, release -> item(file, release) },
|
||||
fetch = {},
|
||||
apply = { id, _ -> applied += id; "DXVK 2.7.1 is now in $id. It applies the next time a game starts." },
|
||||
)
|
||||
assertEquals(listOf("one", "two"), applied)
|
||||
assertEquals(
|
||||
"DXVK 2.7.1 is now in one. It applies the next time a game starts.; " +
|
||||
"DXVK 2.7.1 is now in two. It applies the next time a game starts.",
|
||||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun aBrokenStoredPackageIsFetchedAgain() {
|
||||
proton("proton-exp")
|
||||
File(context.filesDir, "components/packages/$dxvk2").apply { parentFile!!.mkdirs(); writeText("not a package") }
|
||||
val fetched = mutableListOf<String>()
|
||||
install(
|
||||
gpu(650, GpuInfo.Family.A6XX),
|
||||
lookup = { file, release -> item(file, release) },
|
||||
fetch = { fetched += it.file },
|
||||
apply = { _, _ -> "swapped" },
|
||||
)
|
||||
assertEquals(listOf(dxvk2), fetched)
|
||||
assertFalse(File(context.filesDir, "components/packages/$dxvk2").isFile)
|
||||
}
|
||||
|
||||
@Test fun aFailedFetchIsLeftForTheSessionToLog() {
|
||||
proton("proton-exp")
|
||||
val applied = mutableListOf<String>()
|
||||
val error = assertThrows(IllegalStateException::class.java) {
|
||||
install(
|
||||
gpu(650, GpuInfo.Family.A6XX),
|
||||
lookup = { file, release -> item(file, release) },
|
||||
fetch = { throw IllegalStateException("Download failed") },
|
||||
apply = { id, _ -> applied += id; "" },
|
||||
)
|
||||
}
|
||||
assertEquals("Download failed", error.message)
|
||||
assertTrue(applied.isEmpty())
|
||||
}
|
||||
|
||||
@Test fun theCachedListingIsUsedBeforeRefreshing() {
|
||||
writeCatalog()
|
||||
assertEquals(cachedUrl, ComponentsManager.catalogItem(context, dxvk2, "Dxvk-Linux")?.url)
|
||||
}
|
||||
|
||||
@Test fun aCacheMissReadsTheRefreshedListingOnce() {
|
||||
var refreshes = 0
|
||||
val refreshed = item(dxvk2, "Dxvk-Linux")
|
||||
val found = ComponentsManager.resolveCatalogItem(dxvk2, "Dxvk-Linux", { emptyList() }, { refreshes += 1; listOf(refreshed) })
|
||||
assertEquals(refreshed, found)
|
||||
assertEquals(1, refreshes)
|
||||
assertNull(ComponentsManager.resolveCatalogItem("missing.wcp", "Dxvk-Linux", { emptyList() }, { refreshes += 1; emptyList() }))
|
||||
assertEquals(2, refreshes)
|
||||
val cached = item(dxvk2, "Dxvk-Linux")
|
||||
assertEquals(cached, ComponentsManager.resolveCatalogItem(dxvk2, "Dxvk-Linux", { listOf(cached) }, { error("refresh") }))
|
||||
}
|
||||
|
||||
private fun gpu(model: Int, family: GpuInfo.Family) = GpuInfo("Adreno $model", model, family, "", false)
|
||||
|
||||
private fun install(
|
||||
gpu: GpuInfo,
|
||||
lookup: (String, String) -> ComponentsManager.CatalogItem? = ::refuseLookup,
|
||||
fetch: (ComponentsManager.CatalogItem) -> Unit = ::refuseFetch,
|
||||
apply: (String, String) -> String = ::refuseApply,
|
||||
) = ComponentsManager.ensureForcedPackages(context, gpu, lookup, fetch, apply)
|
||||
|
||||
private fun refuseLookup(file: String, release: String): ComponentsManager.CatalogItem? = error("catalog $file $release")
|
||||
|
||||
private fun refuseFetch(item: ComponentsManager.CatalogItem) { error("fetch ${item.file}") }
|
||||
|
||||
private fun refuseApply(protonId: String, file: String): String = error("apply $protonId $file")
|
||||
|
||||
private fun proton(name: String, version: String = "experimental-11") {
|
||||
val dir = File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/common/$name")
|
||||
File(dir, "files/lib/wine").mkdirs()
|
||||
File(dir, "proton").writeText("#!/bin/sh\n")
|
||||
File(dir, "version").writeText("1 $version\n")
|
||||
}
|
||||
|
||||
private fun writeState(protonId: String, file: String, protonVersion: String) {
|
||||
val state = File(context.filesDir, "components/state.json")
|
||||
state.parentFile!!.mkdirs()
|
||||
val active = if (state.isFile) JSONObject(state.readText()).getJSONObject("active") else JSONObject()
|
||||
active.put(protonId, JSONObject().put("dxvk", JSONObject().put("file", file).put("protonVersion", protonVersion)))
|
||||
state.writeText(JSONObject().put("active", active).put("queued", JSONObject()).toString())
|
||||
}
|
||||
|
||||
private val cachedUrl = "https://github.com/The412Banner/Nightlies/releases/download/Dxvk-Linux/$dxvk2?cached=1"
|
||||
|
||||
private fun writeCatalog() {
|
||||
val cache = File(context.filesDir, "components/catalog.json")
|
||||
cache.parentFile!!.mkdirs()
|
||||
cache.writeText(JSONObject()
|
||||
.put("fetchedAt", 1)
|
||||
.put("items", JSONArray().put(JSONObject()
|
||||
.put("file", dxvk2).put("comp", "dxvk").put("release", "Dxvk-Linux")
|
||||
.put("url", cachedUrl).put("size", 4).put("digest", "sha256:" + "ab".repeat(32))))
|
||||
.toString())
|
||||
}
|
||||
|
||||
private fun item(file: String, release: String) = ComponentsManager.CatalogItem(
|
||||
file, "dxvk", release, "https://github.com/The412Banner/Nightlies/releases/download/$release/$file", 4, "sha256:" + "ab".repeat(32),
|
||||
)
|
||||
}
|
||||
@@ -23,6 +23,11 @@ and explicit removals take precedence over these defaults. FEX's own default
|
||||
remains the default preset. Shader caching in VKD3D/DXVK, synchronization, ray
|
||||
tracing and diagnostic logging otherwise retain the runtime's defaults.
|
||||
|
||||
On an Adreno 6xx, Turnip does not expose `storageBuffer8BitAccess`, which DXVK 3
|
||||
requires. A Steam or desktop session installs DXVK 2.7.1 into any Proton whose
|
||||
DXVK is still the copy that Proton shipped, including after a Proton update put
|
||||
that copy back. A DXVK package chosen in Components stays for that Proton build.
|
||||
|
||||
The variable-name picker offers 24 predefined variables and a Custom entry. Like
|
||||
WinNative, known variables use toggles, value dropdowns, multi-select lists, or
|
||||
numeric/text fields. Feature-level and shader-model dropdowns also accept custom
|
||||
|
||||
Reference in new issue
Block a user