mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Default Snapdragon 8 Gen 2 core allocation (#654)
This commit is contained in:
1 parent
a7662ae13a
commit
5e5ffee93e
9 files changed
+209
-19
No files matched your search
@@ -0,0 +1,29 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.SocNames
|
||||
|
||||
/** Device defaults only: affinity does not reserve cores or lock their clocks. */
|
||||
object CpuAllocationDefaults {
|
||||
data class Allocation(val client: String, val game: String)
|
||||
|
||||
val device: Allocation? by lazy {
|
||||
forDevice(SocNames.model(), GpuInfo.systemProperty("ro.board.platform"),
|
||||
CpuCores.all.associateWith(CpuCores::maxGhz))
|
||||
}
|
||||
|
||||
/** Select by topology, including faster SM8550 variants; never guess core indices. */
|
||||
fun forDevice(model: String, platform: String, frequencies: Map<Int, Double?>): Allocation? {
|
||||
val chip = model.trim().uppercase().substringBefore('-')
|
||||
val gen2 = chip in setOf("SM8550", "QCS8550") ||
|
||||
(chip.isEmpty() && platform.trim().equals("kalama", ignoreCase = true))
|
||||
if (!gen2 || frequencies.size != 8 || frequencies.keys.any { it < 0 } ||
|
||||
frequencies.values.any { it == null || !it.isFinite() || it <= 0.0 }) return null
|
||||
val clusters = frequencies.entries.groupBy { it.value }.toSortedMap(compareBy { it ?: 0.0 }).values.toList()
|
||||
if (clusters.map { it.size } != listOf(3, 4, 1)) return null
|
||||
return Allocation(
|
||||
client = CpuCores.format(clusters[2].map { it.key }.toSet()),
|
||||
game = CpuCores.format(clusters[1].map { it.key }.toSet()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,10 @@ object CpuCores {
|
||||
return format(if (cores.isEmpty()) all.toSet() else cores)
|
||||
}
|
||||
|
||||
/** An all-core game must undo the affinity inherited from a pinned Steam client. */
|
||||
fun gameLaunchList(list: String, clientPinned: Boolean): String =
|
||||
restrictionOrEmpty(list).ifEmpty { if (clientPinned) listOrAll("") else "" }
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -147,7 +147,7 @@ object DeviceReport {
|
||||
h("Settings in effect")
|
||||
k("Client core override", SessionPrefs.clientCpusOverride(context))
|
||||
k(" client cores", CpuCores.listOrAll(SessionPrefs.clientCpus(context)))
|
||||
k(" game cores", CpuCores.restrictionOrEmpty(SessionPrefs.gameCpus(context)).ifEmpty { "every core (nothing sent)" })
|
||||
k(" game cores", CpuCores.restrictionOrEmpty(SessionPrefs.gameCpus(context)).ifEmpty { "every core" })
|
||||
k("Turnip sysmem", SessionPrefs.tuSysmem(context))
|
||||
k("Zink lazy descriptors", SessionPrefs.zinkLazy(context))
|
||||
k("Threaded GL (glthread)", SessionPrefs.glThread(context))
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.annotation.StringRes
|
||||
import com.droiddeck.launcher.R
|
||||
import com.droiddeck.launcher.core.CpuAllocationDefaults
|
||||
import com.droiddeck.launcher.core.GameEnvironment
|
||||
import com.droiddeck.launcher.core.TextureFiltering
|
||||
import com.droiddeck.launcher.gpu.ScreenEffects
|
||||
@@ -271,26 +272,34 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("wifiDiscoveryAsked", true).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)
|
||||
internal data class CoreAllocation(val clientOverride: Boolean, val clientCpus: String, val gameCpus: String)
|
||||
|
||||
/** Defaults are read only for absent keys; even a saved empty/all-core choice wins. */
|
||||
internal fun coreAllocation(context: Context, defaults: CpuAllocationDefaults.Allocation? = CpuAllocationDefaults.device): CoreAllocation {
|
||||
val saved = prefs(context)
|
||||
return CoreAllocation(
|
||||
saved.getBoolean("clientCpusOverride", defaults != null && !saved.contains("clientCpus")),
|
||||
saved.getString("clientCpus", defaults?.client.orEmpty()).orEmpty(),
|
||||
saved.getString("gameCpus", defaults?.game.orEmpty()).orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
/** Use the device default until the user saves an override or a client mask. */
|
||||
fun clientCpusOverride(context: Context): Boolean = coreAllocation(context).clientOverride
|
||||
|
||||
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 clientCpus(context: Context): String = coreAllocation(context).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", "") ?: ""
|
||||
/** Comma-separated core list for games (taskset -c syntax); "" = every core (explicitly restored when the client is pinned). */
|
||||
fun gameCpus(context: Context): String = coreAllocation(context).gameCpus
|
||||
|
||||
fun setGameCpus(context: Context, list: String) {
|
||||
prefs(context).edit().putString("gameCpus", list).apply()
|
||||
|
||||
@@ -666,16 +666,15 @@ class SessionService : Service() {
|
||||
extraEnv().forEach { guest.add(it) }
|
||||
// The agent bridge's experiment lines (AgentEnv), after the user's so an agent's win.
|
||||
com.droiddeck.launcher.agent.AgentEnv.beginSession(this).forEach { guest.add(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 (steamHere && SessionPrefs.clientCpusOverride(this)) {
|
||||
// A game launched by pinned Steam inherits its mask. Explicitly restore all game cores
|
||||
// when requested, as well as applying restricted game choices in the Proton wrapper.
|
||||
val clientPinned = steamHere && SessionPrefs.clientCpusOverride(this)
|
||||
if (clientPinned) {
|
||||
guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this)))
|
||||
}
|
||||
// The game cores also pin the desktop and a program from the rail; without a choice the
|
||||
// session script picks every core but the slowest cluster for those (program_cores).
|
||||
CpuCores.restrictionOrEmpty(SessionPrefs.gameCpus(this))
|
||||
CpuCores.gameLaunchList(SessionPrefs.gameCpus(this), clientPinned)
|
||||
.takeIf { it.isNotEmpty() }?.let { guest.add("BL_GAME_CPUS=$it") }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.droiddeck.launcher.core
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
||||
class CpuAllocationDefaultsTest {
|
||||
private val thor = (0..7).associateWith { when (it) { in 0..2 -> 2.016; in 3..6 -> 2.8032; else -> 3.1872 } }
|
||||
|
||||
@Test fun gen2UsesTheFourMiddleCoresAndOnePrime() {
|
||||
for (model in listOf("SM8550", "QCS8550", "sm8550-ac")) {
|
||||
assertEquals(CpuAllocationDefaults.Allocation("7", "3,4,5,6"), CpuAllocationDefaults.forDevice(model, "", thor))
|
||||
}
|
||||
assertEquals(CpuAllocationDefaults.Allocation("7", "3,4,5,6"), CpuAllocationDefaults.forDevice("", "kalama", thor))
|
||||
}
|
||||
|
||||
@Test fun coreNumbersAndClockVariantsAreDetectedInsteadOfHardcoded() {
|
||||
val reordered = mapOf(0 to 3.36, 1 to 2.0, 2 to 2.0, 3 to 2.0, 4 to 2.8, 5 to 2.8, 6 to 2.8, 7 to 2.8)
|
||||
assertEquals(CpuAllocationDefaults.Allocation("0", "4,5,6,7"), CpuAllocationDefaults.forDevice("SM8550-AC", "kalama", reordered))
|
||||
}
|
||||
|
||||
@Test fun anotherOrUnknownChipDoesNotAcquireThisDefault() {
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8650", "pineapple", thor))
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8650", "kalama", thor))
|
||||
assertNull(CpuAllocationDefaults.forDevice("", "", thor))
|
||||
}
|
||||
|
||||
@Test fun missingOrUnexpectedTopologyKeepsTheSchedulerDefault() {
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8550", "kalama", thor - 7))
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8550", "kalama", thor + (7 to null)))
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8550", "kalama", thor + (7 to 0.0)))
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8550", "kalama", thor + (7 to Double.NaN)))
|
||||
assertNull(CpuAllocationDefaults.forDevice("SM8550", "kalama", (0..7).associateWith { 2.8 }))
|
||||
}
|
||||
|
||||
@Test fun allCoreGamesUndoTheClientsInheritedRestriction() {
|
||||
val all = CpuCores.listOrAll("")
|
||||
assertEquals(all, CpuCores.gameLaunchList("", clientPinned = true))
|
||||
assertEquals(all, CpuCores.gameLaunchList(all, clientPinned = true))
|
||||
assertEquals("", CpuCores.gameLaunchList("", clientPinned = false))
|
||||
assertEquals("0", CpuCores.gameLaunchList("0", clientPinned = true))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import com.droiddeck.launcher.core.CpuAllocationDefaults
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [33])
|
||||
class CoreAllocationPrefsTest {
|
||||
private val app get() = RuntimeEnvironment.getApplication()
|
||||
private val defaults = CpuAllocationDefaults.Allocation("7", "3,4,5,6")
|
||||
private val saved get() = app.getSharedPreferences("session", Context.MODE_PRIVATE)
|
||||
|
||||
@Before fun clear() { saved.edit().clear().commit() }
|
||||
|
||||
@Test fun absentPreferencesUseTheDeviceDefaultWithoutWritingIt() {
|
||||
assertEquals(SessionPrefs.CoreAllocation(true, "7", "3,4,5,6"), SessionPrefs.coreAllocation(app, defaults))
|
||||
assertTrue(saved.all.isEmpty())
|
||||
assertEquals(SessionPrefs.CoreAllocation(false, "", ""), SessionPrefs.coreAllocation(app, null))
|
||||
}
|
||||
|
||||
@Test fun explicitDisableAndAllCoreChoicesSurviveAnUpdate() {
|
||||
saved.edit().putBoolean("clientCpusOverride", false).putString("clientCpus", "").putString("gameCpus", "").commit()
|
||||
assertEquals(SessionPrefs.CoreAllocation(false, "", ""), SessionPrefs.coreAllocation(app, defaults))
|
||||
}
|
||||
|
||||
@Test fun existingCustomMasksArePreserved() {
|
||||
saved.edit().putBoolean("clientCpusOverride", true).putString("clientCpus", "0,1").putString("gameCpus", "2,7").commit()
|
||||
assertEquals(SessionPrefs.CoreAllocation(true, "0,1", "2,7"), SessionPrefs.coreAllocation(app, defaults))
|
||||
}
|
||||
|
||||
@Test fun anOldClientMaskIsNotEnabledWithoutAnOverrideChoice() {
|
||||
saved.edit().putString("clientCpus", "0,1").commit()
|
||||
assertEquals(SessionPrefs.CoreAllocation(false, "0,1", "3,4,5,6"), SessionPrefs.coreAllocation(app, defaults))
|
||||
}
|
||||
|
||||
@Test fun changingOneChoiceDoesNotReplaceTheOtherDefaults() {
|
||||
SessionPrefs.setGameCpus(app, "0,1,2,3,4,5,6,7")
|
||||
assertEquals(SessionPrefs.CoreAllocation(true, "7", "0,1,2,3,4,5,6,7"), SessionPrefs.coreAllocation(app, defaults))
|
||||
SessionPrefs.setClientCpusOverride(app, false)
|
||||
assertEquals(SessionPrefs.CoreAllocation(false, "7", "0,1,2,3,4,5,6,7"), SessionPrefs.coreAllocation(app, defaults))
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,23 @@ export XDG_DATA_DIRS=$XDG_DATA_HOME/flatpak/exports/share:${XDG_DATA_DIRS:-/usr/
|
||||
# program from the rail): the user's game cores when set (BL_GAME_CPUS), else every core but the
|
||||
# slowest cluster - Android is free to park an emulator's threads on the little cores, and those
|
||||
# are what compile shaders and recompile PPU/SPU code. BL_PIN_CORES=0 leaves the scheduler alone.
|
||||
# A prime-only client mask can be reset without a thread-count change (Steam or Android).
|
||||
# Read status with shell builtins; only invoke taskset when a single-core mask has drifted.
|
||||
client_single_core_matches() {
|
||||
local wanted=$1 task key cpus
|
||||
shift
|
||||
[[ $wanted =~ ^[0-9]+$ ]] || return 0
|
||||
for task in "$@"; do
|
||||
[ -r "$task/status" ] || continue
|
||||
while read -r key cpus; do
|
||||
[ "$key" = Cpus_allowed_list: ] || continue
|
||||
[ "$cpus" = "$wanted" ] || return 1
|
||||
break
|
||||
done < "$task/status" 2>/dev/null
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
program_cores() {
|
||||
[ "${BL_PIN_CORES:-1}" = 0 ] && return 0
|
||||
command -v taskset >/dev/null 2>&1 || return 0
|
||||
@@ -445,8 +462,8 @@ case $mode in
|
||||
# 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. The beat reads /proc
|
||||
# itself instead of running pgrep, and only runs taskset for a process that is new or has
|
||||
# grown threads since it was last pinned: every fork is a round of traced syscalls, and a
|
||||
# itself instead of running pgrep, and runs taskset for new threads or a single-core mask
|
||||
# that has drifted since it was last pinned: every fork is a round of traced syscalls, and a
|
||||
# beat that forked a taskset per process every 5 s did that for an hour. taskset -a, because
|
||||
# without it only a process's main thread moves and the renderer's workers stay where they were.
|
||||
if [ -n "${BL_CLIENT_CPUS:-}" ]; then
|
||||
@@ -459,7 +476,9 @@ case $mode in
|
||||
case $_c in *steamwebhelper*|*steamrtarm64/steam|*gamescope*) ;; *) continue ;; esac
|
||||
_t=("$_d"/task/*)
|
||||
_p=${_d#/proc/}
|
||||
[ "${_bl_pinned[$_p]:-}" = "${#_t[@]}" ] && continue
|
||||
if [ "${_bl_pinned[$_p]:-}" = "${#_t[@]}" ] && client_single_core_matches "$BL_CLIENT_CPUS" "${_t[@]}"; then
|
||||
continue
|
||||
fi
|
||||
taskset -a -cp "$BL_CLIENT_CPUS" "$_p" >/dev/null 2>&1 || true
|
||||
_bl_pinned[$_p]=${#_t[@]}
|
||||
done
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Single-core Steam affinity is rechecked even if its thread count stayed the same."""
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'linuxfs/overlay/usr/local/bin/droiddeck-session'
|
||||
SOURCE = SCRIPT.read_text()
|
||||
FUNCTION = SOURCE[SOURCE.index('client_single_core_matches() {'):].split('\n}\n', 1)[0] + '\n}\n'
|
||||
|
||||
|
||||
class ClientCoreMaskTest(unittest.TestCase):
|
||||
def matches(self, wanted, tasks):
|
||||
return subprocess.run(['bash', '-c', FUNCTION + '\nclient_single_core_matches "$@"',
|
||||
'test', wanted, *map(str, tasks)], capture_output=True).returncode
|
||||
|
||||
def test_a_mask_change_without_a_new_thread_needs_repinning(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tasks = [Path(tmp) / str(i) for i in (1, 2)]
|
||||
for task in tasks:
|
||||
task.mkdir()
|
||||
(task / 'status').write_text('Name:\tsteam\nCpus_allowed_list:\t7\n')
|
||||
self.assertEqual(self.matches('7', tasks), 0)
|
||||
(tasks[1] / 'status').write_text('Name:\tsteam\nCpus_allowed_list:\t0-7\n')
|
||||
self.assertEqual(self.matches('7', tasks), 1)
|
||||
|
||||
def test_an_exited_thread_does_not_prevent_checking_live_threads(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
live = Path(tmp) / '1'
|
||||
live.mkdir()
|
||||
(live / 'status').write_text('Cpus_allowed_list:\t3-6\n')
|
||||
self.assertEqual(self.matches('7', [Path(tmp) / 'gone', live]), 1)
|
||||
|
||||
def test_multi_core_masks_keep_the_existing_thread_count_policy(self):
|
||||
self.assertEqual(self.matches('0,1,2,3', []), 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user