mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1799fc6621 | ||
|
|
b711a96863 | ||
|
|
f3c923b472 | ||
|
|
1e804562b1 | ||
|
|
e275fbd788 | ||
|
|
c3bd298ecf | ||
|
|
807f5adcba | ||
|
|
d6bc4da759 |
No files matched your search
@@ -12,6 +12,7 @@ captures/
|
||||
# copies cannot drift.
|
||||
app/src/main/assets/linuxfs/
|
||||
app/src/main/jniLibs/arm64-v8a/libproot.so
|
||||
app/src/main/jniLibs/arm64-v8a/.proot-inputs
|
||||
app/src/main/jniLibs/arm64-v8a/libproot-loader.so
|
||||
|
||||
# Kotlin compiler session files (build-tool state, never source).
|
||||
|
||||
@@ -126,7 +126,9 @@ class MainActivity : ComponentActivity() {
|
||||
private var gameCores by mutableStateOf<Set<Int>>(emptySet())
|
||||
private var tuSysmem by mutableStateOf(false)
|
||||
private var zinkLazy by mutableStateOf(false)
|
||||
private var noXalia by mutableStateOf(false)
|
||||
private var noXalia by mutableStateOf(true)
|
||||
private var gamescopeRealtime by mutableStateOf(false)
|
||||
private var sustainedPerformance by mutableStateOf(false)
|
||||
private var prootNoSeccomp by mutableStateOf(false)
|
||||
private var guestHostname by mutableStateOf(SessionPrefs.DEFAULT_GUEST_HOSTNAME)
|
||||
private var phantomWarning by mutableStateOf<String?>(null)
|
||||
@@ -503,8 +505,9 @@ class MainActivity : ComponentActivity() {
|
||||
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
}
|
||||
// The microphone is on by default; ask once, with the storage prompt, so voice chat works
|
||||
// without a trip to the settings. A refusal is not asked again - the toggle asks when used.
|
||||
// The microphone is off by default. Someone who turned it on before the permission was
|
||||
// granted is asked once here, with the storage prompt; a refusal is not asked again - the
|
||||
// toggle asks when used.
|
||||
if (SessionPrefs.micEnabled(this) && !SessionPrefs.micAsked(this)
|
||||
&& checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
wanted.add(Manifest.permission.RECORD_AUDIO)
|
||||
@@ -893,6 +896,7 @@ class MainActivity : ComponentActivity() {
|
||||
cores = CpuCores.all.map { c -> CoreRow(c, "cpu$c" + (CpuCores.maxGhz(c)?.let { String.format(java.util.Locale.US, " · %.1f GHz", it) } ?: "")) },
|
||||
clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores,
|
||||
tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia,
|
||||
gamescopeRealtime = gamescopeRealtime, sustainedPerformance = sustainedPerformance,
|
||||
prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning,
|
||||
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
|
||||
onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on },
|
||||
@@ -900,6 +904,8 @@ class MainActivity : ComponentActivity() {
|
||||
onGlThread = { on -> SessionPrefs.setGlThread(this, on); glThread = on },
|
||||
onNoGlError = { on -> SessionPrefs.setNoGlError(this, on); noGlError = on },
|
||||
onNoXalia = { on -> SessionPrefs.setNoXalia(this, on); noXalia = on },
|
||||
onGamescopeRealtime = { on -> SessionPrefs.setGamescopeRealtime(this, on); gamescopeRealtime = on },
|
||||
onSustainedPerformance = { on -> SessionPrefs.setSustainedPerformance(this, on); sustainedPerformance = on },
|
||||
onProotNoSeccomp = { on -> SessionPrefs.setProotNoSeccomp(this, on); prootNoSeccomp = on },
|
||||
onGuestHostname = { name -> SessionPrefs.setGuestHostname(this, name) },
|
||||
onClientCore = { core, on ->
|
||||
@@ -944,6 +950,7 @@ class MainActivity : ComponentActivity() {
|
||||
customResolution = SessionPrefs.customResolution(this, mode)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
steamDeckMode = SessionPrefs.steamDeckMode(this)
|
||||
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
|
||||
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
|
||||
refreshAddedGames()
|
||||
@@ -993,6 +1000,8 @@ class MainActivity : ComponentActivity() {
|
||||
tuSysmem = SessionPrefs.tuSysmem(this)
|
||||
zinkLazy = SessionPrefs.zinkLazy(this)
|
||||
noXalia = SessionPrefs.noXalia(this)
|
||||
gamescopeRealtime = SessionPrefs.gamescopeRealtime(this)
|
||||
sustainedPerformance = SessionPrefs.sustainedPerformance(this)
|
||||
prootNoSeccomp = SessionPrefs.prootNoSeccomp(this)
|
||||
guestHostname = SessionPrefs.guestHostname(this)
|
||||
refreshPhantomStatus()
|
||||
|
||||
@@ -849,9 +849,18 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return Pair(width and 1.inv(), height and 1.inv())
|
||||
}
|
||||
|
||||
/**
|
||||
* The rate the panel runs the session at: its fastest mode at the current size, which PerfMode
|
||||
* asks for. The display's current rate is read too early for that - a phone still at 60 Hz
|
||||
* when the surface arrives handed gamescope -r 60 and capped every game at 60.
|
||||
*/
|
||||
private fun refreshHz(): Float {
|
||||
val display = if (Build.VERSION.SDK_INT >= 30) display else windowManager.defaultDisplay
|
||||
val hz = display?.refreshRate ?: 60f
|
||||
val cur = display?.mode
|
||||
val best = if (cur == null) null else display.supportedModes
|
||||
.filter { it.physicalWidth == cur.physicalWidth && it.physicalHeight == cur.physicalHeight }
|
||||
.maxOfOrNull { it.refreshRate }
|
||||
val hz = maxOf(best ?: 0f, display?.refreshRate ?: 0f)
|
||||
return if (hz > 1f) hz else 60f
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ object DeviceReport {
|
||||
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
|
||||
k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" })
|
||||
k("Skip xalia", SessionPrefs.noXalia(context))
|
||||
k("gamescope realtime", SessionPrefs.gamescopeRealtime(context))
|
||||
k("Sustained performance mode", SessionPrefs.sustainedPerformance(context))
|
||||
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
|
||||
k("Guest host name", SessionPrefs.guestHostname(context))
|
||||
k("DirectAudio for games", SessionPrefs.directAudio(context))
|
||||
|
||||
@@ -27,8 +27,9 @@ object GameEnvironment {
|
||||
|
||||
fun defaults(preset: String): Map<String, String?> = linkedMapOf<String, String?>(
|
||||
"MESA_SHADER_CACHE_DISABLE" to "false",
|
||||
"VKD3D_FEATURE_LEVEL" to "12_2",
|
||||
"VKD3D_SHADER_MODEL" to "6_9",
|
||||
// The feature level is left to vkd3d-proton, which reports what the driver can do; forcing
|
||||
// 12_2 sent games down DX12 Ultimate paths Turnip only emulates. 6_6 is WinNative's model.
|
||||
"VKD3D_SHADER_MODEL" to "6_6",
|
||||
).apply {
|
||||
FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) }
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import android.util.Log
|
||||
* The manifest already makes the app a game (appCategory + game_mode_config); this is the
|
||||
* per-window half, applied when the session activity is created:
|
||||
*
|
||||
* - sustained performance mode (Android 7+): a clock floor that does not throttle away after
|
||||
* two minutes, which suits an hour in Big Picture better than a boost that fades;
|
||||
* - sustained performance mode (Android 7+), only when the Performance page asks for it: clocks
|
||||
* the SoC can hold for hours, which on many devices caps the peak a session otherwise gets;
|
||||
* - the panel's fastest mode at its current size (Android 6+): a 120 Hz phone is switched to
|
||||
* 120 Hz for the menu, not only when a game votes for it;
|
||||
* - GameManager's game state (Android 13+): "in gameplay", so an OEM framework that releases
|
||||
@@ -30,7 +30,7 @@ object PerfMode {
|
||||
val parts = ArrayList<String>(3)
|
||||
|
||||
// Sustained performance mode.
|
||||
parts += if (Build.VERSION.SDK_INT >= 24) {
|
||||
parts += if (!SessionPrefs.sustainedPerformance(a)) "sustained mode off" else if (Build.VERSION.SDK_INT >= 24) {
|
||||
val pm = a.getSystemService(PowerManager::class.java)
|
||||
if (pm?.isSustainedPerformanceModeSupported == true) {
|
||||
try { a.window.setSustainedPerformanceMode(true); "sustained mode on" } catch (t: Throwable) { Log.w(TAG, "sustained mode", t); "sustained mode refused" }
|
||||
|
||||
@@ -143,8 +143,8 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("directAudio", on).apply()
|
||||
}
|
||||
|
||||
/** The microphone for voice chat, on unless turned off; used only once RECORD_AUDIO is granted. */
|
||||
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", true)
|
||||
/** The microphone for voice chat, off until the user turns it on (which asks for RECORD_AUDIO). */
|
||||
fun micEnabled(context: Context): Boolean = prefs(context).getBoolean("mic", false)
|
||||
|
||||
/** Whether the app has already asked for the microphone once at start-up. */
|
||||
fun micAsked(context: Context): Boolean = prefs(context).getBoolean("micAsked", false)
|
||||
@@ -188,14 +188,39 @@ object SessionPrefs {
|
||||
* xalia is an x86 Windows program Proton launches to give Windows programs gamepad navigation.
|
||||
* Under FEX it cannot load the session's aarch64 preload shim, so its socket() and memfd calls
|
||||
* reach the vendor's seccomp filter raw; where that answers ENOSYS - a Galaxy Fold, measured -
|
||||
* it storms, and the session dies seconds after Big Picture appears.
|
||||
* it storms, and the session dies seconds after Big Picture appears. On by default: where it
|
||||
* does run, it sits beside every game under FEX for nothing a controller-first session needs
|
||||
* (about 10% of a core beside Once Upon a KATAMARI on an SD 8 Gen 2).
|
||||
*/
|
||||
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", false)
|
||||
fun noXalia(context: Context): Boolean = prefs(context).getBoolean("noXalia", true)
|
||||
|
||||
fun setNoXalia(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("noXalia", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether gamescope asks for realtime-priority Vulkan queues (GAMESCOPE_FORCE_VULKAN_REALTIME=1,
|
||||
* which the app's gamescope build honours without CAP_SYS_NICE). Off by default, as in
|
||||
* Bannerlator's session: the compositor's queue preempting the game's buys nothing on a device
|
||||
* whose GPU is waiting on the CPU.
|
||||
*/
|
||||
/**
|
||||
* Whether the session window asks for Android's sustained performance mode. Off by default: it
|
||||
* holds the SoC at clocks it can keep for hours, which on many devices is a cap well under
|
||||
* the peak a game session gets without it (WinNative never asks for it).
|
||||
*/
|
||||
fun sustainedPerformance(context: Context): Boolean = prefs(context).getBoolean("sustainedPerformance", false)
|
||||
|
||||
fun setSustainedPerformance(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("sustainedPerformance", on).apply()
|
||||
}
|
||||
|
||||
fun gamescopeRealtime(context: Context): Boolean = prefs(context).getBoolean("gamescopeRealtime", false)
|
||||
|
||||
fun setGamescopeRealtime(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("gamescopeRealtime", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether proot runs without its seccomp acceleration (PROOT_NO_SECCOMP=1).
|
||||
*
|
||||
@@ -309,8 +334,12 @@ object SessionPrefs {
|
||||
fun resolutionChosen(context: Context, mode: String): Boolean =
|
||||
prefs(context).contains("resolutionCap.$mode") || customResolution(context, mode) != null
|
||||
|
||||
/** The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's defaults. */
|
||||
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "") ?: ""
|
||||
/**
|
||||
* The FEXCore preset for the games the client launches (core/FexPreset ids); "" = FEX's own
|
||||
* defaults. PERFORMANCE_TSO unless chosen: TSO only on the main path, x87 reduced precision
|
||||
* and multiblock, as WinNative ships it.
|
||||
*/
|
||||
fun fexPreset(context: Context): String = prefs(context).getString("fexPreset", "PERFORMANCE_TSO") ?: "PERFORMANCE_TSO"
|
||||
|
||||
fun setFexPreset(context: Context, id: String) {
|
||||
prefs(context).edit().putString("fexPreset", id).apply()
|
||||
|
||||
@@ -240,10 +240,12 @@ class SessionService : Service() {
|
||||
val override = File(Environment.getExternalStorageDirectory(), TU_DEBUG_SWITCH)
|
||||
.takeIf { it.isFile }?.let { FileUtils.readString(it)?.trim() }
|
||||
if (!override.isNullOrEmpty()) return override
|
||||
if (SessionPrefs.tuSysmem(this)) return "sysmem"
|
||||
if (linuxDriverId.isEmpty()) return null
|
||||
val name = LinuxVulkanDriverManager(this).getDriverName(linuxDriverId).lowercase()
|
||||
return if (name.contains("710-720") || name.contains("710_720")) "sysmem" else null
|
||||
// noconform: Turnip keeps the Vulkan features it has not passed conformance for (WinNative's
|
||||
// default beside sysmem), which DXVK and vkd3d-proton reach for.
|
||||
val sysmem = SessionPrefs.tuSysmem(this) || linuxDriverId.isNotEmpty() &&
|
||||
LinuxVulkanDriverManager(this).getDriverName(linuxDriverId).lowercase()
|
||||
.let { it.contains("710-720") || it.contains("710_720") }
|
||||
return if (sysmem) "noconform,sysmem" else "noconform"
|
||||
}
|
||||
|
||||
// ── The session ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -519,6 +521,8 @@ class SessionService : Service() {
|
||||
// BL_STEAMDECK; it is the one that builds the command line).
|
||||
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
|
||||
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
|
||||
// Zink packs its descriptor sets and shader interfaces tighter (WinNative's default).
|
||||
guest.add("ZINK_DEBUG=compact")
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
|
||||
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
|
||||
if (SessionState.mode == MODE_STEAM) {
|
||||
@@ -526,9 +530,12 @@ class SessionService : Service() {
|
||||
SessionPrefs.writeForceFullscreenFlag(this)
|
||||
}
|
||||
// Proton's own gate for its xalia helper (its `proton` script reads this, and sets
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Off by default: xalia is Valve's, and on a device
|
||||
// whose seccomp answers its syscalls normally there is no reason to take it away.
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Skipped by default: under FEX it costs every game
|
||||
// a slice of a core for gamepad navigation the session already has.
|
||||
if (SessionPrefs.noXalia(this)) guest.add("PROTON_USE_XALIA=0")
|
||||
// gamescope's realtime Vulkan queues (the session script turns this into
|
||||
// GAMESCOPE_FORCE_VULKAN_REALTIME); off unless the user turns it on.
|
||||
guest.add("BL_GAMESCOPE_REALTIME=" + (if (SessionPrefs.gamescopeRealtime(this)) "1" else "0"))
|
||||
// Anything else, for a device that cannot be reached with a debugger: Downloads/droiddeck-env
|
||||
// holds KEY=VALUE lines that go into the session's environment as written, after ours, so a
|
||||
// line here wins. Zink and Turnip tunables (ZINK_DESCRIPTORS=lazy, MESA_*), gamescope's,
|
||||
|
||||
@@ -37,6 +37,8 @@ fun PerformancePage(
|
||||
glThread: Boolean,
|
||||
noGlError: Boolean,
|
||||
noXalia: Boolean,
|
||||
gamescopeRealtime: Boolean,
|
||||
sustainedPerformance: Boolean,
|
||||
prootNoSeccomp: Boolean,
|
||||
guestHostname: String,
|
||||
phantomWarning: String?,
|
||||
@@ -46,6 +48,8 @@ fun PerformancePage(
|
||||
onGlThread: (Boolean) -> Unit,
|
||||
onNoGlError: (Boolean) -> Unit,
|
||||
onNoXalia: (Boolean) -> Unit,
|
||||
onGamescopeRealtime: (Boolean) -> Unit,
|
||||
onSustainedPerformance: (Boolean) -> Unit,
|
||||
onProotNoSeccomp: (Boolean) -> Unit,
|
||||
onGuestHostname: (String) -> Unit,
|
||||
onClientCore: (Int, Boolean) -> Unit,
|
||||
@@ -94,6 +98,11 @@ fun PerformancePage(
|
||||
"Disables per-call GL validation.",
|
||||
noGlError, onChange = onNoGlError,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "gsrealtime", "gamescope: realtime GPU queue",
|
||||
"Gives the compositor's GPU work priority over the game's. Can smooth frame pacing, but can cost games GPU time.",
|
||||
gamescopeRealtime, onChange = onGamescopeRealtime,
|
||||
)
|
||||
}
|
||||
SettingsGroup("Session fixes") {
|
||||
ToggleRow(
|
||||
@@ -103,9 +112,14 @@ fun PerformancePage(
|
||||
)
|
||||
ToggleRow(
|
||||
host, "xalia", "Skip Steam's xalia helper",
|
||||
"Disables Proton's gamepad navigation helper. Try if sessions crash at startup.",
|
||||
"Disables Proton's gamepad navigation helper, which costs every game CPU time. Turn off only if a game needs it.",
|
||||
noXalia, onChange = onNoXalia,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "sustained", "Android sustained performance mode",
|
||||
"Holds clocks the device can keep for hours. Steadier on hot devices, but usually a lower peak.",
|
||||
sustainedPerformance, onChange = onSustainedPerformance,
|
||||
)
|
||||
ToggleRow(
|
||||
host, "seccomp", "Run proot without seccomp",
|
||||
"May fix missing syscall errors on some kernels, but can reduce performance.",
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# DroidDeck - Performance findings
|
||||
|
||||
Why DroidDeck 0.2.0 ran slower than WinNative and Bannerlator for many people, what was found, what was
|
||||
changed, what was measured, and what is still open. Written 2026-09-29 from the Discord reports, the GitHub
|
||||
issues, and a day of measurement on an AYN Thor (SD 8 Gen 2, Adreno 740). Companion to
|
||||
[progress-log.md](progress-log.md).
|
||||
|
||||
## What people reported
|
||||
|
||||
- **Discord (#general, 2026-09-26 to 09-29).** The same game at ~90-100 fps in WinNative and ~30-40 in
|
||||
DroidDeck 0.1.7, ~25 in 0.2.0 (Adreno 732); "15 fps in a 2D Unity game" on an 8 Elite while Bannerlator's
|
||||
Steam client is fine; "20 fps / 7 fps in the Steam client, all renders on CPU" on an Adreno 8xx; Bannerlator
|
||||
locked at 120 fps where DroidDeck is not; "less performant, more compatible" as the usual summary.
|
||||
- **[#65](https://github.com/Droid-Deck/DroidDeck/issues/65)** - the best report: Silksong at 85-90 fps on
|
||||
0.1.5 and 37-50 on 0.2.0 on a Fold8, GPU 82% busy vs 16%, proot 3-5x busier, the game's main thread waiting
|
||||
on `futex` and the wineserver pipe. History table: 90 (0.1.5), 103 (0.1.6), 58/110/48 (0.1.7), 47-50 (0.2.0).
|
||||
- **[#64](https://github.com/Droid-Deck/DroidDeck/issues/64)** - Samsung's SSRM clamps CPU/GPU to ~60% in a
|
||||
session (Tab S8 Ultra). A platform limit, separate from everything below; no sanctioned way around it found.
|
||||
|
||||
## Cause 1: proot's kompat extension traced every hot syscall (fixed, PR #81)
|
||||
|
||||
`--kernel-release` (added 2026-09-25 to give the guest the `DroidDeck` hostname, `53ce764`) loads proot's
|
||||
kompat extension. Its filter list includes `futex`, `epoll_pwait`, `fcntl`, `pselect6`, `pipe2`, `eventfd2`,
|
||||
`socket`, so each of those from every guest thread stopped in the single tracer, and it drops
|
||||
`AT_SYSINFO_EHDR` on every `execve`, so glibc ran without the vDSO. With the real kernel release passed, every
|
||||
one of those handlers is a no-op. Patch `tools/proot/patches/0011-kompat-utsname-only.patch` traces only
|
||||
`uname`/`sethostname`/`setdomainname` in that case.
|
||||
|
||||
Guest micro-benchmarks (same rootfs, proot with vs without the patch):
|
||||
|
||||
| | old + `--kernel-release` | patched | no flag at all |
|
||||
|---|---|---|---|
|
||||
| futex ping-pong | 465 us | 102 us | 96 us |
|
||||
| `epoll_pwait(0)` | 60 us | 0.87 us | 0.8 us |
|
||||
| `fcntl(F_GETFL)` | 40-107 us | 0.39 us | 0.38 us |
|
||||
| `clock_gettime` | 0.34 us | 0.19 us | 0.11-0.19 us |
|
||||
|
||||
A-B-A in Once Upon a KATAMARI (standing still, only `libproot.so` swapped): +2-5% fps, proot CPU ~56% ->
|
||||
~35%. Small there because Katamari is limited by translated game code; sync-heavy games should gain more.
|
||||
|
||||
## Cause 2: a crash-looping mangoapp in Deck mode (fixed, PR #81)
|
||||
|
||||
`gamescopereaper --respawn -- mangoapp` restarts the overlay the moment it exits, with no backoff. On a build
|
||||
whose session preload lacked the System V message queue emulation, mangoapp died right after creating its
|
||||
swapchain and was restarted 132 times in ~2.5 minutes; each restart is a full exec under proot and the Steam
|
||||
menu itself lagged. Local builds that skip rebuilding `libblsession.so` hit this; CI builds do not. The wrapper
|
||||
(`tools/mangoapp/mangoapp`) now parks after more than 5 starts in 60 s.
|
||||
|
||||
## What Bannerlator and WinNative do differently
|
||||
|
||||
Read from their sources and a device that has Bannerlator installed:
|
||||
|
||||
- **Same proot patches** (0001-0009 from WinNative); the Steam client mode uses the same Valve ARM64 Proton, so
|
||||
**the same server-side Wine synchronisation** (`wineserver: using server-side synchronization` in every
|
||||
session log here). No structural gap remains once kompat is out.
|
||||
- **Defaults differ.** Bannerlator ships Deck mode off (it notes Steam Input takes the pad and breaks games),
|
||||
never passes `-steamos3`, does not set gamescope's realtime queue, runs xalia. WinNative defaults to
|
||||
`TU_DEBUG=noconform,sysmem`, `ZINK_DEBUG=compact`, FEX `PERFORMANCE_TSO`, vkd3d shader model 6_6, and never
|
||||
turns on sustained performance mode.
|
||||
|
||||
## Changes on `perf/steam-client-parity`
|
||||
|
||||
- xalia skipped by default (it ran ~10% of a core beside every game under FEX).
|
||||
- gamescope realtime Vulkan queues: new toggle (Performance -> Client interface), **off** by default.
|
||||
- Microphone off by default; no permission prompt at start-up.
|
||||
- FEX default preset `PERFORMANCE_TSO`; `TU_DEBUG=noconform` always (plus `sysmem` for the 710-720 drivers or
|
||||
when chosen); `ZINK_DEBUG=compact`; vkd3d shader model 6_6, feature level left to vkd3d-proton.
|
||||
- Sustained performance mode is opt-in (Performance page); PerfMode logs "sustained mode off".
|
||||
- The session gets the panel's fastest mode's refresh rate instead of the rate at the moment the surface arrived.
|
||||
- Session polling without forks: the fill-screen and stop watchers wait on a FIFO read timeout and read files
|
||||
with builtins; the redistributable seeder and the compat registrar (Python) start only when a prefix or the
|
||||
Steam config actually changed, not every 5-15 s.
|
||||
- The Steam page shows the saved Deck mode before the Performance page has been opened (it showed "off" while
|
||||
the session ran Deck mode with mangoapp).
|
||||
- `tools/build_local.sh`: caches its pinned downloads, skips an unchanged proot build, and says why it rebuilds
|
||||
the Docker image; a warm build is ~30 s (it used to hang on a registry fetch when the daemon briefly reported
|
||||
the image missing).
|
||||
- Turnip **sysmem** was made a default and then reverted: it gained nothing on an Adreno 740.
|
||||
|
||||
## How it was measured
|
||||
|
||||
- **fps:** `BannerWayland [stats]` lines in logcat (`adb logcat -s BannerWayland:I`), every 10 s. No `su`, no
|
||||
process on the device. The "screen" number counts presents and runs ~1.5x the game's own overlay counter in
|
||||
Alan Wake (60 vs 38-40): compare like with like, and read the overlay for the game's real rate.
|
||||
- **Scene:** Alan Wake's American Nightmare, first gameplay spot ("Secure the primary pipes"), standing still.
|
||||
Reached without touching the screen: `~/.bl-autolaunch` is ignored once Steam is up, so the game is started with
|
||||
`steam steam://rungameid/<id>` from a second proot invocation (the client forwards it), and A is pressed with
|
||||
`sendevent` on the device's own gamepad node (`/dev/input/event12`) - the app only forwards events from a real
|
||||
controller device, so `adb shell input` presses are dropped.
|
||||
- **Thermals:** the SoC reaches ~92-95 C in any run, so each run starts only after the CPU zones are under 55 C.
|
||||
A one-shot comparison made hot was misleading (the new build read 52-54 fps then, 58-61 cooled).
|
||||
|
||||
Alan Wake, cooled starts, fan at max, 60 s each (screen counter, six 10 s samples):
|
||||
|
||||
| Build | screen fps | game overlay | proot CPU | wineserver CPU |
|
||||
|---|---|---|---|---|
|
||||
| `main` (kompat fix, old defaults) | 56.9-58.6 (avg 57.7) | 39 | ~39% | ~30% |
|
||||
| new defaults | 58.6-61.5 (avg 59.6) | 38-39 | ~27% | ~34% |
|
||||
| new defaults, sysmem off | 59.3-62.9 (avg 61.1) | 38 | - | - |
|
||||
|
||||
Read: the defaults are worth at most a few percent here and inside the run-to-run noise (about +-1.5 fps). The
|
||||
game's own frame rate stayed at 38-40 with CPU ~65% and GPU ~56-60%, so this scene is bound by something none
|
||||
of these settings touch.
|
||||
|
||||
## Open leads, biggest first
|
||||
|
||||
1. **Wine synchronisation.** Proton 11 dropped esync; fsync needs `futex_waitv`, which Android's app sandbox
|
||||
blocks even on a 6.12 kernel; ntsync is not in Android kernels. Every lock and event is a pipe round trip to
|
||||
wineserver (30-34% CPU in Alan Wake, 18% of the main thread blocked in `read()` in #65). Winlator-family apps
|
||||
run esync, which needs only `eventfd` and `poll`. Restoring esync in the ARM64 Proton is the largest
|
||||
likely win and the largest piece of work; emulating `futex_waitv` in proot puts every wait back through the
|
||||
tracer and is probably a dead end.
|
||||
2. **The game's main thread under FEX.** Alan Wake's process uses 139-178% CPU with neither the CPU nor the GPU
|
||||
saturated overall. FEX settings beyond the presets (SMC checks, multiblock, TSO variants) are worth a sweep
|
||||
on a game whose main thread is the limit.
|
||||
3. **#65's other 0.1.5 -> 0.2.0 differences, untested:** the fake `power_supply` tree, `BL_HDR`/`DXVK_HDR`, the
|
||||
output width (1280 -> the panel's shape), `FEX_TSOENABLED` (1 -> 0), the tracer binary. Their Silksong setup
|
||||
is sync-heavy and is the best test of the kompat fix.
|
||||
4. **gamescope** calls `XQueryPointer` on every vblank in the Steam UI (patch `0100`); query only when a
|
||||
gamepad cursor move is pending. Needs a new pinned gamescope release.
|
||||
5. **Heavy games on a fresh install.** Metal Gear Solid V: Ground Zeroes sat on Steam's launch splash for 5+ minutes
|
||||
with wineserver at ~50% CPU and the GPU at 12% (no window ever appeared); Bulletstorm took ~4.5 minutes to reach
|
||||
its first logo, the game process at 45-105% CPU and the GPU at 9%, and the session ended a minute later. Neither
|
||||
reached a scene worth measuring. Both are first launches (redistributable install scripts, shader work), and a
|
||||
wineserver spinning at 50% while a game shows nothing is itself the server-side synchronisation cost of lead 1.
|
||||
6. **Reports not yet reproduced:** software rendering on Adreno 8xx ("all renders on CPU"), Deck mode using
|
||||
most of the RAM, Samsung SSRM clamping (#64), two DroidDeck packages both holding a session.
|
||||
7. **Smaller:** proot tracer pinned to a big core (Max's note: no core wake-up lag), `steamrtarm64` on Proton's
|
||||
`LD_LIBRARY_PATH`, the always-on wake and Wi-Fi locks, ADPF hints fed frame intervals instead of work time.
|
||||
@@ -4,6 +4,7 @@ Running engineering log for the DroidDeck app (`com.droiddeck.launcher`; called
|
||||
`com.steamdeck.launcher`, until 2026-09-23 - entries below that date keep the old name). Newest state first, then
|
||||
the timeline, then lessons and backlog. Companion to the README (what the app *does*) and to
|
||||
`docs/releases/` (what each version said) - this is *how it got here and where it stands*.
|
||||
Performance work has its own write-up: [performance.md](performance.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+57
-16
@@ -89,7 +89,17 @@ linuxfs_replaced=1
|
||||
rm -rf -- "${linuxfs_dir}"
|
||||
mkdir -p "${linuxfs_dir}"
|
||||
|
||||
if ! docker image inspect "${image_name}" >/dev/null 2>&1; then
|
||||
# Docker Desktop's VM restarts now and then, and until its engine has loaded its image store it
|
||||
# answers "No such image" for images it has. The rebuild that follows hangs on the registry
|
||||
# (the base image's credentials go through docker-credential-desktop), so give a restarting daemon
|
||||
# up to a minute before deciding the image is really missing.
|
||||
image_present=0
|
||||
for _ in $(seq 1 30); do
|
||||
if inspect_error=$(docker image inspect "${image_name}" 2>&1 >/dev/null); then image_present=1; break; fi
|
||||
sleep 2
|
||||
done
|
||||
if [[ "${image_present}" = 0 ]]; then
|
||||
echo "Build image ${image_name} not found (${inspect_error:-no error}); building it." >&2
|
||||
docker build --platform linux/amd64 -t "${image_name}" \
|
||||
-f "${repo_root}/tools/local-cross.Dockerfile" "${repo_root}"
|
||||
fi
|
||||
@@ -164,20 +174,39 @@ if [[ -z "${github_repo}" ]]; then
|
||||
github_repo=${github_repo%.git}
|
||||
fi
|
||||
|
||||
# Pinned downloads are kept between builds, named by their checksum, so a rebuild fetches nothing
|
||||
# it already has. A cached file is checked again before use; a bad one is fetched anew.
|
||||
cache_dir=${DROIDDECK_BUILD_CACHE:-"${HOME}/.cache/droiddeck-build"}
|
||||
mkdir -p "${cache_dir}"
|
||||
# cached <sha256> <name> <command...>: prints the cached path, running the command (which writes
|
||||
# to "$out") only when the cache has no file with that checksum.
|
||||
cached() {
|
||||
local sha=$1 name=$2 out
|
||||
shift 2
|
||||
out="${cache_dir}/${sha}-${name}"
|
||||
if [[ -f "${out}" ]] && printf '%s %s\n' "${sha}" "${out}" | shasum -a 256 -c - >/dev/null 2>&1; then
|
||||
echo "${out}"
|
||||
return 0
|
||||
fi
|
||||
rm -f "${out}.part"
|
||||
out="${out}.part" "$@" >&2
|
||||
printf '%s %s\n' "${sha}" "${out}.part" | shasum -a 256 -c - >&2
|
||||
mv "${out}.part" "${out}"
|
||||
echo "${out}"
|
||||
}
|
||||
|
||||
if [[ -f "${repo_root}/tools/gamescope/release.env" ]]; then
|
||||
. "${repo_root}/tools/gamescope/release.env"
|
||||
gamescope_archive="${staging_dir}/gamescope.tzst"
|
||||
gh release download "${GAMESCOPE_TAG}" -R "${github_repo}" -p gamescope.tzst -O "${gamescope_archive}"
|
||||
printf '%s %s\n' "${GAMESCOPE_SHA256}" "${gamescope_archive}" | shasum -a 256 -c -
|
||||
gamescope_archive=$(cached "${GAMESCOPE_SHA256}" gamescope.tzst \
|
||||
bash -c 'gh release download "$0" -R "$1" -p gamescope.tzst -O "$out"' "${GAMESCOPE_TAG}" "${github_repo}")
|
||||
zstd -dc "${gamescope_archive}" | tar -xf - -C "${linuxfs_dir}"
|
||||
test -f "${linuxfs_dir}/usr/local/bin/gamescope"
|
||||
fi
|
||||
|
||||
if [[ -f "${repo_root}/tools/wlroots/release.env" ]]; then
|
||||
. "${repo_root}/tools/wlroots/release.env"
|
||||
wlroots_archive="${staging_dir}/wlroots.tzst"
|
||||
gh release download "${WLROOTS_TAG}" -R "${github_repo}" -p wlroots.tzst -O "${wlroots_archive}"
|
||||
printf '%s %s\n' "${WLROOTS_SHA256}" "${wlroots_archive}" | shasum -a 256 -c -
|
||||
wlroots_archive=$(cached "${WLROOTS_SHA256}" wlroots.tzst \
|
||||
bash -c 'gh release download "$0" -R "$1" -p wlroots.tzst -O "$out"' "${WLROOTS_TAG}" "${github_repo}")
|
||||
zstd -dc "${wlroots_archive}" | tar -xf - -C "${linuxfs_dir}"
|
||||
test -f "${linuxfs_dir}/usr/local/lib/droiddeck-wlroots/libwlroots-0.20.so"
|
||||
fi
|
||||
@@ -187,9 +216,8 @@ mango_pkgs="${staging_dir}/mango-pkgs"
|
||||
mkdir -p "${mango_dir}" "${mango_pkgs}"
|
||||
while read -r package_sha256 package_url; do
|
||||
[[ -n "${package_url}" ]] || continue
|
||||
package_archive="${staging_dir}/$(basename "${package_url}")"
|
||||
curl -fsSL --retry 3 -o "${package_archive}" "${package_url}"
|
||||
printf '%s %s\n' "${package_sha256}" "${package_archive}" | shasum -a 256 -c -
|
||||
package_archive=$(cached "${package_sha256}" "$(basename "${package_url}")" \
|
||||
bash -c 'curl -fsSL --retry 3 -o "$out" "$0"' "${package_url}")
|
||||
zstd -dc "${package_archive}" | tar -xf - -C "${mango_pkgs}"
|
||||
done < <(grep -v '^#' "${repo_root}/tools/mangoapp/packages.txt")
|
||||
install -m644 "${mango_pkgs}/usr/bin/mangoapp" "${mango_dir}/mangoapp"
|
||||
@@ -201,11 +229,14 @@ install -m644 "${repo_root}/tools/mangoapp/mangoapp" "${linuxfs_dir}/usr/local/b
|
||||
|
||||
pa_source=${DROIDDECK_PA13_SOURCE_DIR:-"${staging_dir}/pulseaudio-13.0"}
|
||||
if [[ -z "${DROIDDECK_PA13_SOURCE_DIR:-}" ]]; then
|
||||
curl -fsSL -o "${staging_dir}/pulseaudio-13.0.tar.gz" \
|
||||
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
||||
pa_tarball="${cache_dir}/pulseaudio-13.0.tar.gz"
|
||||
if [[ ! -s "${pa_tarball}" ]] || ! tar -tzf "${pa_tarball}" >/dev/null 2>&1; then
|
||||
curl -fsSL -o "${pa_tarball}.part" \
|
||||
https://github.com/pulseaudio/pulseaudio/archive/refs/tags/v13.0.tar.gz
|
||||
mv "${pa_tarball}.part" "${pa_tarball}"
|
||||
fi
|
||||
mkdir -p "${pa_source}"
|
||||
tar -xzf "${staging_dir}/pulseaudio-13.0.tar.gz" \
|
||||
-C "${pa_source}" --strip-components=1
|
||||
tar -xzf "${pa_tarball}" -C "${pa_source}" --strip-components=1
|
||||
fi
|
||||
if [[ ! -f "${pa_source}/src/pulse/version.h.in" ]]; then
|
||||
echo "PulseAudio 13.0 source not found at ${pa_source}; set DROIDDECK_PA13_SOURCE_DIR." >&2
|
||||
@@ -214,7 +245,17 @@ fi
|
||||
|
||||
sink_output="${staging_dir}/sink-out"
|
||||
"${repo_root}/tools/aaudio-sink/build.sh" "${pa_source}" "${sink_output}"
|
||||
"${repo_root}/tools/proot/build.sh" "${repo_root}/app/src/main/jniLibs/arm64-v8a"
|
||||
# proot is rebuilt only when its sources (source.env, the patches, the build script) changed since
|
||||
# the libraries in jniLibs were built.
|
||||
proot_out="${repo_root}/app/src/main/jniLibs/arm64-v8a"
|
||||
proot_inputs=$(cd "${repo_root}/tools/proot" && find . -type f ! -name '*.pyc' | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -d' ' -f1)
|
||||
if [[ -f "${proot_out}/libproot.so" && -f "${proot_out}/libproot-loader.so" \
|
||||
&& "$(cat "${proot_out}/.proot-inputs" 2>/dev/null)" = "${proot_inputs}" ]]; then
|
||||
echo "proot: sources unchanged, keeping ${proot_out}/libproot.so"
|
||||
else
|
||||
"${repo_root}/tools/proot/build.sh" "${proot_out}"
|
||||
echo "${proot_inputs}" > "${proot_out}/.proot-inputs"
|
||||
fi
|
||||
|
||||
cp -p "${bundle_asset}" "${bundle_backup}"
|
||||
bundle_dir="${staging_dir}/pulseaudio-bundle"
|
||||
@@ -229,7 +270,7 @@ install -m755 "${sink_output}/module-aaudio-sink.so" \
|
||||
"${bundle_dir}/modules/arm64/module-aaudio-sink.so"
|
||||
install -m755 "${sink_output}/module-directaudio-sink.so" \
|
||||
"${bundle_dir}/modules/arm64/module-directaudio-sink.so"
|
||||
tar -cf - -C "${bundle_dir}" . | zstd -19 -c > "${staging_dir}/pulseaudio.tzst"
|
||||
tar -cf - -C "${bundle_dir}" . | zstd -19 -T0 -c > "${staging_dir}/pulseaudio.tzst"
|
||||
bundle_replaced=1
|
||||
mv "${staging_dir}/pulseaudio.tzst" "${bundle_asset}"
|
||||
|
||||
|
||||
@@ -75,8 +75,9 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
|
||||
fi
|
||||
# The app's gamescope build (tools/gamescope) takes realtime-priority Vulkan queues on this
|
||||
# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority
|
||||
# where the driver refuses, and the runtime's own build ignores it.
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-1}
|
||||
# where the driver refuses, and the runtime's own build ignores it. Off unless the app's
|
||||
# Performance page turns it on (BL_GAMESCOPE_REALTIME=1).
|
||||
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
|
||||
export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share}
|
||||
export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache}
|
||||
@@ -371,11 +372,18 @@ case $mode in
|
||||
# ~/.droiddeck-fill, and each change goes to gamescope as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on
|
||||
# the root window, which it watches. What gamescope was started with is where this begins.
|
||||
# Not on the desktop (BL_DESKTOP): no gamescope there, and the loop would outlive the client.
|
||||
# The loops below poll for the whole session, and every sleep, cat or script they start is a
|
||||
# fork and exec under proot. They wait with a read timeout on a FIFO nothing writes to and
|
||||
# read files with builtins, so an idle beat costs no process at all.
|
||||
_bl_fifo=$(mktemp -u "${TMPDIR:-/tmp}/bl-nap.XXXXXX") && mkfifo "$_bl_fifo" \
|
||||
&& exec {_bl_nap}<>"$_bl_fifo" && rm -f "$_bl_fifo"
|
||||
nap() { if [ -n "${_bl_nap:-}" ]; then read -r -t "$1" -u "$_bl_nap" _ || true; else sleep "$1"; fi; }
|
||||
[ -z "${BL_DESKTOP:-}" ] && (
|
||||
last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}
|
||||
while :; do
|
||||
sleep 1
|
||||
want=$(cat "$HOME/.droiddeck-fill" 2>/dev/null) || continue
|
||||
nap 1
|
||||
want=
|
||||
read -r want <"$HOME/.droiddeck-fill" 2>/dev/null || [ -n "$want" ] || continue
|
||||
case $want in 0|1) ;; *) continue ;; esac
|
||||
[ "$want" = "$last" ] && continue
|
||||
if xprop -root -f GAMESCOPE_FORCE_WINDOWS_FULLSCREEN 32c -set GAMESCOPE_FORCE_WINDOWS_FULLSCREEN "$want" 2>/dev/null; then
|
||||
@@ -384,14 +392,37 @@ case $mode in
|
||||
fi
|
||||
done
|
||||
) &
|
||||
# Each beat only starts a script when there is something for it: the seeder when a prefix has
|
||||
# a registry it has not marked, the registrar (Python, the heavier start) when a library, the
|
||||
# tools directory or the client's config changed since its last run.
|
||||
(
|
||||
compat_stamp=$HOME/.cache/droiddeck-compat-stamp
|
||||
needs_seed() {
|
||||
local r
|
||||
for r in "$steam_root"/steamapps/compatdata/*/pfx/system.reg \
|
||||
/mnt/bannerlator-sd/steamapps/compatdata/*/pfx/system.reg; do
|
||||
[ -f "$r" ] && [ ! -e "${r%/system.reg}/.bannerlator-redists" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
needs_compat() {
|
||||
local d
|
||||
[ -e "$compat_stamp" ] || return 0
|
||||
for d in "$steam_root/steamapps" "$steam_root/compatibilitytools.d" "$steam_root/config" \
|
||||
/mnt/bannerlator-sd/steamapps; do
|
||||
[[ -e $d && $d -nt $compat_stamp ]] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
beat=0
|
||||
for _ in $(seq 1 720); do
|
||||
sleep 5
|
||||
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
|
||||
while [ "$beat" -lt 720 ]; do
|
||||
nap 5
|
||||
needs_seed && { bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true; }
|
||||
beat=$((beat + 1))
|
||||
if [ "$((beat % 3))" -eq 0 ]; then
|
||||
if [ "$((beat % 3))" -eq 0 ] && needs_compat; then
|
||||
: >"$compat_stamp"
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
: >"$compat_stamp"
|
||||
fi
|
||||
done
|
||||
) &
|
||||
@@ -558,7 +589,7 @@ BUSCONF
|
||||
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.5
|
||||
nap 0.5
|
||||
done
|
||||
) &
|
||||
stop_watcher=$!
|
||||
|
||||
Reference in new issue
Block a user