mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Performance: the client and game core masks, from Bannerlator's Linux session
A Steam session runs two things that want different cores at the same time, and one affinity cannot serve both (Bannerlator cfca3912). The client is the case that prompted it: Steam pins its own interface renderer to a subset of cores it chooses - five of eight on one device, leaving out both little cores and the fastest one - reasonable while a game is running and wrong when the client is all there is, where it shows as Big Picture feeling sluggish. BL_CLIENT_CPUS overrides it, re-applied on a beat by the session script because steamwebhelper spawns children continuously and each inherits Steam's choice. That block is Bannerlator's, verbatim. One deliberate difference: the client's mask is sent whenever the override is on, even when it names every core, because undoing Steam's pin is the whole point and the scheduler's default is exactly what that pin takes away. A game gets BL_GAME_CPUS instead, consumed by the Proton wrappers that r27 already carries, and as in Bannerlator it is sent only when it is a real restriction: a list naming every core is what the scheduler does unaided. Both live behind a Performance dialog with a checkbox per core, labelled with its ceiling, and apply at the next session start.
This commit is contained in:
1 parent
fbe3737dc4
commit
beab4e7849
8 files changed
+242
No files matched your search
@@ -196,6 +196,24 @@ staged into the runtime at every launch like the session scripts, so a fix reach
|
||||
runtime without re-hosting it. Proven in Bannerlator's Linux session with voice in Steam's own
|
||||
tester; ⚠️ not yet exercised in this app.
|
||||
|
||||
## Cores
|
||||
|
||||
**Performance** on the main screen carries the two core masks Bannerlator's Linux session has, kept
|
||||
separate because they are wanted at the same time and suit different things:
|
||||
|
||||
- **Steam client cores** — Steam pins its own interface renderer to a subset of cores it chooses (on
|
||||
one device 5 of 8, leaving out both little cores and the fastest), which is reasonable while a
|
||||
game runs and makes Big Picture sluggish when the client is all there is. Turning the override
|
||||
on pins the client, its UI helper and gamescope to the cores you tick, re-applied every few
|
||||
seconds because the UI keeps spawning helpers that inherit Steam's choice. Every core ticked is
|
||||
the usual fix — and, unlike the game mask, it *is* sent when it names every core, since undoing
|
||||
Steam's pin is the whole point.
|
||||
- **Game cores** — applied by the Proton wrapper, which execs the game through `taskset` so every
|
||||
thread inherits the mask from its first instruction. Leaving every core ticked sends nothing;
|
||||
untick the small cores to keep a heavy game off them.
|
||||
|
||||
Both apply at the next session start.
|
||||
|
||||
## Compatibility tools
|
||||
|
||||
**Compatibility tools** (main screen and drawer) installs GE-Proton or proton-cachyos — native
|
||||
|
||||
@@ -33,7 +33,10 @@ import com.steamdeck.launcher.session.ProtonExtras
|
||||
import com.steamdeck.launcher.session.SessionPrefs
|
||||
import com.steamdeck.launcher.ui.ProtonDialog
|
||||
import com.steamdeck.launcher.ui.ProtonRow
|
||||
import com.steamdeck.launcher.core.CpuCores
|
||||
import com.steamdeck.launcher.ui.AudioDialog
|
||||
import com.steamdeck.launcher.ui.CoreRow
|
||||
import com.steamdeck.launcher.ui.PerformanceDialog
|
||||
import com.steamdeck.launcher.ui.DriverDialog
|
||||
import com.steamdeck.launcher.ui.DriverRow
|
||||
import com.steamdeck.launcher.ui.ConfirmDialog
|
||||
@@ -75,6 +78,10 @@ class MainActivity : ComponentActivity() {
|
||||
private var protonRows by mutableStateOf<List<ProtonRow>>(emptyList())
|
||||
private var showDrivers by mutableStateOf(false)
|
||||
private var showAudio by mutableStateOf(false)
|
||||
private var showPerformance by mutableStateOf(false)
|
||||
private var clientOverride by mutableStateOf(false)
|
||||
private var clientCores by mutableStateOf<Set<Int>>(emptySet())
|
||||
private var gameCores by mutableStateOf<Set<Int>>(emptySet())
|
||||
private var directAudio by mutableStateOf(false)
|
||||
private var mic by mutableStateOf(false)
|
||||
private var linuxRows by mutableStateOf<List<DriverRow>>(emptyList())
|
||||
@@ -121,6 +128,7 @@ class MainActivity : ComponentActivity() {
|
||||
onProtons = { refreshProtons(); showProtons = true },
|
||||
onDrivers = { refreshDrivers(); showDrivers = true },
|
||||
onAudio = { directAudio = SessionPrefs.directAudio(this); mic = SessionPrefs.micEnabled(this); showAudio = true },
|
||||
onPerformance = { refreshCores(); showPerformance = true },
|
||||
onCredits = { showCredits = true },
|
||||
)
|
||||
if (showApps) DesktopAppsDialog(
|
||||
@@ -161,6 +169,20 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onDismiss = { showAudio = false },
|
||||
)
|
||||
if (showPerformance) PerformanceDialog(
|
||||
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,
|
||||
onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on },
|
||||
onClientCore = { core, on ->
|
||||
clientCores = if (on) clientCores + core else clientCores - core
|
||||
SessionPrefs.setClientCpus(this, CpuCores.format(clientCores))
|
||||
},
|
||||
onGameCore = { core, on ->
|
||||
gameCores = if (on) gameCores + core else gameCores - core
|
||||
SessionPrefs.setGameCpus(this, CpuCores.format(gameCores))
|
||||
},
|
||||
onDismiss = { showPerformance = false },
|
||||
)
|
||||
if (showRemove) ConfirmDialog(
|
||||
title = "Remove Linux runtime",
|
||||
text = "This deletes the runtime, the Steam client inside it, and every game installed there.",
|
||||
@@ -293,6 +315,13 @@ class MainActivity : ComponentActivity() {
|
||||
null
|
||||
}
|
||||
|
||||
/** The two masks as the dialog shows them; an empty stored list shows as every core ticked. */
|
||||
private fun refreshCores() {
|
||||
clientOverride = SessionPrefs.clientCpusOverride(this)
|
||||
clientCores = CpuCores.parse(SessionPrefs.clientCpus(this)).ifEmpty { CpuCores.all.toSet() }
|
||||
gameCores = CpuCores.parse(SessionPrefs.gameCpus(this)).ifEmpty { CpuCores.all.toSet() }
|
||||
}
|
||||
|
||||
private fun refreshProtons() {
|
||||
protonRows = ProtonExtras.tools.map { ProtonRow(it.id, it.name, ProtonExtras.installed(this, it), ProtonExtras.queued(this, it)) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.steamdeck.launcher.core
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* The device's cores as taskset names them, for the two masks a Steam session can carry.
|
||||
*
|
||||
* A list is stored the way taskset's `-c` takes it - comma-separated core numbers - so what the
|
||||
* dialog produces is what the session script and the Proton wrappers consume unchanged.
|
||||
*/
|
||||
object CpuCores {
|
||||
/** Core numbers present on this device, in order. */
|
||||
val all: List<Int> by lazy { (0 until Runtime.getRuntime().availableProcessors()).toList() }
|
||||
|
||||
/** The core's ceiling in GHz for the dialog's labels, or null where the kernel hides it. */
|
||||
fun maxGhz(core: Int): Double? =
|
||||
FileUtils.readString(File("/sys/devices/system/cpu/cpu$core/cpufreq/cpuinfo_max_freq"))
|
||||
?.trim()?.toLongOrNull()?.let { it / 1_000_000.0 }
|
||||
|
||||
fun parse(list: String): Set<Int> =
|
||||
list.split(',').mapNotNull { it.trim().toIntOrNull() }.filter { it in all }.toSet()
|
||||
|
||||
fun format(cores: Set<Int>): String = cores.sorted().joinToString(",")
|
||||
|
||||
/** The stored list, or every core when nothing (or nothing valid) is stored. */
|
||||
fun listOrAll(list: String): String {
|
||||
val cores = parse(list)
|
||||
return format(if (cores.isEmpty()) all.toSet() else cores)
|
||||
}
|
||||
|
||||
/**
|
||||
* The list only when it is a real restriction, else "". Empty, absent, or naming every core
|
||||
* all mean "no preference": pinning a process to all cores is what the scheduler does
|
||||
* unaided, and passing it would only put a meaningless line in the session log. Bannerlator's
|
||||
* rule, kept for the game mask.
|
||||
*/
|
||||
fun restrictionOrEmpty(list: String): String {
|
||||
val cores = parse(list)
|
||||
return if (cores.isEmpty() || cores.size >= all.size) "" else format(cores)
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,31 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("mic", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the client's own core pick is overridden. When on, BL_CLIENT_CPUS is sent even when
|
||||
* it names every core - unlike a game mask, the point here is to undo a pin Steam applies to
|
||||
* itself, and the scheduler's default is exactly what Steam's choice takes away.
|
||||
*/
|
||||
fun clientCpusOverride(context: Context): Boolean = prefs(context).getBoolean("clientCpusOverride", false)
|
||||
|
||||
fun setClientCpusOverride(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("clientCpusOverride", on).apply()
|
||||
}
|
||||
|
||||
/** Comma-separated core list for the client (taskset -c syntax); "" = every core. */
|
||||
fun clientCpus(context: Context): String = prefs(context).getString("clientCpus", "") ?: ""
|
||||
|
||||
fun setClientCpus(context: Context, list: String) {
|
||||
prefs(context).edit().putString("clientCpus", list).apply()
|
||||
}
|
||||
|
||||
/** Comma-separated core list for games (taskset -c syntax); "" = every core = nothing sent. */
|
||||
fun gameCpus(context: Context): String = prefs(context).getString("gameCpus", "") ?: ""
|
||||
|
||||
fun setGameCpus(context: Context, list: String) {
|
||||
prefs(context).edit().putString("gameCpus", list).apply()
|
||||
}
|
||||
|
||||
/** The Android driver the compositor loads: "" = pick by GPU, else a bundled or imported id. */
|
||||
@JvmStatic
|
||||
fun androidDriver(context: Context): String = prefs(context).getString("androidDriver", "") ?: ""
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.steamdeck.launcher.R
|
||||
import com.steamdeck.launcher.SessionActivity
|
||||
import com.steamdeck.launcher.audio.DirectAudioRelayComponent
|
||||
import com.steamdeck.launcher.audio.PulseAudioComponent
|
||||
import com.steamdeck.launcher.core.CpuCores
|
||||
import com.steamdeck.launcher.core.EnvVars
|
||||
import com.steamdeck.launcher.core.EnvironmentComponent
|
||||
import com.steamdeck.launcher.core.FileUtils
|
||||
@@ -134,6 +135,17 @@ class SessionService : Service() {
|
||||
// imported driver from the A710/A720/A722 legs gets "sysmem" on its own, which is what both
|
||||
// its authors advise for those GPUs and what nothing else in the list needs.
|
||||
tuDebug(linuxDriverId)?.let { guest.add("TU_DEBUG=$it") }
|
||||
// Core masks, Bannerlator's two (cfca3912). The client's is sent whenever the override is
|
||||
// on, even naming every core: it exists to undo the pin Steam applies to its own interface
|
||||
// renderer, and the scheduler's default is exactly what that pin takes away. A game's is
|
||||
// sent only when it is a real restriction - a game has no pin of its own to undo.
|
||||
if (SessionState.mode == MODE_STEAM) {
|
||||
if (SessionPrefs.clientCpusOverride(this)) {
|
||||
guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this)))
|
||||
}
|
||||
CpuCores.restrictionOrEmpty(SessionPrefs.gameCpus(this))
|
||||
.takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") }
|
||||
}
|
||||
|
||||
// PulseAudio always: the client is a native Linux program and has no other way to make a
|
||||
// sound - its menus, its music and its voice chat all go through here. DirectAudio is not
|
||||
|
||||
@@ -68,6 +68,7 @@ fun MainScreen(
|
||||
onProtons: () -> Unit,
|
||||
onDrivers: () -> Unit,
|
||||
onAudio: () -> Unit,
|
||||
onPerformance: () -> Unit,
|
||||
onOffline: () -> Unit,
|
||||
onCredits: () -> Unit,
|
||||
) {
|
||||
@@ -187,6 +188,10 @@ fun MainScreen(
|
||||
onClick = onAudio,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp * k).height(buttonHeight + 5.dp * k),
|
||||
) { Text("Audio", fontSize = 13.sp * k) }
|
||||
OutlinedButton(
|
||||
onClick = onPerformance,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 5.dp * k).height(buttonHeight + 5.dp * k),
|
||||
) { Text("Performance", fontSize = 13.sp * k) }
|
||||
Spacer(Modifier.height(10.dp * k))
|
||||
Text(
|
||||
"by The412Banner and maxjivi05 · credits",
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.steamdeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/** One core as the dialog labels it: its number and, where known, its ceiling. */
|
||||
class CoreRow(val core: Int, val label: String)
|
||||
|
||||
/**
|
||||
* The two core masks a Steam session carries, kept separate because they are wanted at the same
|
||||
* time and suit different things: the client's menus and a game each get their own. Both apply
|
||||
* at the next session start; the game's is applied by the Proton wrapper at each launch.
|
||||
*/
|
||||
@Composable
|
||||
fun PerformanceDialog(
|
||||
cores: List<CoreRow>,
|
||||
clientOverride: Boolean,
|
||||
clientCores: Set<Int>,
|
||||
gameCores: Set<Int>,
|
||||
onClientOverride: (Boolean) -> Unit,
|
||||
onClientCore: (Int, Boolean) -> Unit,
|
||||
onGameCore: (Int, Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text("Performance") },
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
Text("Steam client cores", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"Steam pins its own interface to a subset of cores it chooses — on one device 5 of 8, leaving out the fastest — " +
|
||||
"which suits a running game and makes the menus sluggish when the client is all there is. Turning this on " +
|
||||
"pins the client, its UI helper and gamescope to the cores ticked below instead, re-applied every few seconds " +
|
||||
"because the UI keeps spawning helpers that inherit Steam's choice. Every core ticked is the usual fix.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||
Text("Override Steam's own core choice", style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Switch(checked = clientOverride, onCheckedChange = onClientOverride)
|
||||
}
|
||||
CoreGrid(cores, clientCores, enabled = clientOverride, onToggle = onClientCore)
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(Modifier.height(10.dp))
|
||||
|
||||
Text("Game cores", style = MaterialTheme.typography.titleSmall)
|
||||
Text(
|
||||
"The cores a game the client launches may run on, applied by exec'ing it through taskset so every thread " +
|
||||
"inherits the mask from its first instruction. Leaving every core ticked sends nothing — that is what the " +
|
||||
"scheduler does unaided. Untick the small cores to keep a heavy game off them.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
CoreGrid(cores, gameCores, enabled = true, onToggle = onGameCore)
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(onClick = onDismiss) { Text("OK") } },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoreGrid(cores: List<CoreRow>, selected: Set<Int>, enabled: Boolean, onToggle: (Int, Boolean) -> Unit) {
|
||||
for (row in cores.chunked(2)) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
for (c in row) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
Checkbox(checked = c.core in selected, enabled = enabled, onCheckedChange = { onToggle(c.core, it) })
|
||||
Text(c.label, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
if (row.size == 1) Spacer(Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,6 +186,23 @@ case $mode in
|
||||
done < "$HOME/.bl-steam-urls"
|
||||
rm -f "$HOME/.bl-steam-urls"
|
||||
fi
|
||||
# Cores for the client itself. Steam pins its own interface renderer to a subset - on the
|
||||
# device it took 5 of 8, leaving out both little cores and the fastest one - which is a
|
||||
# reasonable thing to do while a game is running and the wrong thing when the client is all
|
||||
# there is. BL_CLIENT_CPUS overrides it. Re-applied on a beat because steamwebhelper spawns
|
||||
# children continuously and each one inherits Steam's choice, not ours.
|
||||
if [ -n "${BL_CLIENT_CPUS:-}" ]; then
|
||||
step "client cores: $BL_CLIENT_CPUS"
|
||||
(
|
||||
for _ in $(seq 1 720); do
|
||||
for _p in $(pgrep -f 'steamwebhelper|steamrtarm64/steam|gamescope' 2>/dev/null); do
|
||||
taskset -cp "$BL_CLIENT_CPUS" "$_p" >/dev/null 2>&1 || true
|
||||
done
|
||||
sleep 5
|
||||
done
|
||||
) &
|
||||
fi
|
||||
|
||||
step "registering the app's game library with Steam"
|
||||
bannerlator-steam-library "$steam_root" || true
|
||||
step "setting up the ARM64 Proton compatibility tool"
|
||||
|
||||
Reference in new issue
Block a user