mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-18 21:27:16 -04:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5272eefc | ||
|
|
d4b3de0b07 | ||
|
|
d063741856 |
No files matched your search
@@ -47,7 +47,7 @@ Compatibility and performance vary by device; hardware validation is limited. De
|
||||
|
||||
## Credits and licence
|
||||
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. The Steam client's audio goes through [@The412Banner](https://github.com/The412Banner)'s [DirectAudio](https://github.com/The412Banner/directaudio) PulseAudio sink (LGPL-2.1-or-later), built from `tools/directaudio/sink` with its request size kept to its ring so clients are not asked to queue 100 ms ahead. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). Why the x86-64 Steam client refused the arm64 client's sign-in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
GPL-3.0. Runtime, shim, input, and controller work build on WinNative and Bannerlator (maxjivi05). Epic Games and GOG support build on [Phobos’s](https://github.com/phobos665) adaptation of [Legendary](https://github.com/legendary-gl/legendary) and [gogdl](https://github.com/Heroic-Games-Launcher/heroic-gogdl), respectively. LSFG frame generation is from the work of Camille LaVey and the [Eden](https://eden-emu.dev) emulator project, following [lsfg-vk](https://github.com/PancakeTAS/lsfg-vk), ported to WinNative and DroidDeck by [@maxjivi05](https://github.com/maxjivi05); it needs your own copy of [Lossless Scaling](https://store.steampowered.com/app/993090/) and ships none of its shaders. x86 AppImages, and any whose own runtime cannot unpack them, are unpacked with [uruntime](https://github.com/VHSgunzo/uruntime) by VHSgunzo (MIT), shipped unmodified with its licence. mmap ring-buffer controller input was developed by ribbit384 ([WinNative #345](https://github.com/WinNative-Emu/WinNative/pull/345)). See [LICENSE](LICENSE). Steam and Proton belong to Valve Corporation; this project is not affiliated with Valve.
|
||||
|
||||
XR glasses support builds on WinNative's VITURE glasses work. VITURE protocol details were checked against [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) by [@wheaney](https://github.com/wheaney), including the VITURE protocol header it ships, [ar-drivers-rs](https://github.com/badicsalex/ar-drivers-rs) by [@badicsalex](https://github.com/badicsalex) and the [go-macos/iokit VITURE package](https://pkg.go.dev/github.com/go-macos/iokit/viture); the optional VITURE SDK belongs to VITURE and is not included. RayNeo support adapts [@informalTechCode](https://github.com/informalTechCode)'s [RayNeo-Air-Series-3DoF-Scaffold](https://github.com/informalTechCode/RayNeo-Air-Series-3DoF-Scaffold) (Apache-2.0) and references [@verncat](https://github.com/verncat)'s [RayNeo-Air-3S-Pro-OpenVR](https://github.com/verncat/RayNeo-Air-3S-Pro-OpenVR) (MIT) and [Monado](https://gitlab.freedesktop.org/monado/monado) (BSL-1.0); see [docs/third-party/rayneo](docs/third-party/rayneo/README.md). XREAL 1S support references [XRLinuxDriver](https://github.com/wheaney/XRLinuxDriver) and [xrealOneDeviceKit](https://github.com/wheaney/xrealOneDeviceKit) by @wheaney, [xreal_one_driver](https://github.com/rohitsangwan01/xreal_one_driver) by [@rohitsangwan01](https://github.com/rohitsangwan01) (MIT) and [xr-tools](https://github.com/0xcaff/xr-tools) by [@0xcaff](https://github.com/0xcaff). The REDMAGIC glasses-display findings draw on [@mekhontsev](https://github.com/mekhontsev)'s [MagicDesk investigation](https://github.com/mekhontsev/magicdesk/blob/main/docs/xr-resolution-fix.md) and [@Coding-BR](https://github.com/Coding-BR)'s [decompiled Nubia kernel](https://github.com/Coding-BR/android_kernel_nubia_sm8850_qwjujube). VITURE, RayNeo, XREAL and REDMAGIC are trademarks of their owners; this project is not affiliated with them.
|
||||
|
||||
|
||||
@@ -192,8 +192,6 @@ android {
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.includeAndroidResources = true
|
||||
unitTests.all {
|
||||
// Robolectric keeps a sandbox per SDK level the tests ask for; seven no longer fit in Gradle's default 512 MB.
|
||||
maxHeapSize = '2g'
|
||||
def logTarget = providers.systemProperty('robolectric.logging').orNull
|
||||
if (logTarget != null) systemProperty 'robolectric.logging', logTarget
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
deckyChecking = true
|
||||
Thread({
|
||||
val channels = runCatching { DeckyManager.releaseChannels(activity) }
|
||||
.getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) }
|
||||
.getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList(), emptyList()) }
|
||||
ui.post {
|
||||
if (request == deckyReleaseRequest) {
|
||||
deckyReleases = channels.stable.ifEmpty { channels.prerelease }
|
||||
deckyReleases = channels.newest
|
||||
deckyChecking = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1882,7 +1882,7 @@ class MainActivity : ComponentActivity() {
|
||||
val listX64 = SessionPrefs.activeSteamClientArch(this) == "x86_64"
|
||||
if (listX64 != steamGamesX64) {
|
||||
steamGamesX64 = listX64
|
||||
steamGames = steamGames.filterNot { Library.fromSteamClient(it) || (it.library == Library.ADDED && it.gameId in 1..0xFFFFFFFFL) }
|
||||
steamGames = steamGames.filterNot(Library::fromSteamClient)
|
||||
}
|
||||
if (!busy && LinuxRuntimeInstaller.isBusy()) followRuntimeOperation { LinuxRuntimeInstaller.attach(it) }
|
||||
if (showComponents) drivers.refreshDrivers()
|
||||
@@ -1916,13 +1916,12 @@ class MainActivity : ComponentActivity() {
|
||||
ui.post { if (scanGeneration == libraryScanGeneration) emulatorList = emus }
|
||||
if (ready) {
|
||||
val quick = runCatching { Library.steamGames(this) }.getOrNull()
|
||||
val claimed = quick?.let { runCatching { Library.claimedFolders(it) }.getOrDefault(emptySet()) }.orEmpty()
|
||||
if (quick != null) ui.post {
|
||||
if (scanGeneration == libraryScanGeneration && steamGames.isNotEmpty()) {
|
||||
val before = steamGames.map { it.gameId }.toSet()
|
||||
val fresh = quick.filter { it.gameId !in before }
|
||||
if (fresh.isNotEmpty()) {
|
||||
steamGames = (quick + steamGames.filter { it.library == Library.ADDED && it.gameFiles?.absolutePath.let { path -> path == null || path !in claimed } }).distinctBy { it.gameId }
|
||||
steamGames = (quick + steamGames.filter { it.library == Library.ADDED }).distinctBy { it.gameId }
|
||||
fresh.forEach { com.droiddeck.launcher.frontend.AutoComponents.queueGame(this, it, asked = false) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1609,9 +1609,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
glassesScreen?.hz?.takeIf { it > 1f }?.let { return it }
|
||||
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
|
||||
val wanted = window.attributes.preferredDisplayModeId
|
||||
val requested = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
|
||||
val running = display?.refreshRate?.takeIf { it > 1f }
|
||||
val hz = listOfNotNull(requested, running).minOrNull() ?: 60f
|
||||
val hz = display?.supportedModes?.firstOrNull { wanted != 0 && it.modeId == wanted }?.refreshRate
|
||||
?: display?.refreshRate ?: 60f
|
||||
val mirrorCap = mirrorRefreshLimit().takeIf { it > 0 }?.toFloat() ?: Float.POSITIVE_INFINITY
|
||||
val capped = minOf(hz, PerfMode.peakRefreshCap(this), mirrorCap)
|
||||
return if (capped > 1f) capped else 60f
|
||||
|
||||
@@ -72,7 +72,6 @@ object Library {
|
||||
private val NAME = Regex("^\\s*\"name\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
private val STATE = Regex("^\\s*\"StateFlags\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val LAST_PLAYED = Regex("^\\s*\"LastPlayed\"\\s*\"(\\d+)\"", RegexOption.MULTILINE)
|
||||
private val PRIVATE_APPS = Regex("\"PrivateApps_\\d+\"\\s*\"\\[([^\\]]*)\\]\"")
|
||||
private val INSTALL_DIR = Regex("^\\s*\"installdir\"\\s*\"([^\"]*)\"", RegexOption.MULTILINE)
|
||||
|
||||
internal fun steamLibraries(context: Context, arch: String = SessionPrefs.activeSteamClientArch(context)): List<Pair<File, String>> {
|
||||
@@ -138,35 +137,13 @@ object Library {
|
||||
}
|
||||
|
||||
fun steamLibraryArt(context: Context): List<Pair<Int, File>> {
|
||||
val root = SessionPrefs.activeSteamRoot(context)
|
||||
val cache = File(root, "appcache/librarycache")
|
||||
val concealed = concealedSteamApps(root)
|
||||
val cache = File(SessionPrefs.activeSteamRoot(context), "appcache/librarycache")
|
||||
return cache.listFiles().orEmpty().mapNotNull { dir ->
|
||||
val appId = dir.name.toIntOrNull()?.takeIf { it !in NOT_GAMES && it.toLong() !in concealed } ?: return@mapNotNull null
|
||||
val appId = dir.name.toIntOrNull()?.takeIf { it !in NOT_GAMES } ?: return@mapNotNull null
|
||||
steamCacheImage(cache, appId, STEAM_CAPSULES)?.let { appId to it }
|
||||
}
|
||||
}
|
||||
|
||||
/** Apps the user hid (the "hidden" collection) or marked private in Steam. */
|
||||
internal fun concealedSteamApps(root: File): Set<Long> {
|
||||
val config = AddedGames.accountConfig(root) ?: return emptySet()
|
||||
val hidden = runCatching {
|
||||
val entries = org.json.JSONArray(File(config, "cloudstorage/cloud-storage-namespace-1.json").readText())
|
||||
val entry = (0 until entries.length()).map { entries.getJSONArray(it) }
|
||||
.firstOrNull { it.optString(0) == "user-collections.hidden" }?.optJSONObject(1)
|
||||
if (entry == null || entry.optBoolean("is_deleted")) emptySet() else {
|
||||
val collection = org.json.JSONObject(entry.optString("value"))
|
||||
fun ids(key: String) = collection.optJSONArray(key)?.let { a -> (0 until a.length()).map { a.optLong(it) } }.orEmpty()
|
||||
ids("added").toSet() - ids("removed").toSet()
|
||||
}
|
||||
}.getOrDefault(emptySet())
|
||||
val private = runCatching {
|
||||
PRIVATE_APPS.find(File(config, "localconfig.vdf").readText())?.groupValues?.get(1)
|
||||
?.split(',')?.mapNotNull { it.trim().toLongOrNull() }?.toSet()
|
||||
}.getOrNull().orEmpty()
|
||||
return hidden + private
|
||||
}
|
||||
|
||||
fun fromSteamClient(g: SteamGame): Boolean = g.library != ADDED && g.source == SOURCE_STEAM
|
||||
|
||||
fun launchableGames(
|
||||
@@ -178,23 +155,9 @@ object Library {
|
||||
var steam = steamGames(context, strictRead, arch)
|
||||
val targets = steam.filter(::fromSteamClient).map { SteamGrid.target(it, context) } + list.map { SteamGrid.target(it, context) }
|
||||
if (runCatching { SteamGrid.sync(context, targets) }.getOrDefault(false)) steam = steamGames(context, strictRead, arch)
|
||||
val claimed = claimedFolders(steam)
|
||||
val own = list.filter { g ->
|
||||
g.steamAppId != null || claimed.isEmpty() ||
|
||||
(runCatching { g.folder.canonicalPath }.getOrDefault(g.folder.absolutePath) !in claimed &&
|
||||
AddedGames.guestPath(context, g.folder).let { it == null || it !in claimed })
|
||||
}
|
||||
return (steam + own.map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
return (steam + list.map { g -> addedGame(context, g, roots) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
fun claimedFolders(steam: List<SteamGame>): Set<String> = steam.filter(::fromSteamClient).flatMap { g ->
|
||||
val dir = g.gameFiles ?: return@flatMap emptyList()
|
||||
listOfNotNull(
|
||||
runCatching { dir.canonicalPath }.getOrNull(),
|
||||
runCatching { java.nio.file.Files.readSymbolicLink(dir.toPath()).toString() }.getOrNull(),
|
||||
)
|
||||
}.toSet()
|
||||
|
||||
fun addedGame(context: Context, g: AddedGames.Game, roots: List<File> = steamLibraries(context).map { it.first }): SteamGame =
|
||||
AddedGameArt.resolve(context, g).let { art ->
|
||||
SteamGame(
|
||||
|
||||
@@ -32,7 +32,7 @@ object DeckyManager {
|
||||
private set
|
||||
|
||||
data class Release(val tag: String, val prerelease: Boolean, val asset: String, val url: String, val digest: String?, val shaUrl: String?, val size: Long, val machine: Int)
|
||||
data class ReleaseChannels(val stable: List<Release>, val prerelease: List<Release>)
|
||||
data class ReleaseChannels(val stable: List<Release>, val prerelease: List<Release>, val newest: List<Release>)
|
||||
|
||||
fun installed(context: Context): String? {
|
||||
val loader = loader(context)
|
||||
@@ -80,11 +80,13 @@ object DeckyManager {
|
||||
}
|
||||
|
||||
fun releaseChannels(context: Context): ReleaseChannels {
|
||||
val body = Downloader.downloadString(RELEASES) ?: return ReleaseChannels(emptyList(), emptyList())
|
||||
val body = Downloader.downloadString(RELEASES) ?: return ReleaseChannels(emptyList(), emptyList(), emptyList())
|
||||
return try {
|
||||
val array = JSONArray(body)
|
||||
val stable = mutableListOf<Release>()
|
||||
val prerelease = mutableListOf<Release>()
|
||||
// GitHub lists releases newest first, so this keeps one ordered list across both channels.
|
||||
val newest = mutableListOf<Release>()
|
||||
for (i in 0 until array.length()) {
|
||||
val release = array.getJSONObject(i)
|
||||
if (release.optBoolean("draft", false)) continue
|
||||
@@ -106,9 +108,10 @@ object DeckyManager {
|
||||
if (size <= 0) continue
|
||||
val row = Release(release.optString("tag_name", "unknown"), isPrerelease, name, url, digest, checksum, size, if (name == "PluginLoader-arm64") 183 else 62)
|
||||
(if (isPrerelease) prerelease else stable).add(row)
|
||||
newest.add(row)
|
||||
}
|
||||
ReleaseChannels(stable, prerelease)
|
||||
} catch (e: Exception) { Log.w(TAG, "release metadata", e); ReleaseChannels(emptyList(), emptyList()) }
|
||||
ReleaseChannels(stable, prerelease, newest)
|
||||
} catch (e: Exception) { Log.w(TAG, "release metadata", e); ReleaseChannels(emptyList(), emptyList(), emptyList()) }
|
||||
}
|
||||
|
||||
fun install(context: Context, release: Release, progress: (String, Int) -> Unit): String? {
|
||||
|
||||
@@ -214,9 +214,6 @@ public final class LinuxRuntime {
|
||||
bind(cmd, fake.getPath() + ":" + entry[1]);
|
||||
}
|
||||
}
|
||||
if (unreadablePciDevice()) {
|
||||
bind(cmd, new File(root, "etc/droiddeck/empty").getPath() + ":/sys/bus/pci/devices");
|
||||
}
|
||||
bindGpuNode(context, cmd);
|
||||
bindAdrenoStats(cmd, extraBinds);
|
||||
bindCpuTemps(cmd, root);
|
||||
@@ -228,15 +225,6 @@ public final class LinuxRuntime {
|
||||
return specs;
|
||||
}
|
||||
|
||||
private static boolean unreadablePciDevice() {
|
||||
File[] devices = new File("/sys/bus/pci/devices").listFiles();
|
||||
if (devices == null) return false;
|
||||
for (File device : devices) {
|
||||
if (!new File(device, "vendor").canRead()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void bindGpuNode(Context context, List<String> cmd) {
|
||||
StructStat st;
|
||||
try {
|
||||
|
||||
@@ -22,17 +22,4 @@ class LibraryArchTest {
|
||||
assertEquals(listOf("Team Fortress 2"), Library.launchableGames(app, added = emptyList(), arch = "x86_64").map { it.name })
|
||||
assertEquals(emptyList<String>(), Library.launchableGames(app, added = emptyList(), arch = "arm64").map { it.name })
|
||||
}
|
||||
|
||||
@Test fun anAddedGameSteamAlreadyListsFromItsFolderIsShownOnce() {
|
||||
SessionPrefs.setGameStorage(app, SessionPrefs.GAME_STORAGE_OFF, "")
|
||||
val folder = File(app.filesDir, "games/Example").apply { mkdirs() }
|
||||
val exe = File(folder, "Example.exe").apply { writeText("game") }
|
||||
val apps = File(SessionPrefs.steamRoot(app, "arm64"), "steamapps").apply { mkdirs() }
|
||||
java.nio.file.Files.createSymbolicLink(File(apps, "common").apply { mkdirs() }.toPath().resolve("DroidDeck-42"), folder.toPath())
|
||||
File(apps, "appmanifest_42.acf").writeText("\"AppState\"\n{\n\t\"appid\"\t\t\"42\"\n\t\"name\"\t\t\"Example\"\n\t\"StateFlags\"\t\t\"4\"\n\t\"installdir\"\t\t\"DroidDeck-42\"\n}\n")
|
||||
val added = AddedGames.Game(folder, "Example", exe, "/g/Example.exe", "/g", 0x87654321L, (0x87654321L shl 32) or 0x02000000L, emptyList())
|
||||
val other = File(app.filesDir, "games/Other").apply { mkdirs() }
|
||||
val second = AddedGames.Game(other, "Other", File(other, "Other.exe"), "/g/Other.exe", "/g", 0x81234567L, (0x81234567L shl 32) or 0x02000000L, emptyList())
|
||||
assertEquals(listOf("Example" to "internal", "Other" to Library.ADDED), Library.launchableGames(app, added = listOf(added, second), arch = "arm64").map { it.name to it.library })
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.droiddeck.launcher.frontend
|
||||
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
class LibraryConcealedAppsTest {
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
private fun steamRoot(): File = tmp.newFolder("Steam").also { root ->
|
||||
File(root, "config").mkdirs()
|
||||
File(root, "config/loginusers.vdf").writeText(
|
||||
"\"users\"\n{\n\t\"76561198352563669\"\n\t{\n\t\t\"AccountName\"\t\t\"a\"\n\t\t\"MostRecent\"\t\t\"1\"\n\t}\n}\n")
|
||||
}
|
||||
|
||||
private fun config(root: File) = File(root, "userdata/392297941/config").apply { mkdirs() }
|
||||
|
||||
@Test fun hiddenCollectionAndPrivateAppsAreBothConcealed() {
|
||||
val root = steamRoot()
|
||||
val config = config(root)
|
||||
File(config, "cloudstorage").mkdirs()
|
||||
File(config, "cloudstorage/cloud-storage-namespace-1.json").writeText(
|
||||
"""[["user-collections.favorite",{"key":"user-collections.favorite","value":"{\"id\":\"favorite\",\"added\":[620],\"removed\":[]}"}],""" +
|
||||
"""["user-collections.hidden",{"key":"user-collections.hidden","value":"{\"id\":\"hidden\",\"name\":\"Hidden\",\"added\":[223470,2931908220,400],\"removed\":[400]}"}]]""")
|
||||
File(config, "localconfig.vdf").writeText(
|
||||
"\"UserLocalConfigStore\"\n{\n\t\"WebStorage\"\n\t{\n\t\t\"PrivateApps_392297941\"\t\t\"[339800,567090]\"\n\t}\n}\n")
|
||||
assertEquals(setOf(223470L, 2931908220L, 339800L, 567090L), Library.concealedSteamApps(root))
|
||||
}
|
||||
|
||||
@Test fun deletedHiddenCollectionAndMissingFilesConcealNothing() {
|
||||
val root = steamRoot()
|
||||
assertEquals(emptySet<Long>(), Library.concealedSteamApps(root))
|
||||
val config = config(root)
|
||||
File(config, "cloudstorage").mkdirs()
|
||||
File(config, "cloudstorage/cloud-storage-namespace-1.json").writeText(
|
||||
"""[["user-collections.hidden",{"key":"user-collections.hidden","is_deleted":true}]]""")
|
||||
assertEquals(emptySet<Long>(), Library.concealedSteamApps(root))
|
||||
}
|
||||
}
|
||||
@@ -62,12 +62,6 @@ class GlassesMirrorPacingTest {
|
||||
assertEquals(30, SessionState.fpsLimit)
|
||||
}
|
||||
|
||||
@Test fun pacingFollowsThePanelWhenTheSystemKeepsALowerRateThanRequested() {
|
||||
Shadow.extract<ModeDisplay>(activity.display).current = mode(2, 90f)
|
||||
apply(GlassesState(), settings)
|
||||
expect(3, 90f, 0)
|
||||
}
|
||||
|
||||
private fun apply(state: GlassesState, config: GlassesSettings) {
|
||||
ReflectionHelpers.getField<MutableState<GlassesState>>(activity, "glasses\$delegate").value = state
|
||||
ReflectionHelpers.getField<MutableState<GlassesSettings>>(activity, "glassesSettings\$delegate").value = config
|
||||
@@ -89,6 +83,5 @@ class GlassesMirrorPacingTest {
|
||||
var supported = emptyArray<Display.Mode>()
|
||||
@Implementation protected fun getMode(): Display.Mode = current
|
||||
@Implementation protected fun getSupportedModes(): Array<Display.Mode> = supported
|
||||
@Implementation override fun getRefreshRate(): Float = current.refreshRate
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,6 @@ arm64 client down first.
|
||||
standalone launcher honours up to five times, as `steam.sh` does).
|
||||
- Before each start `x64_sync_client` brings the client to Valve's current build, downloading only the
|
||||
changed parts (9 of 28 on a device). If that fails, the client updates itself.
|
||||
- The client is the `steamrt64` platform (`steamrt64/steam`, the manifest it checks for updates), so
|
||||
the install, the sync and the web-helper build check read `steam_client_<channel>_steamrt64`. The
|
||||
`ubuntu12` manifest lags it (device: 1792108788 against 1792192922): an install from it was always
|
||||
one build behind the arm64 client, so the arm64 web helper was never used, and the client updated
|
||||
itself in place at its first start (506 MB) after the helper had already been chosen.
|
||||
- `.steam-enable-steamrt64-client` in the client folder opts into the SteamRT3 build
|
||||
(`steamrt64/steam`). Without it the client re-executes itself in the scout environment through
|
||||
`steam.sh`, whose start-up checks need what proot cannot give; the session stalled before the
|
||||
@@ -124,11 +119,6 @@ The client's web helper draws its whole UI. There are two:
|
||||
backend through FEX's GL thunk to zink and Turnip. On ANGLE's Vulkan backend the x86 compositor
|
||||
thread was the bottleneck (16 fps against 60). Never `--in-process-gpu`, the preload's default: a
|
||||
GPU crash there took the whole helper down (102 restarts). `BL_X64_CEF_GPU=0` keeps it in software.
|
||||
Its GPU process loads libpci, which exits when `/sys/bus/pci/devices` lists a device whose files
|
||||
the app may not read (a RedMagic's PCIe root ports): every GPU process exited with status 1 in about
|
||||
50 ms, software ones too, until Chromium gave up ("GPU process isn't usable") and the menus stayed
|
||||
blank. `LinuxRuntime` binds an empty folder over `/sys/bus/pci/devices` on such devices; device:
|
||||
0 GPU process exits, ANGLE on zink and Turnip.
|
||||
|
||||
The session shim (`preload/net.c`) turns `steamrt64/steamwebhelper.sh`'s exec of `./steamwebhelper`
|
||||
into `BL_X64_ARM_WEBHELPER`. Nothing in the client's folder is edited: the client checksums its files
|
||||
@@ -227,7 +217,7 @@ alone would then read as an installed client.
|
||||
|
||||
- **What a session log shows.** `FEX ready (<tag>, <source>)` names the build and where it came from
|
||||
(`catalog`, `built-in`, or `kept` when a newer one did not install, with the reason a line above);
|
||||
`x86-64 rootfs:` names the image; `x86-64 client:` the client's steamrt64 build at start
|
||||
`x86-64 rootfs:` names the image; `x86-64 client:` the client's ubuntu12 and steamrt64 builds at start
|
||||
(it may update itself after). The rootfs download reports every tenth of its size as a step. When the
|
||||
web helper's GPU process keeps exiting until Chromium gives up on the GPU (the menus then never appear),
|
||||
one line says so with the exit codes, followed by x86 `glxinfo -B` and `vulkaninfo --summary` through
|
||||
@@ -311,24 +301,15 @@ alone would then read as an installed client.
|
||||
in 51 s.
|
||||
- **Sign-in.** Before either client starts, the newer sign-in (`config/loginusers.vdf`, the login token
|
||||
in `local.vdf`, `AutoLoginUser` and `RememberPassword` in `registry.vdf`) is copied to the other, so
|
||||
signing in or switching accounts in one counts for both; never while either client runs, since each
|
||||
rewrites these files. A client that refused its token rewrites `local.vdf` with an empty
|
||||
`ConnectCache`, so it is never the source. The copy keeps the time stamp, so both match until one
|
||||
client signs in again. The rest of `registry.vdf` (language, skin) stays each client's.
|
||||
- **Account IDs.** A client logs on with the token only once it knows the account's SteamID, which it
|
||||
takes from `Accounts` in `config/config.vdf`. The x86 client's `config.vdf` is made by this script
|
||||
without one, so it logged on as `[U:1:0]` with the arm64 token and Steam answered "Invalid Password"
|
||||
(and the client then dropped the token and `AutoLoginUser`): every switch asked for the password.
|
||||
Each start adds the accounts either client knows to the other's `Accounts`, and an `AutoLoginUser` or
|
||||
`RememberPassword` one client has and the other lacks. Device: the x86 client signed in with the
|
||||
arm64 token (`RecvMsgClientLogOnResponse ... 'OK'`).
|
||||
signing in or switching accounts in one counts for both. Steam renews its token every few days and
|
||||
refuses an old copy (a 3-day-old one asked for the password), so the newest is copied at every start,
|
||||
never while either client runs, since each rewrites these files. A client that refused its token
|
||||
rewrites `local.vdf` with an empty `ConnectCache`: newest, but copied over the other it would sign
|
||||
that one out too (seen after the x86 client's "Invalid Password"), so it is never the source. The copy keeps
|
||||
the time stamp, so both match until one client signs in again. The rest of `registry.vdf`
|
||||
(language, skin) stays each client's.
|
||||
- **App side.** The library, art picker, grid sync and boot movie follow the running client, else the
|
||||
selected one; a changed client applies from the next session. A game is listed once per client: an added game
|
||||
whose folder a Steam install already covers (its `DroidDeck-<appid>` import) is shown as that Steam
|
||||
game only, and a client switch drops the old client's imported entries with its Steam games. In
|
||||
Steam, an import stays one when its ownership cannot be confirmed that session instead of gaining a
|
||||
shortcut beside it, and only the client's own library folders count as an install (the arm64 card
|
||||
library made the x86 client skip a game's import and its shortcut). Offline Mode is written to both
|
||||
selected one; a changed client applies from the next session. Offline Mode is written to both
|
||||
clients' `loginusers.vdf`; until the x86 client's first start its sign-in is the arm64 one. The log
|
||||
redactor learns account names from both clients. The startup movie is staged into the x86 client's
|
||||
config only once its first start has created it; `--prepare` links the arm64 `config/uioverrides`
|
||||
|
||||
@@ -23,8 +23,7 @@ if [[ ! -d "$TOOLCHAIN" && "$NDK_HOST" == linux-aarch64 && -d "$NDK/toolchains/l
|
||||
TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
fi
|
||||
CC="$TOOLCHAIN/aarch64-linux-android${API}-clang"
|
||||
CC28="$TOOLCHAIN/aarch64-linux-android28-clang"
|
||||
if [[ ! -x "$CC" || ! -x "$CC28" ]]; then
|
||||
if [[ ! -x "$CC" ]]; then
|
||||
echo "Android NDK toolchain not found at $TOOLCHAIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -35,8 +34,8 @@ sed -e 's/@PA_MAJOR@/13/g' -e 's/@PA_MINOR@/0/g' -e 's/@PA_API_VERSION@/12/g' -e
|
||||
"$PA_SRC/src/pulse/version.h.in" > "$INC/pulse/version.h"
|
||||
cp "$HERE/config.h" "$HERE/ltdl.h" "$INC/"
|
||||
build_one() {
|
||||
local src=$1 OUT=$2 extra=$3 cc=${4:-$CC}
|
||||
"$cc" -O2 -shared -fPIC -Wall -Wno-unused-parameter -Wno-error=int-conversion -Wno-visibility -DHAVE_CONFIG_H \
|
||||
local src=$1 OUT=$2 extra=$3
|
||||
"$CC" -O2 -shared -fPIC -Wall -Wno-unused-parameter -Wno-error=int-conversion -Wno-visibility -DHAVE_CONFIG_H \
|
||||
-I"$INC" -I"$PA_SRC/src" -I"$HERE" \
|
||||
-o "$OUT" "$HERE/$src" \
|
||||
-L"$LIBS" -l:libpulsecore-13.0.so -l:libpulsecommon-13.0.so -l:libpulse.so $extra
|
||||
@@ -55,4 +54,3 @@ build_one() {
|
||||
ls -l "$OUT"
|
||||
}
|
||||
build_one module-aaudio-sink.c "$OUTDIR/module-aaudio-sink.so" -laaudio
|
||||
build_one ../directaudio/sink/module-directaudio-native-sink.c "$OUTDIR/module-directaudio-native-sink.so" -laaudio "$CC28"
|
||||
@@ -1,502 +0,0 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -1,209 +0,0 @@
|
||||
/* DirectAudio relay - the contract between the driver and the relay helper.
|
||||
*
|
||||
* Copyright 2026 The412Banner
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2.1 of the License, or (at your option) any later version.
|
||||
*
|
||||
* WHY THIS EXISTS. The driver's whole premise is an in-process AAudio client:
|
||||
* the guest's WASAPI buffers are mixed and handed to Android's audio server
|
||||
* from inside the game, no daemon in the path. That is only possible when the
|
||||
* game process is a bionic process. Under a glibc runtime (the Linux Steam
|
||||
* client, where the game runs inside a proot'd Arch rootfs on Valve's own
|
||||
* ARM64 Proton) libaaudio cannot be loaded at all: it is a bionic library and
|
||||
* the rootfs's dynamic linker does not know how to bring it into a glibc
|
||||
* process. The game side therefore has to stop at a boundary it CAN cross, and
|
||||
* the only thing both worlds agree on is the kernel: a unix socket, a memfd and
|
||||
* a futex word.
|
||||
*
|
||||
* SHAPE. One helper process (directaudio-relay, bionic, links libaaudio) runs
|
||||
* on the Android side and listens on a unix socket. Each game process that
|
||||
* needs audio connects once, sends its launch config (the same knobs the
|
||||
* in-process build reads from BANNER_AUDIO_DIRECT_*), and gets back one or two
|
||||
* shared rings (memfds passed with SCM_RIGHTS):
|
||||
*
|
||||
* render ring game (producer) -> helper (consumer) -> AAudio OUTPUT
|
||||
* capture ring helper (producer, AAudio INPUT) -> game (consumer)
|
||||
*
|
||||
* The driver keeps its mixer and its per-voice WASAPI rings exactly as in the
|
||||
* in-process build; what used to be the AAudio data callback is now a pump
|
||||
* thread that keeps the render ring topped up, and the capture callback is a
|
||||
* pump that drains the capture ring into the capture voices. Everything that
|
||||
* needs an AAudioStream (open, adaptive buffer growth and decay, route-change
|
||||
* reopen, the stalled-callback watchdog, the live-config mailbox) moves to the
|
||||
* helper unchanged in spirit.
|
||||
*
|
||||
* COST. One extra hand-off: the ring. The helper's callback pulls a burst from
|
||||
* the ring, so the ring must hold at least one burst at every callback; the
|
||||
* driver keeps DA_RING_TARGET_BURSTS bursts queued and the helper raises that
|
||||
* target when a callback finds the ring short. Everything else - the AAudio
|
||||
* buffer, AudioFlinger's own latency - is what the in-process build pays too.
|
||||
*
|
||||
* WIRE FORMAT. Fixed-width, native-endian, same-machine only. The two sides are
|
||||
* different libcs but the same CPU, so no marshalling beyond fixed-size structs.
|
||||
* Bump DA_RELAY_VERSION on any layout change; the helper refuses a mismatch.
|
||||
*/
|
||||
#ifndef DA_RELAY_PROTO_H
|
||||
#define DA_RELAY_PROTO_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define DA_RELAY_VERSION 1
|
||||
#define DA_RING_MAGIC 0x44415247u /* 'DARG' */
|
||||
#define DA_HELLO_MAGIC 0x44414831u /* 'DAH1' */
|
||||
#define DA_ACK_MAGIC 0x44414131u /* 'DAA1' */
|
||||
|
||||
/* Socket path. The driver reads BANNER_AUDIO_DIRECT_RELAY; when unset it falls
|
||||
* back to $XDG_RUNTIME_DIR/DA_RELAY_DEFAULT_NAME, which a host that already
|
||||
* hands its runtime dir into the session gets for free. */
|
||||
#define DA_RELAY_ENV "BANNER_AUDIO_DIRECT_RELAY"
|
||||
#define DA_RELAY_DEFAULT_NAME "directaudio-relay"
|
||||
|
||||
/* Both rings carry float32 interleaved stereo. The render ring is always
|
||||
* 48 kHz (the mixer's output rate); the capture ring runs at whatever rate the
|
||||
* input stream was granted, published in da_ring.rate, and the driver resamples
|
||||
* per voice as the in-process build already does. */
|
||||
#define DA_RING_RATE 48000
|
||||
#define DA_RING_CHANNELS 2
|
||||
|
||||
/* Render ring sizing. Capacity is the ceiling adaptive growth can reach; the
|
||||
* initial target is what the driver keeps queued. Two bursts: the helper's
|
||||
* callback consumes one burst and wakes the pump, which then has a full burst
|
||||
* period to refill before the next callback. */
|
||||
#define DA_RING_CAP_MS 250
|
||||
#define DA_RING_TARGET_BURSTS 2
|
||||
#define DA_RING_MAX_TARGET_MS 100
|
||||
|
||||
/* Capture ring: 200 ms is far more than a game ever leaves queued; on overrun
|
||||
* the helper drops the newest block, matching the in-process "guest is not
|
||||
* draining" behaviour of dropping data rather than blocking the mic thread. */
|
||||
#define DA_CAPTURE_RING_MS 200
|
||||
|
||||
enum da_ring_state
|
||||
{
|
||||
DA_RING_OPENING = 0,
|
||||
DA_RING_PLAYING = 1,
|
||||
DA_RING_ERROR = 2,
|
||||
};
|
||||
|
||||
struct da_ring
|
||||
{
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t cap_frames; /* ring capacity in frames */
|
||||
uint32_t rate; /* frames per second of the data in this ring */
|
||||
uint32_t channels; /* DA_RING_CHANNELS */
|
||||
uint32_t _pad0;
|
||||
|
||||
/* Single producer, single consumer; monotonic frame totals, wrap-safe with
|
||||
* unsigned arithmetic. Producer writes widx with RELEASE after the data;
|
||||
* consumer writes ridx with RELEASE after it has copied the data out. */
|
||||
volatile uint32_t widx;
|
||||
volatile uint32_t ridx;
|
||||
|
||||
/* Futex word the CONSUMER of a render ring / PRODUCER of a capture ring
|
||||
* bumps after each block, so the other side can sleep on it instead of
|
||||
* polling. Waiters always use a timeout, so a missed wake only costs one
|
||||
* period. */
|
||||
volatile uint32_t wake;
|
||||
|
||||
volatile uint32_t quit; /* either side: tear down */
|
||||
volatile uint32_t state; /* enum da_ring_state, helper-owned */
|
||||
|
||||
/* Render ring only, helper-owned, read by the driver: */
|
||||
volatile int32_t target_frames; /* keep at least this many queued */
|
||||
volatile int32_t hw_buf_frames; /* AAudio buffer size right now (get_latency) */
|
||||
volatile int32_t hw_burst; /* frames per burst of the output stream */
|
||||
volatile uint32_t underruns; /* callbacks that found fewer than they needed */
|
||||
volatile uint32_t cb_count; /* data callbacks so far (liveness) */
|
||||
uint32_t _pad1[3];
|
||||
|
||||
float data[]; /* cap_frames * channels */
|
||||
};
|
||||
|
||||
static inline size_t da_ring_bytes(uint32_t cap_frames)
|
||||
{
|
||||
return sizeof(struct da_ring) + (size_t)cap_frames * DA_RING_CHANNELS * sizeof(float);
|
||||
}
|
||||
|
||||
static inline uint32_t da_ring_avail(const struct da_ring *r)
|
||||
{
|
||||
uint32_t w = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE);
|
||||
uint32_t rd = __atomic_load_n(&r->ridx, __ATOMIC_ACQUIRE);
|
||||
return w - rd;
|
||||
}
|
||||
|
||||
static inline uint32_t da_ring_space(const struct da_ring *r)
|
||||
{
|
||||
return r->cap_frames - da_ring_avail(r);
|
||||
}
|
||||
|
||||
/* ---- wire messages ------------------------------------------------------- */
|
||||
|
||||
#define DA_HELLO_MIC 0x01u /* also open an INPUT stream + capture ring */
|
||||
#define DA_HELLO_EXCLUSIVE 0x02u /* AAUDIO_SHARING_MODE_EXCLUSIVE requested */
|
||||
#define DA_HELLO_ADAPTIVE 0x04u
|
||||
#define DA_HELLO_DECAY 0x08u
|
||||
#define DA_HELLO_WATCHDOG 0x10u
|
||||
|
||||
/* First message on a fresh connection, driver -> helper. Mirrors the launch
|
||||
* config the in-process build derives from BANNER_AUDIO_DIRECT_* so a preset
|
||||
* means the same thing under both runtimes. */
|
||||
struct da_hello
|
||||
{
|
||||
uint32_t magic; /* DA_HELLO_MAGIC */
|
||||
uint32_t version; /* DA_RELAY_VERSION */
|
||||
uint32_t flags; /* DA_HELLO_* */
|
||||
int32_t perf; /* 0 NONE, 1 LOW_LATENCY, 2 POWER_SAVING (host convention) */
|
||||
int32_t target_ms; /* AAudio buffer, ms; 0 = use target_frames or the default */
|
||||
int32_t max_ms; /* adaptive ceiling, ms; 0 = max_frames or the default */
|
||||
int32_t target_frames; /* frame forms, used only when the ms forms are 0 */
|
||||
int32_t max_frames;
|
||||
uint32_t stall_ms; /* watchdog: callback silent this long => rebuild */
|
||||
uint32_t quiet_ms; /* decay pacing, see the driver */
|
||||
uint32_t punish_ms;
|
||||
uint32_t max_backoff;
|
||||
int32_t pid; /* the game process, for the log */
|
||||
uint32_t log; /* BANNER_AUDIO_DIRECT_LOG */
|
||||
char runtime_path[256]; /* BANNER_AUDIO_DIRECT_RUNTIME mailbox, "" = none */
|
||||
char name[64]; /* process name, for the log */
|
||||
};
|
||||
|
||||
#define DA_ACK_MIC_FAILED 0x01u /* mic asked for, input stream would not open */
|
||||
|
||||
/* Reply, helper -> driver, followed by SCM_RIGHTS carrying the ring fds:
|
||||
* fd[0] = render ring, fd[1] = capture ring (present iff in_cap_frames > 0). */
|
||||
struct da_ack
|
||||
{
|
||||
uint32_t magic; /* DA_ACK_MAGIC */
|
||||
int32_t status; /* 0 = ok, else an AAudio error code (negative) */
|
||||
int32_t rate; /* output stream rate (48000) */
|
||||
int32_t burst; /* output frames per burst */
|
||||
int32_t buf_frames; /* output buffer size as opened */
|
||||
int32_t out_cap_frames; /* render ring capacity */
|
||||
int32_t in_cap_frames; /* capture ring capacity, 0 = no capture ring */
|
||||
int32_t in_rate; /* rate of the capture ring's data */
|
||||
uint32_t flags; /* DA_ACK_* */
|
||||
uint32_t _pad;
|
||||
};
|
||||
|
||||
/* Subsequent messages, driver -> helper, on the same socket. The helper never
|
||||
* writes after the ack; everything it has to say is in the ring header. EOF on
|
||||
* the socket is how the helper learns the game is gone. */
|
||||
enum da_msg_type
|
||||
{
|
||||
DA_MSG_MIC_START = 1, /* first capture voice started: make the mic hot */
|
||||
DA_MSG_MIC_STOP = 2, /* last capture voice gone: stop (keep open) */
|
||||
DA_MSG_PING = 3,
|
||||
};
|
||||
|
||||
struct da_msg
|
||||
{
|
||||
uint32_t type;
|
||||
int32_t arg;
|
||||
};
|
||||
|
||||
#endif /* DA_RELAY_PROTO_H */
|
||||
@@ -1,782 +0,0 @@
|
||||
/*
|
||||
* module-directaudio-native-sink: a PulseAudio 13 sink that is DirectAudio's AAudio engine,
|
||||
* in-process. For the native Linux Steam client on Android (DroidDeck, Bannerlator's Linux
|
||||
* session), whose sound can only ever reach a PulseAudio daemon: this module is where that
|
||||
* daemon's mixed output meets Android, one step away from AudioFlinger, with the same engine the
|
||||
* game driver has - adaptive device buffer that grows a burst on every xrun, optional decay with a
|
||||
* remembered floor, a primed ring with fades instead of clicks, route-change / error reopen on a
|
||||
* fresh stream, and a stall watchdog.
|
||||
*
|
||||
* It is the relay helper's per-client OUTPUT engine (directaudio-relay.c) with the socket and the
|
||||
* shared-memory ring replaced by a ring in this process, fed by the sink's IO thread exactly as
|
||||
* module-directaudio-sink feeds the relay. Compared with that module there is no second process
|
||||
* and no 40 ms proot safety margin: the ring here only has to cover the IO thread's own
|
||||
* scheduling jitter, so it starts at two bursts and grows only if a callback finds it short.
|
||||
*
|
||||
* The daemon must run on the Android side (the host app's process), not inside proot, for AAudio
|
||||
* to be reachable at all - which is where both host apps already run it.
|
||||
*
|
||||
* Field fixes carried from the relay (MaxsTechReview, Droid-Deck/DroidDeck#338): prime before
|
||||
* playing, fade on an underrun, one consumer per ring across a reopen, a device buffer of at
|
||||
* least two bursts.
|
||||
*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include <config.h>
|
||||
#endif
|
||||
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <aaudio/AAudio.h>
|
||||
|
||||
#include <pulse/rtclock.h>
|
||||
#include <pulse/timeval.h>
|
||||
#include <pulse/util.h>
|
||||
#include <pulse/xmalloc.h>
|
||||
|
||||
#include <pulsecore/core-util.h>
|
||||
#include <pulsecore/log.h>
|
||||
#include <pulsecore/macro.h>
|
||||
#include <pulsecore/modargs.h>
|
||||
#include <pulsecore/module.h>
|
||||
#include <pulsecore/rtpoll.h>
|
||||
#include <pulsecore/sink.h>
|
||||
#include <pulsecore/thread-mq.h>
|
||||
#include <pulsecore/thread.h>
|
||||
|
||||
#include "da_relay_proto.h" /* struct da_ring + the ring helpers; nothing of the socket protocol */
|
||||
|
||||
PA_MODULE_AUTHOR("The412Banner");
|
||||
PA_MODULE_DESCRIPTION("Android audio output: DirectAudio's AAudio engine, in-process");
|
||||
PA_MODULE_VERSION(PACKAGE_VERSION);
|
||||
PA_MODULE_LOAD_ONCE(false);
|
||||
PA_MODULE_USAGE(
|
||||
"sink_name=<name for the sink> "
|
||||
"sink_properties=<properties for the sink> "
|
||||
"volume=<initial volume, linear, 1.0 = full> "
|
||||
"performance_mode=<0 none, 1 low latency, 2 power saving> "
|
||||
"buffer_ms=<device buffer to start with, ms; rounded up to whole bursts, never below two> "
|
||||
"max_ms=<largest the device buffer may grow to, ms> "
|
||||
"ring_ms=<queue kept ahead of the device, ms; never below two bursts> "
|
||||
"adaptive=<grow the buffers after underruns: 0 or 1> "
|
||||
"decay=<shrink the device buffer again after a quiet spell: 0 or 1> "
|
||||
"limiter=<soft-knee limiter on the way out: 0 or 1> "
|
||||
"watchdog=<rebuild the stream when its callback stalls: 0 or 1>");
|
||||
|
||||
#define DEFAULT_SINK_NAME "DirectAudio"
|
||||
#define STATS_EVERY_USEC (30 * PA_USEC_PER_SEC)
|
||||
#define WATCHDOG_TICK_USEC (1 * PA_USEC_PER_SEC)
|
||||
#define AUDIO_NICE (-16)
|
||||
|
||||
/* ---- tunables: the in-process driver's, same defaults ----------------------- */
|
||||
#define DA_KNEE 0.75f
|
||||
#define DA_DEFAULT_MS 12
|
||||
#define DA_DEFAULT_MAX_MS 100
|
||||
#define DA_DEFAULT_RING_MS 12
|
||||
#define DA_RING_CAP_MS_LOCAL 250
|
||||
#define DA_FREEZE_GAP_NS 500000000ull
|
||||
#define DA_CB_STALL_NS 1000000000ull
|
||||
#define DA_DECAY_QUIET_NS 10000000000ull
|
||||
#define DA_DECAY_PUNISH_NS 5000000000ull
|
||||
#define DA_DECAY_MAX_BACKOFF 32
|
||||
#define DA_MIN_BUFFER_BURSTS 2
|
||||
#define DA_FADE_FRAMES 64
|
||||
|
||||
static const char* const valid_modargs[] = {
|
||||
"sink_name", "sink_properties", "volume", "performance_mode", "buffer_ms", "max_ms", "ring_ms",
|
||||
"adaptive", "decay", "limiter", "watchdog", NULL
|
||||
};
|
||||
|
||||
struct userdata {
|
||||
pa_core *core;
|
||||
pa_module *module;
|
||||
pa_sink *sink;
|
||||
|
||||
pa_thread *thread;
|
||||
pa_thread_mq thread_mq;
|
||||
pa_rtpoll *rtpoll;
|
||||
|
||||
pa_sample_spec ss;
|
||||
size_t frame_size;
|
||||
|
||||
/* configuration */
|
||||
aaudio_performance_mode_t perf;
|
||||
int32_t target_ms, max_ms, ring_ms;
|
||||
bool adaptive, decay, limiter, watchdog_on;
|
||||
|
||||
/* the ring: this process's, same layout as the relay's so the engine is shared */
|
||||
struct da_ring *ring;
|
||||
int32_t burst;
|
||||
int32_t ring_max_target;
|
||||
|
||||
/* the one OUTPUT stream and its adaptive state (the relay's struct client, minus the socket) */
|
||||
AAudioStream *aq;
|
||||
pthread_mutex_t lock;
|
||||
int32_t last_xrun;
|
||||
uint64_t last_cb_ns, last_xrun_ns, last_decay_ns, wd_last_tick_ns;
|
||||
int32_t base_buf_frames, decay_floor;
|
||||
unsigned int decay_backoff, cb_count, wd_last_cb;
|
||||
int reopen_running, reopen_redo;
|
||||
int consuming, primed, starved;
|
||||
volatile int dead;
|
||||
|
||||
/* what the IO thread last logged, so engine changes made on the AAudio thread get a line */
|
||||
int32_t seen_target, seen_hw_buf;
|
||||
uint32_t seen_underruns;
|
||||
pa_usec_t next_watchdog, settled_at, stats_at;
|
||||
unsigned int opens;
|
||||
};
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t) ts.tv_sec * 1000000000ull + (uint64_t) ts.tv_nsec;
|
||||
}
|
||||
|
||||
static void request_reopen(struct userdata *u, const char *why);
|
||||
|
||||
/* ---- the engine: the relay's out_cb / open_output / reopen / watchdog, on a local ring ---- */
|
||||
|
||||
static void fade(float *p, uint32_t frames, int in) {
|
||||
uint32_t i, ch;
|
||||
|
||||
for (i = 0; i < frames; i++) {
|
||||
float g = (float) (in ? i + 1 : frames - i) / (float) (frames + 1);
|
||||
|
||||
for (ch = 0; ch < DA_RING_CHANNELS; ch++)
|
||||
p[(size_t) i * DA_RING_CHANNELS + ch] *= g;
|
||||
}
|
||||
}
|
||||
|
||||
/* The driver's soft-knee limiter: below DA_KNEE the signal is untouched, so ordinary material is
|
||||
* bit-identical; above it a smooth knee folds the overshoot back under full scale. */
|
||||
static void limit(float *p, uint32_t samples) {
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0; i < samples; i++) {
|
||||
float x = p[i], a = x < 0.0f ? -x : x;
|
||||
|
||||
if (a > DA_KNEE) {
|
||||
float t = (a - DA_KNEE) / (1.0f - DA_KNEE);
|
||||
float y = DA_KNEE + (1.0f - DA_KNEE) * (t / (1.0f + t));
|
||||
|
||||
p[i] = x < 0.0f ? -y : y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static aaudio_data_callback_result_t out_cb(AAudioStream *aq, void *user, void *audioData, int32_t numFrames) {
|
||||
struct userdata *u = user;
|
||||
struct da_ring *r = u->ring;
|
||||
float *out = audioData;
|
||||
AAudioStream *live = __atomic_load_n(&u->aq, __ATOMIC_SEQ_CST);
|
||||
uint32_t cap = r->cap_frames;
|
||||
uint32_t frames = (uint32_t) numFrames;
|
||||
uint32_t ridx, avail, n = 0, i;
|
||||
int fade_in = 0, short_read = 0, starving = 0;
|
||||
uint64_t now;
|
||||
|
||||
/* Only the promoted stream reads the ring; an outgoing one during a reopen plays silence. */
|
||||
if ((live && aq != live) || __atomic_exchange_n(&u->consuming, 1, __ATOMIC_ACQUIRE)) {
|
||||
memset(out, 0, (size_t) frames * DA_RING_CHANNELS * sizeof(float));
|
||||
return AAUDIO_CALLBACK_RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
ridx = __atomic_load_n(&r->ridx, __ATOMIC_ACQUIRE);
|
||||
avail = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE) - ridx;
|
||||
if (!u->primed) {
|
||||
uint32_t start = (uint32_t) __atomic_load_n(&r->target_frames, __ATOMIC_ACQUIRE);
|
||||
|
||||
if (start < frames) start = frames;
|
||||
if (start > cap) start = cap;
|
||||
u->primed = fade_in = avail >= start;
|
||||
if (u->primed) u->starved = 0;
|
||||
else starving = u->starved;
|
||||
}
|
||||
if (u->primed) {
|
||||
uint32_t slot = ridx % cap, first;
|
||||
|
||||
n = frames < avail ? frames : avail;
|
||||
first = cap - slot < n ? cap - slot : n;
|
||||
memcpy(out, &r->data[(size_t) slot * DA_RING_CHANNELS], (size_t) first * DA_RING_CHANNELS * sizeof(float));
|
||||
if (n > first)
|
||||
memcpy(out + (size_t) first * DA_RING_CHANNELS, r->data, (size_t) (n - first) * DA_RING_CHANNELS * sizeof(float));
|
||||
if (u->limiter)
|
||||
limit(out, n * DA_RING_CHANNELS);
|
||||
if (fade_in)
|
||||
fade(out, n < DA_FADE_FRAMES ? n : DA_FADE_FRAMES, 1);
|
||||
if (n < frames) {
|
||||
uint32_t tail = n < DA_FADE_FRAMES ? n : DA_FADE_FRAMES;
|
||||
|
||||
fade(out + (size_t) (n - tail) * DA_RING_CHANNELS, tail, 0);
|
||||
u->primed = 0;
|
||||
u->starved = 1;
|
||||
short_read = 1;
|
||||
}
|
||||
__atomic_store_n(&r->ridx, ridx + n, __ATOMIC_RELEASE);
|
||||
}
|
||||
for (i = n * DA_RING_CHANNELS; i < frames * DA_RING_CHANNELS; i++)
|
||||
out[i] = 0.0f;
|
||||
__atomic_store_n(&u->consuming, 0, __ATOMIC_RELEASE);
|
||||
|
||||
now = now_ns();
|
||||
{
|
||||
uint64_t prev = __atomic_exchange_n(&u->last_cb_ns, now, __ATOMIC_SEQ_CST);
|
||||
|
||||
if (prev && now - prev > DA_FREEZE_GAP_NS) {
|
||||
u->last_xrun = AAudioStream_getXRunCount(aq);
|
||||
u->last_xrun_ns = now;
|
||||
}
|
||||
}
|
||||
__atomic_add_fetch(&u->cb_count, 1, __ATOMIC_SEQ_CST);
|
||||
__atomic_store_n(&r->cb_count, u->cb_count, __ATOMIC_RELAXED);
|
||||
|
||||
/* The ring's own adaptive step: a short ring means the IO thread did not get scheduled in
|
||||
* time - an xrun one stage earlier - so keep one more burst queued. Only grows. */
|
||||
if (short_read)
|
||||
__atomic_add_fetch(&r->underruns, 1, __ATOMIC_RELAXED);
|
||||
if ((short_read || starving) && u->adaptive) {
|
||||
int32_t burst = AAudioStream_getFramesPerBurst(aq);
|
||||
int32_t cur = __atomic_load_n(&r->target_frames, __ATOMIC_RELAXED);
|
||||
int32_t want = cur + (burst > 0 ? burst : numFrames);
|
||||
|
||||
if (want > u->ring_max_target) want = u->ring_max_target;
|
||||
if (want > cur)
|
||||
__atomic_store_n(&r->target_frames, want, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
/* The device buffer: grow a burst per xrun; decay a burst after a quiet spell, never below
|
||||
* the floor a punished decay taught us. */
|
||||
if (u->adaptive) {
|
||||
int32_t xruns = AAudioStream_getXRunCount(aq);
|
||||
int32_t burst = AAudioStream_getFramesPerBurst(aq);
|
||||
int32_t cur = AAudioStream_getBufferSizeInFrames(aq);
|
||||
|
||||
if (xruns > u->last_xrun) {
|
||||
int32_t capf = AAudioStream_getBufferCapacityInFrames(aq);
|
||||
int32_t want = cur + (burst > 0 ? burst : 1);
|
||||
int32_t max_frames = u->max_ms > 0 ? u->max_ms * DA_RING_RATE / 1000 : 0;
|
||||
|
||||
if (max_frames > 0 && want > max_frames) want = max_frames;
|
||||
if (want > capf) want = capf;
|
||||
if (want > cur) AAudioStream_setBufferSizeInFrames(aq, want);
|
||||
|
||||
if (u->decay && u->last_decay_ns && now - u->last_decay_ns < DA_DECAY_PUNISH_NS) {
|
||||
if (want > u->decay_floor) u->decay_floor = want;
|
||||
if (u->decay_backoff < DA_DECAY_MAX_BACKOFF) u->decay_backoff *= 2;
|
||||
}
|
||||
u->last_xrun = xruns;
|
||||
u->last_xrun_ns = now;
|
||||
__atomic_store_n(&r->hw_buf_frames, AAudioStream_getBufferSizeInFrames(aq), __ATOMIC_RELAXED);
|
||||
} else if (u->decay && burst > 0) {
|
||||
uint64_t quiet = DA_DECAY_QUIET_NS * u->decay_backoff;
|
||||
int32_t floor = u->base_buf_frames;
|
||||
|
||||
if (u->decay_floor > floor) floor = u->decay_floor;
|
||||
if (cur > floor && u->last_xrun_ns && now - u->last_xrun_ns > quiet &&
|
||||
(!u->last_decay_ns || now - u->last_decay_ns > quiet)) {
|
||||
int32_t want = cur - burst;
|
||||
|
||||
if (want < floor) want = floor;
|
||||
if (want < cur) {
|
||||
AAudioStream_setBufferSizeInFrames(aq, want);
|
||||
u->last_decay_ns = now;
|
||||
__atomic_store_n(&r->hw_buf_frames, AAudioStream_getBufferSizeInFrames(aq), __ATOMIC_RELAXED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AAUDIO_CALLBACK_RESULT_CONTINUE;
|
||||
}
|
||||
|
||||
static void out_error_cb(AAudioStream *aq, void *user, aaudio_result_t error) {
|
||||
struct userdata *u = user;
|
||||
|
||||
if (aq != __atomic_load_n(&u->aq, __ATOMIC_SEQ_CST)) return;
|
||||
/* A disconnected stream (headphones plugged or unplugged, a Bluetooth route coming or going)
|
||||
* can never be restarted: it has to be closed and a new one opened on the current route. */
|
||||
if (error != AAUDIO_ERROR_DISCONNECTED &&
|
||||
error != AAUDIO_ERROR_INVALID_STATE &&
|
||||
error != AAUDIO_ERROR_INVALID_HANDLE &&
|
||||
error != AAUDIO_ERROR_TIMEOUT)
|
||||
return;
|
||||
request_reopen(u, "stream error");
|
||||
}
|
||||
|
||||
/* 48 kHz / float / stereo, buffer from the ms form rounded up to a burst and never below two,
|
||||
* capacity from the ceiling. The ring target floor is two bursts or ring_ms, whichever is more. */
|
||||
static aaudio_result_t open_output(struct userdata *u, AAudioStream **out) {
|
||||
AAudioStreamBuilder *builder = NULL;
|
||||
AAudioStream *aq = NULL;
|
||||
aaudio_result_t res;
|
||||
int32_t capf, burst, want;
|
||||
|
||||
res = AAudio_createStreamBuilder(&builder);
|
||||
if (res != AAUDIO_OK || !builder) return res != AAUDIO_OK ? res : AAUDIO_ERROR_NO_MEMORY;
|
||||
|
||||
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
|
||||
AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_SHARED);
|
||||
AAudioStreamBuilder_setPerformanceMode(builder, u->perf);
|
||||
AAudioStreamBuilder_setUsage(builder, AAUDIO_USAGE_GAME);
|
||||
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT);
|
||||
AAudioStreamBuilder_setChannelCount(builder, DA_RING_CHANNELS);
|
||||
AAudioStreamBuilder_setSampleRate(builder, DA_RING_RATE);
|
||||
AAudioStreamBuilder_setDataCallback(builder, out_cb, u);
|
||||
AAudioStreamBuilder_setErrorCallback(builder, out_error_cb, u);
|
||||
AAudioStreamBuilder_setBufferCapacityInFrames(builder, (u->max_ms > 0 ? u->max_ms : DA_DEFAULT_MAX_MS) * DA_RING_RATE / 1000);
|
||||
|
||||
res = AAudioStreamBuilder_openStream(builder, &aq);
|
||||
AAudioStreamBuilder_delete(builder);
|
||||
if (res != AAUDIO_OK || !aq) return res != AAUDIO_OK ? res : AAUDIO_ERROR_INTERNAL;
|
||||
|
||||
capf = AAudioStream_getBufferCapacityInFrames(aq);
|
||||
burst = AAudioStream_getFramesPerBurst(aq);
|
||||
want = (u->target_ms > 0 ? u->target_ms : DA_DEFAULT_MS) * DA_RING_RATE / 1000;
|
||||
if (burst > 0) {
|
||||
int32_t nb = (want + burst - 1) / burst;
|
||||
want = (nb < 1 ? 1 : nb) * burst;
|
||||
if (want < DA_MIN_BUFFER_BURSTS * burst) want = DA_MIN_BUFFER_BURSTS * burst;
|
||||
}
|
||||
if (want > capf) want = capf;
|
||||
if (want > 0) AAudioStream_setBufferSizeInFrames(aq, want);
|
||||
|
||||
if (burst <= 0) burst = 192;
|
||||
u->burst = burst;
|
||||
u->ring_max_target = DA_RING_MAX_TARGET_MS * DA_RING_RATE / 1000;
|
||||
if (u->ring_max_target > (int32_t) u->ring->cap_frames) u->ring_max_target = (int32_t) u->ring->cap_frames;
|
||||
{
|
||||
int32_t floor = DA_RING_TARGET_BURSTS * burst;
|
||||
int32_t asked = (u->ring_ms > 0 ? u->ring_ms : DA_DEFAULT_RING_MS) * DA_RING_RATE / 1000;
|
||||
|
||||
if (floor < asked) floor = asked;
|
||||
if (floor > u->ring_max_target) floor = u->ring_max_target;
|
||||
if (__atomic_load_n(&u->ring->target_frames, __ATOMIC_ACQUIRE) < floor)
|
||||
__atomic_store_n(&u->ring->target_frames, floor, __ATOMIC_RELEASE);
|
||||
}
|
||||
__atomic_store_n(&u->ring->hw_burst, burst, __ATOMIC_RELAXED);
|
||||
|
||||
u->last_xrun = AAudioStream_getXRunCount(aq);
|
||||
u->base_buf_frames = AAudioStream_getBufferSizeInFrames(aq);
|
||||
u->decay_floor = 0;
|
||||
u->decay_backoff = 1;
|
||||
u->last_xrun_ns = now_ns();
|
||||
u->last_decay_ns = 0;
|
||||
__atomic_store_n(&u->ring->hw_buf_frames, u->base_buf_frames, __ATOMIC_RELAXED);
|
||||
|
||||
pa_log("directaudio-native-sink: open #%u: device buffer %d frames (%d ms), burst %d, capacity %d, performance mode %d (asked %d), ring target %d frames - Android adds its own output latency on top",
|
||||
++u->opens, u->base_buf_frames, u->base_buf_frames * 1000 / DA_RING_RATE, AAudioStream_getFramesPerBurst(aq),
|
||||
capf, AAudioStream_getPerformanceMode(aq), u->perf, (int) u->ring->target_frames);
|
||||
|
||||
res = AAudioStream_requestStart(aq);
|
||||
if (res != AAUDIO_OK) { AAudioStream_close(aq); return res; }
|
||||
|
||||
__atomic_store_n(&u->last_cb_ns, now_ns(), __ATOMIC_SEQ_CST);
|
||||
u->wd_last_tick_ns = 0;
|
||||
__atomic_store_n(&u->ring->state, DA_RING_PLAYING, __ATOMIC_RELEASE);
|
||||
*out = aq;
|
||||
return AAUDIO_OK;
|
||||
}
|
||||
|
||||
static void *reopen_thread(void *user) {
|
||||
struct userdata *u = user;
|
||||
|
||||
for (;;) {
|
||||
AAudioStream *old = NULL, *neu = NULL;
|
||||
int expected = 0;
|
||||
|
||||
__atomic_store_n(&u->reopen_redo, 0, __ATOMIC_SEQ_CST);
|
||||
if (u->dead) break;
|
||||
|
||||
if (open_output(u, &neu) == AAUDIO_OK) {
|
||||
pthread_mutex_lock(&u->lock);
|
||||
old = u->aq;
|
||||
__atomic_store_n(&u->aq, neu, __ATOMIC_SEQ_CST);
|
||||
pthread_mutex_unlock(&u->lock);
|
||||
} else
|
||||
pa_log("directaudio-native-sink: reopen failed; keeping the old stream");
|
||||
|
||||
if (old) {
|
||||
AAudioStream_requestStop(old);
|
||||
AAudioStream_close(old);
|
||||
pa_log("directaudio-native-sink: reopened the output on the current route");
|
||||
}
|
||||
|
||||
if (__atomic_load_n(&u->reopen_redo, __ATOMIC_SEQ_CST)) { usleep(20000); continue; }
|
||||
__atomic_store_n(&u->reopen_running, 0, __ATOMIC_SEQ_CST);
|
||||
if (!__atomic_load_n(&u->reopen_redo, __ATOMIC_SEQ_CST)) break;
|
||||
if (!__atomic_compare_exchange_n(&u->reopen_running, &expected, 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST))
|
||||
break;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void request_reopen(struct userdata *u, const char *why) {
|
||||
int expected = 0;
|
||||
|
||||
__atomic_store_n(&u->reopen_redo, 1, __ATOMIC_SEQ_CST);
|
||||
if (__atomic_compare_exchange_n(&u->reopen_running, &expected, 1, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST)) {
|
||||
pthread_t th;
|
||||
|
||||
pa_log("directaudio-native-sink: reopen: %s", why);
|
||||
if (pthread_create(&th, NULL, reopen_thread, u))
|
||||
__atomic_store_n(&u->reopen_running, 0, __ATOMIC_SEQ_CST);
|
||||
else
|
||||
pthread_detach(th);
|
||||
}
|
||||
}
|
||||
|
||||
/* Ticked once a second from the IO thread: a callback silent for a second is a dead stream. */
|
||||
static void watchdog(struct userdata *u) {
|
||||
uint64_t now = now_ns(), last_cb, prev_tick;
|
||||
unsigned int cb;
|
||||
|
||||
if (!u->watchdog_on) return;
|
||||
if (!__atomic_load_n(&u->aq, __ATOMIC_SEQ_CST)) return;
|
||||
|
||||
prev_tick = u->wd_last_tick_ns;
|
||||
u->wd_last_tick_ns = now;
|
||||
cb = __atomic_load_n(&u->cb_count, __ATOMIC_SEQ_CST);
|
||||
|
||||
if (!prev_tick || now - prev_tick > 2 * DA_FREEZE_GAP_NS + 1000000000ull) {
|
||||
u->wd_last_cb = cb;
|
||||
__atomic_store_n(&u->last_cb_ns, now, __ATOMIC_SEQ_CST);
|
||||
return;
|
||||
}
|
||||
if (cb != u->wd_last_cb) { u->wd_last_cb = cb; return; }
|
||||
|
||||
last_cb = __atomic_load_n(&u->last_cb_ns, __ATOMIC_SEQ_CST);
|
||||
if (!last_cb || now - last_cb < DA_CB_STALL_NS) return;
|
||||
|
||||
__atomic_store_n(&u->last_cb_ns, now, __ATOMIC_SEQ_CST);
|
||||
request_reopen(u, "data callback stalled");
|
||||
}
|
||||
|
||||
static void close_output(struct userdata *u) {
|
||||
AAudioStream *aq;
|
||||
|
||||
u->dead = 1;
|
||||
pthread_mutex_lock(&u->lock);
|
||||
aq = u->aq;
|
||||
__atomic_store_n(&u->aq, NULL, __ATOMIC_SEQ_CST);
|
||||
pthread_mutex_unlock(&u->lock);
|
||||
if (aq) {
|
||||
AAudioStream_requestStop(aq);
|
||||
AAudioStream_close(aq);
|
||||
}
|
||||
/* A reopen still in flight sees u->dead and stops; give it a moment to let go of u. */
|
||||
while (__atomic_load_n(&u->reopen_running, __ATOMIC_SEQ_CST))
|
||||
usleep(10000);
|
||||
}
|
||||
|
||||
/* ---- the PulseAudio side: module-directaudio-sink's producer, on the local ring ---- */
|
||||
|
||||
static pa_usec_t frames_to_usec(const struct userdata *u, int64_t frames) {
|
||||
return (pa_usec_t) (frames * (int64_t) PA_USEC_PER_SEC / (int64_t) u->ss.rate);
|
||||
}
|
||||
|
||||
static pa_usec_t sink_latency(struct userdata *u) {
|
||||
return frames_to_usec(u, (int64_t) da_ring_avail(u->ring) + (u->ring->hw_buf_frames > 0 ? u->ring->hw_buf_frames : 0));
|
||||
}
|
||||
|
||||
static void produce(struct userdata *u, uint32_t frames) {
|
||||
struct da_ring *r = u->ring;
|
||||
uint32_t w = __atomic_load_n(&r->widx, __ATOMIC_ACQUIRE);
|
||||
uint32_t pos = w % r->cap_frames;
|
||||
uint32_t first = PA_MIN(frames, r->cap_frames - pos);
|
||||
float *dst = r->data + (size_t) pos * DA_RING_CHANNELS;
|
||||
size_t bytes = (size_t) frames * u->frame_size;
|
||||
|
||||
if (PA_SINK_IS_OPENED(u->sink->thread_info.state)) {
|
||||
pa_memchunk chunk;
|
||||
const uint8_t *src;
|
||||
|
||||
if (u->sink->thread_info.rewind_requested)
|
||||
pa_sink_process_rewind(u->sink, 0);
|
||||
pa_sink_render_full(u->sink, bytes, &chunk);
|
||||
src = pa_memblock_acquire_chunk(&chunk);
|
||||
memcpy(dst, src, (size_t) first * u->frame_size);
|
||||
if (frames > first)
|
||||
memcpy(r->data, src + (size_t) first * u->frame_size, (size_t) (frames - first) * u->frame_size);
|
||||
pa_memblock_release(chunk.memblock);
|
||||
pa_memblock_unref(chunk.memblock);
|
||||
} else {
|
||||
memset(dst, 0, (size_t) first * u->frame_size);
|
||||
if (frames > first)
|
||||
memset(r->data, 0, (size_t) (frames - first) * u->frame_size);
|
||||
}
|
||||
__atomic_store_n(&r->widx, w + frames, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
/* Tops the ring up to the engine's target and narrates what the engine changed. */
|
||||
static void service_ring(struct userdata *u) {
|
||||
struct da_ring *r = u->ring;
|
||||
uint32_t avail, space;
|
||||
int32_t target = __atomic_load_n(&r->target_frames, __ATOMIC_ACQUIRE);
|
||||
int32_t hw = __atomic_load_n(&r->hw_buf_frames, __ATOMIC_RELAXED);
|
||||
uint32_t under = __atomic_load_n(&r->underruns, __ATOMIC_RELAXED);
|
||||
pa_usec_t now = pa_rtclock_now();
|
||||
|
||||
if (target <= 0) target = 2 * u->burst;
|
||||
avail = da_ring_avail(r);
|
||||
space = da_ring_space(r);
|
||||
if (avail < (uint32_t) target) {
|
||||
uint32_t want = PA_MIN((uint32_t) target - avail, space);
|
||||
|
||||
if (want > 0)
|
||||
produce(u, want);
|
||||
}
|
||||
if (target != u->seen_target || hw != u->seen_hw_buf) {
|
||||
pa_sink_set_max_request_within_thread(u->sink, (size_t) target * u->frame_size);
|
||||
pa_sink_set_fixed_latency_within_thread(u->sink, frames_to_usec(u, (int64_t) target + (hw > 0 ? hw : 0)));
|
||||
}
|
||||
if (target != u->seen_target) {
|
||||
if (u->seen_target)
|
||||
pa_log("directaudio-native-sink: ring grow: target %d -> %d frames (%d ms)", u->seen_target, target, target * 1000 / DA_RING_RATE);
|
||||
u->seen_target = target;
|
||||
}
|
||||
if (hw != u->seen_hw_buf) {
|
||||
if (u->seen_hw_buf)
|
||||
pa_log("directaudio-native-sink: device buffer %d -> %d frames (%d ms)", u->seen_hw_buf, hw, hw * 1000 / DA_RING_RATE);
|
||||
u->seen_hw_buf = hw;
|
||||
}
|
||||
if (u->settled_at && now >= u->settled_at) {
|
||||
pa_log("directaudio-native-sink: after 10 s: device buffer %d frames, ring target %d, ring underruns %u, stream xruns %d",
|
||||
hw, target, under, u->aq ? AAudioStream_getXRunCount(u->aq) : -1);
|
||||
u->settled_at = 0;
|
||||
}
|
||||
if (under != u->seen_underruns && now - u->stats_at >= STATS_EVERY_USEC) {
|
||||
pa_log("directaudio-native-sink: %u ring underrun(s) so far, target %d frames, device buffer %d", under, target, hw);
|
||||
u->seen_underruns = under;
|
||||
u->stats_at = now;
|
||||
}
|
||||
if (now >= u->next_watchdog) {
|
||||
watchdog(u);
|
||||
u->next_watchdog = now + WATCHDOG_TICK_USEC;
|
||||
}
|
||||
}
|
||||
|
||||
static int sink_process_msg(pa_msgobject *o, int code, void *data, int64_t offset, pa_memchunk *chunk) {
|
||||
struct userdata *u = PA_SINK(o)->userdata;
|
||||
|
||||
switch (code) {
|
||||
case PA_SINK_MESSAGE_GET_LATENCY:
|
||||
*((pa_usec_t*) data) = sink_latency(u);
|
||||
return 0;
|
||||
}
|
||||
return pa_sink_process_msg(o, code, data, offset, chunk);
|
||||
}
|
||||
|
||||
static void thread_func(void *userdata) {
|
||||
struct userdata *u = userdata;
|
||||
|
||||
pa_log_debug("IO thread starting");
|
||||
pa_thread_mq_install(&u->thread_mq);
|
||||
if (setpriority(PRIO_PROCESS, (id_t) syscall(SYS_gettid), AUDIO_NICE) < 0)
|
||||
pa_log_debug("directaudio-native-sink: could not raise the IO thread's priority (%s)", strerror(errno));
|
||||
if (u->core->realtime_scheduling)
|
||||
pa_thread_make_realtime(u->core->realtime_priority);
|
||||
pa_rtpoll_set_timer_relative(u->rtpoll, 0);
|
||||
|
||||
for (;;) {
|
||||
int ret;
|
||||
|
||||
/* Half a burst between visits, never blocking on the stream: clients' audio reaches this
|
||||
* thread as messages pa_rtpoll_run handles, and a thread stuck in a write or a wait lets
|
||||
* them run dry (the crackle #338 fixed). */
|
||||
service_ring(u);
|
||||
pa_rtpoll_set_timer_relative(u->rtpoll, PA_MAX(frames_to_usec(u, u->burst) / 2, PA_USEC_PER_MSEC));
|
||||
|
||||
if ((ret = pa_rtpoll_run(u->rtpoll)) < 0)
|
||||
goto fail;
|
||||
if (ret == 0)
|
||||
goto finish;
|
||||
}
|
||||
|
||||
fail:
|
||||
pa_asyncmsgq_post(u->thread_mq.outq, PA_MSGOBJECT(u->core), PA_CORE_MESSAGE_UNLOAD_MODULE, u->module, 0, NULL, NULL);
|
||||
pa_asyncmsgq_wait_for(u->thread_mq.inq, PA_MESSAGE_SHUTDOWN);
|
||||
|
||||
finish:
|
||||
pa_log_debug("IO thread shutting down");
|
||||
}
|
||||
|
||||
void pa__done(pa_module *m);
|
||||
|
||||
int pa__init(pa_module *m) {
|
||||
struct userdata *u = NULL;
|
||||
pa_modargs *ma = NULL;
|
||||
pa_sink_new_data data;
|
||||
pa_channel_map map;
|
||||
pa_cvolume volume;
|
||||
uint32_t perf = 1, buffer_ms = DA_DEFAULT_MS, max_ms = DA_DEFAULT_MAX_MS, ring_ms = DA_DEFAULT_RING_MS;
|
||||
double linear_volume = 1.0;
|
||||
bool adaptive = true, decay = false, limiter = true, watchdog_on = true;
|
||||
AAudioStream *aq = NULL;
|
||||
aaudio_result_t res;
|
||||
uint32_t cap_frames;
|
||||
|
||||
pa_assert(m);
|
||||
|
||||
if (!(ma = pa_modargs_new(m->argument, valid_modargs))) {
|
||||
pa_log("Failed to parse module arguments.");
|
||||
goto fail;
|
||||
}
|
||||
if (pa_modargs_get_value_u32(ma, "performance_mode", &perf) < 0 || perf > 2) {
|
||||
pa_log("performance_mode must be 0, 1 or 2");
|
||||
goto fail;
|
||||
}
|
||||
if (pa_modargs_get_value_u32(ma, "buffer_ms", &buffer_ms) < 0 || buffer_ms == 0 || buffer_ms > 200) {
|
||||
pa_log("buffer_ms must be between 1 and 200");
|
||||
goto fail;
|
||||
}
|
||||
if (pa_modargs_get_value_u32(ma, "max_ms", &max_ms) < 0 || max_ms < buffer_ms || max_ms > 250) {
|
||||
pa_log("max_ms must be between buffer_ms and 250");
|
||||
goto fail;
|
||||
}
|
||||
if (pa_modargs_get_value_u32(ma, "ring_ms", &ring_ms) < 0 || ring_ms == 0 || ring_ms > 100) {
|
||||
pa_log("ring_ms must be between 1 and 100");
|
||||
goto fail;
|
||||
}
|
||||
if (pa_modargs_get_value_boolean(ma, "adaptive", &adaptive) < 0 ||
|
||||
pa_modargs_get_value_boolean(ma, "decay", &decay) < 0 ||
|
||||
pa_modargs_get_value_boolean(ma, "limiter", &limiter) < 0 ||
|
||||
pa_modargs_get_value_boolean(ma, "watchdog", &watchdog_on) < 0) {
|
||||
pa_log("adaptive, decay, limiter and watchdog must be 0 or 1");
|
||||
goto fail;
|
||||
}
|
||||
if (pa_modargs_get_value_double(ma, "volume", &linear_volume) < 0 || linear_volume < 0.0 || linear_volume > 10.0) {
|
||||
pa_log("volume must be a linear factor between 0 and 10");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
m->userdata = u = pa_xnew0(struct userdata, 1);
|
||||
u->core = m->core;
|
||||
u->module = m;
|
||||
u->perf = perf == 0 ? AAUDIO_PERFORMANCE_MODE_NONE : perf == 2 ? AAUDIO_PERFORMANCE_MODE_POWER_SAVING : AAUDIO_PERFORMANCE_MODE_LOW_LATENCY;
|
||||
u->target_ms = (int32_t) buffer_ms;
|
||||
u->max_ms = (int32_t) max_ms;
|
||||
u->ring_ms = (int32_t) ring_ms;
|
||||
u->adaptive = adaptive;
|
||||
u->decay = decay;
|
||||
u->limiter = limiter;
|
||||
u->watchdog_on = watchdog_on;
|
||||
u->ss.format = PA_SAMPLE_FLOAT32NE;
|
||||
u->ss.rate = DA_RING_RATE;
|
||||
u->ss.channels = DA_RING_CHANNELS;
|
||||
u->frame_size = pa_frame_size(&u->ss);
|
||||
u->burst = 192;
|
||||
pthread_mutex_init(&u->lock, NULL);
|
||||
pa_channel_map_init_stereo(&map);
|
||||
|
||||
/* The ring: the relay's layout in this process's memory. */
|
||||
cap_frames = DA_RING_CAP_MS_LOCAL * DA_RING_RATE / 1000;
|
||||
u->ring = pa_xmalloc0(da_ring_bytes(cap_frames));
|
||||
u->ring->magic = DA_RING_MAGIC;
|
||||
u->ring->version = DA_RELAY_VERSION;
|
||||
u->ring->cap_frames = cap_frames;
|
||||
u->ring->rate = DA_RING_RATE;
|
||||
u->ring->channels = DA_RING_CHANNELS;
|
||||
u->ring->target_frames = 0;
|
||||
|
||||
if ((res = open_output(u, &aq)) != AAUDIO_OK) {
|
||||
pa_log("directaudio-native-sink: could not open the AAudio output (%s); is the daemon running on the Android side?", AAudio_convertResultToText(res));
|
||||
goto fail;
|
||||
}
|
||||
__atomic_store_n(&u->aq, aq, __ATOMIC_SEQ_CST);
|
||||
u->seen_hw_buf = u->base_buf_frames;
|
||||
u->seen_target = u->ring->target_frames;
|
||||
u->settled_at = pa_rtclock_now() + 10 * PA_USEC_PER_SEC;
|
||||
u->stats_at = pa_rtclock_now();
|
||||
|
||||
u->rtpoll = pa_rtpoll_new();
|
||||
if (pa_thread_mq_init(&u->thread_mq, m->core->mainloop, u->rtpoll) < 0) {
|
||||
pa_log("pa_thread_mq_init() failed.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
pa_sink_new_data_init(&data);
|
||||
data.driver = __FILE__;
|
||||
data.module = m;
|
||||
pa_sink_new_data_set_name(&data, pa_modargs_get_value(ma, "sink_name", DEFAULT_SINK_NAME));
|
||||
pa_sink_new_data_set_sample_spec(&data, &u->ss);
|
||||
pa_sink_new_data_set_channel_map(&data, &map);
|
||||
pa_cvolume_set(&volume, u->ss.channels, pa_sw_volume_from_linear(linear_volume));
|
||||
pa_sink_new_data_set_volume(&data, &volume);
|
||||
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_STRING, "directaudio");
|
||||
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_DESCRIPTION, "Android audio output (DirectAudio)");
|
||||
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_CLASS, "sound");
|
||||
pa_proplist_sets(data.proplist, PA_PROP_DEVICE_API, "directaudio");
|
||||
if (pa_modargs_get_proplist(ma, "sink_properties", data.proplist, PA_UPDATE_REPLACE) < 0) {
|
||||
pa_log("Invalid properties");
|
||||
pa_sink_new_data_done(&data);
|
||||
goto fail;
|
||||
}
|
||||
u->sink = pa_sink_new(m->core, &data, PA_SINK_HARDWARE | PA_SINK_LATENCY);
|
||||
pa_sink_new_data_done(&data);
|
||||
if (!u->sink) {
|
||||
pa_log("Failed to create sink.");
|
||||
goto fail;
|
||||
}
|
||||
u->sink->parent.process_msg = sink_process_msg;
|
||||
u->sink->userdata = u;
|
||||
pa_sink_set_asyncmsgq(u->sink, u->thread_mq.inq);
|
||||
pa_sink_set_rtpoll(u->sink, u->rtpoll);
|
||||
pa_sink_set_max_request(u->sink, (size_t) u->ring->target_frames * u->frame_size);
|
||||
/* What clients should keep queued on their side: the ring's starting target plus the device
|
||||
* buffer. Grows are reported live through GET_LATENCY. */
|
||||
pa_sink_set_fixed_latency(u->sink, frames_to_usec(u, (int64_t) u->ring->target_frames + u->base_buf_frames));
|
||||
|
||||
if (!(u->thread = pa_thread_new("directaudio-native-sink", thread_func, u))) {
|
||||
pa_log("Failed to create thread.");
|
||||
goto fail;
|
||||
}
|
||||
pa_sink_put(u->sink);
|
||||
pa_modargs_free(ma);
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
if (ma)
|
||||
pa_modargs_free(ma);
|
||||
pa__done(m);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int pa__get_n_used(pa_module *m) {
|
||||
struct userdata *u;
|
||||
|
||||
pa_assert(m);
|
||||
pa_assert_se(u = m->userdata);
|
||||
return pa_sink_linked_by(u->sink);
|
||||
}
|
||||
|
||||
void pa__done(pa_module *m) {
|
||||
struct userdata *u;
|
||||
|
||||
pa_assert(m);
|
||||
if (!(u = m->userdata))
|
||||
return;
|
||||
if (u->sink)
|
||||
pa_sink_unlink(u->sink);
|
||||
if (u->thread) {
|
||||
pa_asyncmsgq_send(u->thread_mq.inq, NULL, PA_MESSAGE_SHUTDOWN, NULL, 0, NULL);
|
||||
pa_thread_free(u->thread);
|
||||
}
|
||||
pa_thread_mq_done(&u->thread_mq);
|
||||
if (u->sink)
|
||||
pa_sink_unref(u->sink);
|
||||
close_output(u);
|
||||
if (u->rtpoll)
|
||||
pa_rtpoll_free(u->rtpoll);
|
||||
pthread_mutex_destroy(&u->lock);
|
||||
pa_xfree(u->ring);
|
||||
pa_xfree(u);
|
||||
}
|
||||
@@ -291,7 +291,7 @@ def libraries(steam_root):
|
||||
except OSError:
|
||||
text = ""
|
||||
paths = [path.replace("\\\\", "\\") for path in re.findall(r'"path"\s+"((?:[^"\\]|\\.)*)"', text)]
|
||||
return list(dict.fromkeys([Path(steam_root)] + [Path(path) for path in paths]))
|
||||
return list(dict.fromkeys([Path(steam_root), Path(LIBRARY)] + [Path(path) for path in paths]))
|
||||
|
||||
|
||||
def ours(manifest, appid):
|
||||
@@ -311,16 +311,6 @@ def unpark(steamapps):
|
||||
os.replace(manifest, live)
|
||||
|
||||
|
||||
def seeded(steamapps, game, appid):
|
||||
target = steamapps / "common" / f"DroidDeck-{appid}"
|
||||
manifest = steamapps / f"appmanifest_{appid}.acf"
|
||||
try:
|
||||
return (target.is_symlink() and target.is_dir() and manifest.is_file() and ours(manifest, appid)
|
||||
and os.readlink(target) in (game.get("folder", ""), legacy_path(game.get("folder", ""))))
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def park(steamapps):
|
||||
for target in sorted((steamapps / "common").glob("DroidDeck-*")):
|
||||
appid = target.name.removeprefix("DroidDeck-")
|
||||
@@ -438,9 +428,6 @@ def route(steam_root, acct, games):
|
||||
continue
|
||||
except OSError as error:
|
||||
print(f"droiddeck-steam-games: import unavailable ({error})")
|
||||
elif isinstance(appid, int) and 0 < appid < 0x80000000 and seeded(steamapps, game, appid):
|
||||
routes[str(game["appid"])] = appid
|
||||
continue
|
||||
shortcuts.append(game)
|
||||
try:
|
||||
park(steamapps)
|
||||
|
||||
@@ -8,7 +8,7 @@ ROOTFS_SHA256=2854b06d3ff1b8f6e526135bfb6dd5b7b30ab3ab73e79ae933a3d9fed959a178
|
||||
CDN=https://client-update.fastly.steamstatic.com
|
||||
# Must match the -clientbeta droiddeck-session starts this client with (steam_channel).
|
||||
X64_CHANNEL=steamdeck_publicbeta
|
||||
MANIFEST=steam_client_${X64_CHANNEL}_steamrt64
|
||||
MANIFEST=steam_client_${X64_CHANNEL}_ubuntu12
|
||||
|
||||
REAL_HOME=${HOME:-/root}
|
||||
X64_HOME=${BL_X64_HOME:-$REAL_HOME/.droiddeck-x64}
|
||||
@@ -37,17 +37,17 @@ x64_client_parts() {
|
||||
/^\t"/ && NF >= 3 && $4 == "" { block = $2 }
|
||||
$2 == "file" { file[block] = $4 }
|
||||
$2 == "sha2" { sha[block] = $4 }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_steamrt64$/) print b, file[b], sha[b] }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_ubuntu12$/) print b, file[b], sha[b] }
|
||||
' "$1" | sort
|
||||
}
|
||||
|
||||
x64_sync_client() {
|
||||
local chan cur version work total i block file sum old
|
||||
chan=$(cat "$STEAM_X64/package/beta" 2>/dev/null || echo "$X64_CHANNEL")
|
||||
old=$STEAM_X64/package/steam_client_${chan}_steamrt64.manifest
|
||||
old=$STEAM_X64/package/steam_client_${chan}_ubuntu12.manifest
|
||||
cur=$(awk -F'"' '$2 == "version" { print $4; exit }' "$old" 2>/dev/null || true)
|
||||
work=$(mktemp -d)
|
||||
if ! curl -fsSL "$CDN/steam_client_${chan}_steamrt64" -o "$work/manifest"; then
|
||||
if ! curl -fsSL "$CDN/steam_client_${chan}_ubuntu12" -o "$work/manifest"; then
|
||||
rm -rf "$work"; return 0
|
||||
fi
|
||||
version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
|
||||
@@ -471,7 +471,7 @@ if [ ! -x "$STEAM_X64/steamrt64/steam" ] || [ ! -f "$STEAM_X64/package/droiddeck
|
||||
/^\t"/ && NF >= 3 && $4 == "" { block = $2 }
|
||||
$2 == "file" { file[block] = $4 }
|
||||
$2 == "sha2" { sha[block] = $4 }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_steamrt64$/) print b, file[b], sha[b] }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_ubuntu12$/) print b, file[b], sha[b] }
|
||||
' "$work/manifest" | sort > "$work/parts"
|
||||
[ -s "$work/parts" ] || { step "no client components in $MANIFEST"; exit 1; }
|
||||
mkdir -p "$STEAM_X64/package"
|
||||
@@ -910,7 +910,7 @@ x64_exec() {
|
||||
[ -x "$ARM_STEAM/steamrtarm64/steamwebhelper" ]; then
|
||||
local chan x86_build arm_build
|
||||
chan=$(cat "$STEAM_X64/package/beta" 2>/dev/null || echo "$X64_CHANNEL")
|
||||
x86_build=$(awk -F'"' '$2 == "version" { print $4; exit }' "$STEAM_X64/package/steam_client_${chan}_steamrt64.manifest" 2>/dev/null || true)
|
||||
x86_build=$(awk -F'"' '$2 == "version" { print $4; exit }' "$STEAM_X64/package/steam_client_${chan}_ubuntu12.manifest" 2>/dev/null || true)
|
||||
arm_build=$(awk -F'"' '$2 == "version" { print $4; exit }' "$ARM_STEAM/package/steam_client_${chan}_linuxarm64.manifest" 2>/dev/null || true)
|
||||
if [ -n "$x86_build" ] && [ "$x86_build" = "$arm_build" ]; then
|
||||
export BL_X64_ARM_WEBHELPER=/usr/local/bin/droiddeck-x64-arm-webhelper
|
||||
@@ -937,7 +937,7 @@ x64_exec() {
|
||||
touch "$STEAM_X64/logs/bootstrap_log.txt"
|
||||
( tail -n0 -F --pid=$$ "$STEAM_X64/logs/bootstrap_log.txt" 2>/dev/null |
|
||||
grep --line-buffered -oE 'Downloading update \([0-9,]+ of [0-9,]+ KB\)' | sed -u 's/,//g' >&2 ) &
|
||||
echo "== x86-64 client: steamrt64 $(x64_manifest_version steamrt64) (the client may update itself)" >&2
|
||||
echo "== x86-64 client: ubuntu12 $(x64_manifest_version ubuntu12), steamrt64 $(x64_manifest_version steamrt64) (the client may update itself)" >&2
|
||||
x64_webhelper_gpu && { x64_gpu_watch >&2 & }
|
||||
exec "${BL_FEX:-/usr/local/bin/FEX}" "$STEAM_X64/steamrt64/steam" "$@"
|
||||
}
|
||||
@@ -952,76 +952,6 @@ arm, x64, arm_reg, x64_reg = sys.argv[1:5]
|
||||
FILES = ("config/loginusers.vdf", "local.vdf")
|
||||
|
||||
|
||||
def read(path):
|
||||
try:
|
||||
return open(path, encoding="utf-8", errors="surrogateescape").read()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def write(path, text):
|
||||
open(path, "w", encoding="utf-8", errors="surrogateescape").write(text)
|
||||
|
||||
|
||||
def block(text, name):
|
||||
m = re.search(r'"%s"\s*\{' % name, text)
|
||||
if not m:
|
||||
return None
|
||||
depth, k = 1, m.end()
|
||||
while depth and k < len(text):
|
||||
depth += text[k] == "{"
|
||||
depth -= text[k] == "}"
|
||||
k += 1
|
||||
return m, k
|
||||
|
||||
|
||||
def accounts(root):
|
||||
text = read(os.path.join(root, "config/config.vdf")) or ""
|
||||
found = block(text, "Accounts")
|
||||
if not found:
|
||||
return {}
|
||||
return dict(re.findall(r'"([^"]+)"\s*\{\s*"SteamID"\s*"(\d+)"', text[found[0].end():found[1]]))
|
||||
|
||||
|
||||
def add_accounts(root, wanted):
|
||||
path = os.path.join(root, "config/config.vdf")
|
||||
text = read(path)
|
||||
if text is None:
|
||||
return
|
||||
have = accounts(root)
|
||||
missing = {name: sid for name, sid in wanted.items() if name not in have}
|
||||
if not missing:
|
||||
return
|
||||
entries = "".join('\n\t\t\t\t\t"%s"\n\t\t\t\t\t{\n\t\t\t\t\t\t"SteamID"\t\t"%s"\n\t\t\t\t\t}' % item for item in missing.items())
|
||||
found = block(text, "Accounts")
|
||||
if found:
|
||||
text = text[:found[0].end()] + entries + text[found[0].end():]
|
||||
else:
|
||||
steam = re.search(r'"Valve"\s*\{\s*"Steam"\s*\{', text)
|
||||
if not steam:
|
||||
return
|
||||
text = text[:steam.end()] + '\n\t\t\t\t"Accounts"\n\t\t\t\t{' + entries + '\n\t\t\t\t}' + text[steam.end():]
|
||||
write(path, text)
|
||||
|
||||
|
||||
def set_keys(src_reg, dst_reg, force):
|
||||
st, dt = read(src_reg), read(dst_reg)
|
||||
if st is None or dt is None:
|
||||
return
|
||||
for key in ("AutoLoginUser", "RememberPassword"):
|
||||
v = re.search(r'"%s"\s*"([^"]*)"' % key, st)
|
||||
if not v:
|
||||
continue
|
||||
if re.search(r'"%s"\s*"' % key, dt):
|
||||
if force:
|
||||
dt = re.sub(r'("%s"\s*")[^"]*(")' % key, lambda m: m.group(1) + v.group(1) + m.group(2), dt, count=1)
|
||||
continue
|
||||
hkcu = re.search(r'"HKCU"\s*\{\s*"Software"\s*\{\s*"Valve"\s*\{\s*"Steam"\s*\{\n', dt)
|
||||
if hkcu:
|
||||
dt = dt[:hkcu.end()] + '\t\t\t\t\t"%s"\t\t"%s"\n' % (key, v.group(1)) + dt[hkcu.end():]
|
||||
write(dst_reg, dt)
|
||||
|
||||
|
||||
def stamp(root):
|
||||
if not all(os.path.isfile(os.path.join(root, f)) for f in FILES):
|
||||
return None
|
||||
@@ -1033,26 +963,31 @@ def stamp(root):
|
||||
|
||||
|
||||
a, x = stamp(arm), stamp(x64)
|
||||
if a is not None or x is not None:
|
||||
if x is None or (a is not None and a > x):
|
||||
src, dst, sreg, dreg, who = arm, x64, arm_reg, x64_reg, "arm64"
|
||||
if a is None and x is None:
|
||||
sys.exit(0)
|
||||
if a is not None and x is not None and abs(a - x) < 1:
|
||||
sys.exit(0)
|
||||
if x is None or (a is not None and a > x):
|
||||
src, dst, sreg, dreg, who = arm, x64, arm_reg, x64_reg, "arm64"
|
||||
else:
|
||||
src, dst, sreg, dreg, who = x64, arm, x64_reg, arm_reg, "x86-64"
|
||||
for f in FILES:
|
||||
os.makedirs(os.path.dirname(os.path.join(dst, f)), exist_ok=True)
|
||||
# copy2 keeps the time stamp, so both match until one client signs in again.
|
||||
shutil.copy2(os.path.join(src, f), os.path.join(dst, f))
|
||||
if os.path.isfile(sreg):
|
||||
st = open(sreg, encoding="utf-8", errors="surrogateescape").read()
|
||||
if not os.path.isfile(dreg):
|
||||
os.makedirs(os.path.dirname(dreg), exist_ok=True)
|
||||
shutil.copy2(sreg, dreg)
|
||||
else:
|
||||
src, dst, sreg, dreg, who = x64, arm, x64_reg, arm_reg, "x86-64"
|
||||
copy = a is None or x is None or abs(a - x) >= 1
|
||||
if copy:
|
||||
for f in FILES:
|
||||
os.makedirs(os.path.dirname(os.path.join(dst, f)), exist_ok=True)
|
||||
# copy2 keeps the time stamp, so both match until one client signs in again.
|
||||
shutil.copy2(os.path.join(src, f), os.path.join(dst, f))
|
||||
if os.path.isfile(sreg) and not os.path.isfile(dreg):
|
||||
os.makedirs(os.path.dirname(dreg), exist_ok=True)
|
||||
shutil.copy2(sreg, dreg)
|
||||
set_keys(sreg, dreg, copy)
|
||||
set_keys(dreg, sreg, False)
|
||||
add_accounts(dst, accounts(src))
|
||||
add_accounts(src, accounts(dst))
|
||||
if copy:
|
||||
print("droiddeck-steam-x64: sign-in shared from the %s client (the newer one)" % who)
|
||||
dt = open(dreg, encoding="utf-8", errors="surrogateescape").read()
|
||||
for key in ("AutoLoginUser", "RememberPassword"):
|
||||
v = re.search(r'"%s"\s*"([^"]*)"' % key, st)
|
||||
if v and re.search(r'"%s"\s*"' % key, dt):
|
||||
dt = re.sub(r'("%s"\s*")[^"]*(")' % key, lambda m: m.group(1) + v.group(1) + m.group(2), dt, count=1)
|
||||
open(dreg, "w", encoding="utf-8", errors="surrogateescape").write(dt)
|
||||
print("droiddeck-steam-x64: sign-in shared from the %s client (the newer one)" % who)
|
||||
PY
|
||||
}
|
||||
|
||||
|
||||
@@ -208,19 +208,6 @@ static long openat_in_root(int dirfd, const char *path, int flags, long mode) {
|
||||
}
|
||||
memmove(pending + target_len, tail, rest_len + 1);
|
||||
if (pending[0] == '/') {
|
||||
char saved = pending[target_len];
|
||||
pending[target_len] = 0;
|
||||
const char *inside = pending;
|
||||
while (*inside == '/') {
|
||||
inside++;
|
||||
}
|
||||
bl_stat probe;
|
||||
int host = *inside && real_syscall(BL_SYS_FSTATAT, (long)dirfd, (long)inside, (long)&probe, (long)AT_SYMLINK_NOFOLLOW) != 0 &&
|
||||
errno == ENOENT && real_syscall(BL_SYS_FSTATAT, (long)AT_FDCWD, (long)pending, (long)&probe, 0L) == 0;
|
||||
pending[target_len] = saved;
|
||||
if (host) {
|
||||
return real_syscall(SYS_openat, (long)AT_FDCWD, (long)pending, (long)flags, mode);
|
||||
}
|
||||
resolved_len = 0;
|
||||
}
|
||||
rest = pending;
|
||||
|
||||
@@ -117,17 +117,8 @@ CASES = [
|
||||
("o", os.O_RDONLY, "proc/self/status", "proc"),
|
||||
]
|
||||
|
||||
# Only the preload: an absolute link to a path missing in the tree but present outside it, as droiddeck-fex's rootfs has.
|
||||
HOST_CASES = [
|
||||
("r", os.O_RDONLY, "lib/x86_64-linux-gnu/libbar.so.2", "host:libbar.so.2"),
|
||||
("r", os.O_RDONLY, "hostdir/sub/data", "host:sub/data"),
|
||||
("r", os.O_RDONLY | os.O_DIRECTORY, "hostdir/sub", "host:sub"),
|
||||
("r", os.O_RDONLY, "etc/alternatives/hn", "etc/hostname"),
|
||||
("r", os.O_RDONLY, "hostdir/missing", "ENOENT"),
|
||||
]
|
||||
|
||||
|
||||
def make_tree(root, outside):
|
||||
def make_tree(root):
|
||||
for folder in ("usr/lib/x86_64-linux-gnu", "etc/alternatives", "loop", "chain"):
|
||||
(root / folder).mkdir(parents=True)
|
||||
(root / "usr/lib/x86_64-linux-gnu/libfoo.so.1.2").write_text("x86")
|
||||
@@ -138,24 +129,16 @@ def make_tree(root, outside):
|
||||
"etc/alternatives/hn": "/etc/hostname", "etc/escape": "../../../../../../etc/hostname", "loop/a": "b",
|
||||
"loop/b": "a", "dirlink": "/usr", "rootlink": "/", "dangling": "/usr/newfile"}
|
||||
links.update({"chain/l%d" % i: ("l%d" % (i + 1) if i < 41 else "end") for i in range(1, 42)})
|
||||
links.update({"usr/lib/x86_64-linux-gnu/libbar.so.2": str(outside / "libbar.so.2"), "hostdir": str(outside)})
|
||||
for link, target in links.items():
|
||||
os.symlink(target, root / link)
|
||||
|
||||
|
||||
def make_outside(outside):
|
||||
(outside / "sub").mkdir(parents=True)
|
||||
(outside / "libbar.so.2").write_text("host")
|
||||
(outside / "sub/data").write_text("host")
|
||||
|
||||
|
||||
def labels(lines, root, outside):
|
||||
def labels(lines, root):
|
||||
inodes = {(os.lstat(root).st_dev, os.lstat(root).st_ino): "."}
|
||||
for base, prefix in ((root, ""), (outside, "host:")):
|
||||
for folder, dirs, files in os.walk(base):
|
||||
for name in dirs + files:
|
||||
st = os.lstat(os.path.join(folder, name))
|
||||
inodes[(st.st_dev, st.st_ino)] = prefix + os.path.relpath(os.path.join(folder, name), base)
|
||||
for folder, dirs, files in os.walk(root):
|
||||
for name in dirs + files:
|
||||
st = os.lstat(os.path.join(folder, name))
|
||||
inodes[(st.st_dev, st.st_ino)] = os.path.relpath(os.path.join(folder, name), root)
|
||||
out = []
|
||||
for line in lines:
|
||||
kind, *values = line.split()
|
||||
@@ -178,19 +161,16 @@ class Openat2PreloadTest(unittest.TestCase):
|
||||
source.write_text(PROGRAM)
|
||||
subprocess.run(["cc", "-O2", "-Wall", "-Werror", "-o", str(cls.tmp / "openat2"), str(source), "-ldl"], check=True)
|
||||
cls.trees = (cls.tmp / "kernel", cls.tmp / "preload")
|
||||
cls.outside = cls.tmp / "outside"
|
||||
make_outside(cls.outside)
|
||||
for tree in cls.trees:
|
||||
make_tree(tree, cls.outside)
|
||||
every = CASES + HOST_CASES
|
||||
cases = "".join("%s\t%d\t%s\n" % case[:3] for case in every)
|
||||
make_tree(tree)
|
||||
cases = "".join("%s\t%d\t%s\n" % case[:3] for case in CASES)
|
||||
result = subprocess.run([str(cls.tmp / "openat2"), *map(str, cls.trees)], input=cases, capture_output=True, text=True,
|
||||
env=dict(os.environ, LD_PRELOAD=str(cls.lib)), timeout=60)
|
||||
if result.returncode != 0:
|
||||
raise AssertionError(result.stdout + result.stderr)
|
||||
lines = result.stdout.splitlines()
|
||||
cls.kernel = labels(lines[1:1 + len(CASES)], cls.trees[0], cls.outside) if lines[0] == "kernel" else None
|
||||
cls.preload = labels(lines[-len(every):], cls.trees[1], cls.outside)
|
||||
cls.kernel = labels(lines[1:1 + len(CASES)], cls.trees[0]) if lines[0] == "kernel" else None
|
||||
cls.preload = labels(lines[-len(CASES):], cls.trees[1])
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
@@ -201,10 +181,6 @@ class Openat2PreloadTest(unittest.TestCase):
|
||||
self.assertEqual(got, case[3], case)
|
||||
self.assertTrue((self.trees[1] / "usr/newfile").exists())
|
||||
|
||||
def test_preload_finishes_a_link_to_a_path_only_outside_the_root_there(self):
|
||||
for case, got in zip(HOST_CASES, self.preload[len(CASES):]):
|
||||
self.assertEqual(got, case[3], case)
|
||||
|
||||
def test_the_kernel_agrees(self):
|
||||
if self.kernel is None:
|
||||
self.skipTest("this kernel or sandbox has no openat2")
|
||||
|
||||
@@ -311,24 +311,6 @@ class ImportsTest(unittest.TestCase):
|
||||
self.assertFalse((self.steam / 'steamapps/appmanifest_42.acf').exists())
|
||||
self.assertFalse(os.path.lexists(self.steam / 'steamapps/common/DroidDeck-42'))
|
||||
|
||||
def test_imported_game_stays_imported_when_ownership_is_unconfirmed(self):
|
||||
self.snapshot()
|
||||
imports.route(self.steam, self.acct, [self.game])
|
||||
for owned, account in [(False, '123'), (True, '456')]:
|
||||
self.snapshot(owned, account)
|
||||
self.assertEqual(([], {str(self.game['appid']): 42}), imports.route(self.steam, self.acct, [self.game]))
|
||||
self.assertTrue((self.steam / 'steamapps/appmanifest_42.acf').is_file())
|
||||
|
||||
def test_unlisted_card_library_is_not_another_clients_install(self):
|
||||
card = self.root / 'card'
|
||||
(card / 'steamapps').mkdir(parents=True)
|
||||
(card / 'steamapps/appmanifest_42.acf').write_text('the other client installed it here')
|
||||
self.snapshot()
|
||||
with patch.object(imports, 'LIBRARY', str(card)):
|
||||
self.assertEqual(([], {str(self.game['appid']): 42}), imports.route(self.steam, self.acct, [self.game]))
|
||||
self.assertTrue((self.steam / 'steamapps/appmanifest_42.acf').is_file())
|
||||
self.assertEqual(self.folder, (self.steam / 'steamapps/common/DroidDeck-42').resolve())
|
||||
|
||||
def test_replaced_game_does_not_inherit_cached_ownership(self):
|
||||
self.snapshot()
|
||||
self.exe.write_bytes(b'a different game')
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin/droiddeck-steam-x64'
|
||||
|
||||
REGISTRY = ('"Registry"\n{\n\t"HKCU"\n\t{\n\t\t"Software"\n\t\t{\n\t\t\t"Valve"\n\t\t\t{\n\t\t\t\t"Steam"\n\t\t\t\t{\n'
|
||||
'\t\t\t\t\t"language"\t\t"english"\n%s\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n')
|
||||
CONFIG = ('"InstallConfigStore"\n{\n\t"Software"\n\t{\n\t\t"Valve"\n\t\t{\n\t\t\t"Steam"\n\t\t\t{\n%s'
|
||||
'\t\t\t\t"CompatToolMapping"\n\t\t\t\t{\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n')
|
||||
ACCOUNTS = ('\t\t\t\t"Accounts"\n\t\t\t\t{\n\t\t\t\t\t"someone"\n\t\t\t\t\t{\n'
|
||||
'\t\t\t\t\t\t"SteamID"\t\t"76561198000000001"\n\t\t\t\t\t}\n\t\t\t\t}\n')
|
||||
|
||||
|
||||
def local(token):
|
||||
entry = '\t\t"abc1"\t\t"%s"\n' % token if token else ''
|
||||
return '"MachineUserConfigStore"\n{\n\t"ConnectCache"\n\t{\n%s\t}\n}\n' % entry
|
||||
|
||||
|
||||
class X64SignInTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.home = Path(self.temp.name)
|
||||
self.arm = self.home / '.local/share/Steam'
|
||||
self.x64 = self.home / '.droiddeck-x64/.local/share/Steam'
|
||||
for root in (self.arm, self.x64):
|
||||
(root / 'config').mkdir(parents=True)
|
||||
(self.home / '.steam').mkdir()
|
||||
(self.home / '.droiddeck-x64/.steam').mkdir(parents=True)
|
||||
self.write(self.arm / 'local.vdf', local('00ff'))
|
||||
self.write(self.arm / 'config/loginusers.vdf', '"users"\n{\n}\n')
|
||||
self.write(self.arm / 'config/config.vdf', CONFIG % ACCOUNTS)
|
||||
self.write(self.home / '.steam/registry.vdf', REGISTRY % '\t\t\t\t\t"AutoLoginUser"\t\t"someone"\n')
|
||||
self.write(self.x64 / 'local.vdf', local(None))
|
||||
self.write(self.x64 / 'config/loginusers.vdf', '"users"\n{\n}\n')
|
||||
self.write(self.x64 / 'config/config.vdf', CONFIG % '')
|
||||
self.write(self.home / '.droiddeck-x64/.steam/registry.vdf', REGISTRY % '')
|
||||
|
||||
def write(self, path, text):
|
||||
path.write_text(text)
|
||||
|
||||
def sync(self):
|
||||
env = dict(os.environ, HOME=str(self.home))
|
||||
env.pop('XDG_DATA_HOME', None)
|
||||
env.pop('BL_X64_HOME', None)
|
||||
return subprocess.run(['bash', str(SCRIPT), '--sync-login'], env=env, capture_output=True, text=True, check=True)
|
||||
|
||||
def test_a_refused_x64_sign_in_gets_the_token_account_and_auto_login(self):
|
||||
self.assertIn('sign-in shared from the arm64 client', self.sync().stdout)
|
||||
self.assertEqual(local('00ff'), (self.x64 / 'local.vdf').read_text())
|
||||
self.assertIn('"SteamID"\t\t"76561198000000001"', (self.x64 / 'config/config.vdf').read_text())
|
||||
self.assertIn('"AutoLoginUser"\t\t"someone"', (self.home / '.droiddeck-x64/.steam/registry.vdf').read_text())
|
||||
|
||||
def test_an_earlier_copy_still_gains_the_account_and_nothing_repeats(self):
|
||||
self.sync()
|
||||
self.write(self.x64 / 'config/config.vdf', CONFIG % '')
|
||||
self.write(self.home / '.droiddeck-x64/.steam/registry.vdf', REGISTRY % '')
|
||||
self.assertNotIn('sign-in shared', self.sync().stdout)
|
||||
self.assertIn('"76561198000000001"', (self.x64 / 'config/config.vdf').read_text())
|
||||
self.assertIn('"AutoLoginUser"', (self.home / '.droiddeck-x64/.steam/registry.vdf').read_text())
|
||||
self.sync()
|
||||
for root in (self.arm, self.x64):
|
||||
self.assertEqual(1, (root / 'config/config.vdf').read_text().count('"SteamID"'))
|
||||
self.assertEqual(1, (self.home / '.droiddeck-x64/.steam/registry.vdf').read_text().count('AutoLoginUser'))
|
||||
|
||||
def test_a_newer_x64_sign_in_moves_to_arm64(self):
|
||||
self.write(self.x64 / 'local.vdf', local('11ee'))
|
||||
os.utime(self.arm / 'local.vdf', (1, 1))
|
||||
self.assertIn('from the x86-64 client', self.sync().stdout)
|
||||
self.assertEqual(local('11ee'), (self.arm / 'local.vdf').read_text())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user