Merge branch 'main' into upscaler-setting

This commit is contained in:
Magarto authored and GitHub committed 2026-10-01 22:02:07 +02:00
commit 074e4f090b
16 files changed
+959 -17

No files matched your search

+5 -1
View File
@@ -208,9 +208,13 @@ jobs:
rm mango-pkgs/pkg.tar.zst
done
install -m644 mango-pkgs/usr/bin/mangoapp "$M/mangoapp"
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1 libtracefs.so.1; do
for so in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
cp -L "mango-pkgs/usr/lib/$so" "$M/$so"
done
# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c).
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
-o "$M/libtracefs.so.1" tools/mangoapp/libtracefs-shim.c
aarch64-linux-gnu-strip --strip-unneeded "$M/libtracefs.so.1"
install -Dm644 tools/mangoapp/mangoapp "$D/usr/local/bin/mangoapp"
rm -rf mango-pkgs
ls -l "$M"
@@ -47,7 +47,7 @@ object GuestCommand {
.forEach { cmd.add("-b"); cmd.add(it) }
cmd += listOf(
"/usr/bin/env", "-i", "HOME=/root", "USER=root", "LANG=C.UTF-8",
"PATH=/usr/local/bin:/usr/bin:/bin", "XDG_RUNTIME_DIR=${runtimeDir.path}",
"PATH=/usr/local/bin:/usr/bin:/bin", "XDG_RUNTIME_DIR=${LinuxRuntime.GUEST_RUNTIME_DIR}",
"XDG_DATA_HOME=/root/.local/share", "FLATPAK_BWRAP=${FlatpakManager.BWRAP}",
"XDG_DATA_DIRS=/root/.local/share/flatpak/exports/share:/usr/local/share:/usr/share",
)
@@ -34,6 +34,14 @@ public final class LinuxRuntime {
/** Shortcut extra naming which of the modes above a Linux entry launches. */
public static final String EXTRA_LINUX_MODE = "linux_mode";
private static final String KGSL_DEVICE = "/dev/kgsl-3d0";
/**
* Where the guest sees a command's XDG_RUNTIME_DIR. A Unix socket's path must fit in 108 bytes,
* and libwayland checks the guest's path before proot ever sees it: with the app on an SD card
* adopted as internal storage the files directory is /mnt/expand/<uuid>/user/0/<package>/files,
* and gamescope-0 under it is one byte over, so gamescope could not open its socket and the
* session ended at once. A long host path behind it is proot's to shorten.
*/
public static final String GUEST_RUNTIME_DIR = "/run/droiddeck";
/** Where every Linux session's debug log lands: public, so a user can just hand the folder over. */
public static final String DEBUG_LOG_DIR = "DroidDeck";
@@ -185,7 +193,10 @@ public final class LinuxRuntime {
bind(cmd, new File(root, "etc/bannerlator/empty").getPath() + ":/sys/fs/selinux");
bind(cmd, context.getFilesDir().getPath());
bind(cmd, context.getCacheDir().getPath());
if (runtimeDir != null) bind(cmd, runtimeDir.getPath());
if (runtimeDir != null) {
bind(cmd, runtimeDir.getPath());
bind(cmd, runtimeDir.getPath() + ":" + GUEST_RUNTIME_DIR);
}
if (sessionRoot != null) bind(cmd, sessionRoot.getPath());
if (externalStorage != null && externalStorage.isDirectory()) {
bind(cmd, externalStorage.getPath());
@@ -231,12 +242,15 @@ public final class LinuxRuntime {
};
for (String[] entry : procFiles) {
File fake = new File(fakeProc, entry[0]);
if (fake.isFile() && !new File(entry[1]).canRead()) {
// A live stand-in the session binds itself (CpuStatComponent's /proc/stat) wins.
boolean live = false;
if (extraBinds != null) for (String spec : extraBinds) live |= spec.endsWith(":" + entry[1]);
if (!live && fake.isFile() && !new File(entry[1]).canRead()) {
bind(cmd, fake.getPath() + ":" + entry[1]);
}
}
bindGpuNode(context, cmd);
bindAdrenoStats(cmd);
bindAdrenoStats(cmd, extraBinds);
bindCpuTemps(cmd, root);
if (extraBinds != null) {
for (String spec : extraBinds) bind(cmd, spec);
@@ -282,12 +296,71 @@ public final class LinuxRuntime {
if (!Files.isSymbolicLink(subsystem.toPath())) {
Os.symlink("/sys/bus/platform", subsystem.getPath());
}
// What MangoHud names a GPU's driver by; msm_drm is how an Adreno's render node reads
// on a mainline kernel, and the driver it reads the load of (bindAdrenoStats).
File driver = new File(device, "driver");
if (!Files.isSymbolicLink(driver.toPath())) {
Os.symlink("/sys/bus/platform/drivers/msm_drm", driver.getPath());
}
} catch (IOException | ErrnoException e) {
return;
}
bind(cmd, new File(base, "sys").getPath() + ":/sys/dev/char");
bind(cmd, dri.getPath() + ":/dev/dri");
bind(cmd, KGSL_DEVICE + ":/dev/dri/" + node);
bindDrmClass(base, cmd, node, major + ":" + minor);
}
/**
* MangoHud (Deck mode's performance overlay, mangoapp) finds the GPU by listing
* {@code /sys/class/drm}, and an exception from that listing ends the process: under an
* enforcing SELinux policy - every retail phone - an app may not list it, so mangoapp died on
* start, gamescope restarted it until the session gave up, and there was no overlay. Where the
* app cannot list it, the session's {@code /sys/class/drm} is ours: the render node above,
* named as an Adreno's is. MangoHud then reads the GPU's stats from KGSL's sysfs, which such a
* device refuses too - SessionService's GpuStatsComponent stands in for that. Where the
* listing is allowed it is left alone.
*/
private static void bindDrmClass(File base, List<String> cmd, String node, String devNumbers) {
if (new File("/sys/class/drm").list() != null) return;
File drmClass = new File(base, "class");
try {
if (!drmClass.isDirectory() && !drmClass.mkdirs()) return;
// Only symlinks are ever made here; deleting them must not follow one into its target.
File[] old = drmClass.listFiles();
if (old != null) for (File f : old) Files.deleteIfExists(f.toPath());
Os.symlink("/sys/dev/char/" + devNumbers, new File(drmClass, node).getPath());
} catch (IOException | ErrnoException e) {
return;
}
bind(cmd, drmClass.getPath() + ":/sys/class/drm");
android.util.Log.i("LinuxRuntime", "hud: /sys/class/drm is not listable here; the session's lists " + node);
// Also refused to list, also asked for by MangoHud twice a second - an error line each
// time in the session log - and holding nothing it reads on an Adreno phone.
if (new File("/sys/class/powercap").list() == null) {
File empty = new File(base, "empty-powercap");
if (empty.isDirectory() || empty.mkdirs()) bind(cmd, empty.getPath() + ":/sys/class/powercap");
}
}
/** The temperature of a CPU zone the app may read (the first by name), or null. */
public static String cpuTempSource() {
for (java.util.Map.Entry<String, File> e : new java.util.TreeMap<>(thermalZones()).entrySet()) {
File temp = new File(e.getValue(), "temp");
if (e.getKey().contains("cpu") && !e.getKey().contains("gpu") && temp.canRead()) return temp.getPath();
}
return null;
}
/** The file the GPU's temperature reads from, or null where the app may read none. */
public static String gpuTempSource() {
String kgsl = "/sys/class/kgsl/kgsl-3d0/";
return firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu"));
}
private static String gpuLoadSource() {
String kgsl = "/sys/class/kgsl/kgsl-3d0/";
return firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load");
}
/**
@@ -298,11 +371,11 @@ public final class LinuxRuntime {
* Android PC emulators (GameNative, Winlator forks) read them in. A value found nowhere is left
* alone.
*/
private static void bindAdrenoStats(List<String> cmd) {
private static void bindAdrenoStats(List<String> cmd, List<String> extraBinds) {
String kgsl = "/sys/class/kgsl/kgsl-3d0/";
String gpuTemp = firstReadable(kgsl + "temp", kgsl + "devfreq/temp", thermalZone("gpu"));
String gpuTemp = gpuTempSource();
String[][] stats = {
{firstReadable(kgsl + "gpu_busy_percentage", kgsl + "devfreq/gpu_load"), "/sys/kernel/debug/dri/0/perf_now"},
{gpuLoadSource(), "/sys/kernel/debug/dri/0/perf_now"},
{firstReadable(kgsl + "devfreq/cur_freq", kgsl + "gpuclk"),
"/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"},
{gpuTemp, "/sys/class/thermal/thermal_zone28/temp"},
@@ -310,8 +383,21 @@ public final class LinuxRuntime {
{thermalZone("ddr"), "/sys/class/thermal/thermal_zone22/temp"},
};
for (String[] stat : stats) {
if (stat[0] != null) bind(cmd, stat[0] + ":" + stat[1]);
// A value the session feeds itself (GpuStatsComponent, where KGSL is refused) wins.
boolean fed = false;
if (extraBinds != null) for (String spec : extraBinds) fed |= spec.endsWith(":" + stat[1]);
if (stat[0] != null && !fed) bind(cmd, stat[0] + ":" + stat[1]);
}
// Which of the overlay's GPU values this device lets the app read: a phone under an
// enforcing policy refuses some of them, and the overlay then has no line for that value.
android.util.Log.i("LinuxRuntime", "hud sources: gpu load " + orNone(stats[0][0]) + ", gpu clock "
+ orNone(stats[1][0]) + ", gpu temp " + orNone(gpuTemp) + ", ddr temp " + orNone(stats[4][0])
+ ", thermal zones " + thermalZones().size()
+ ", kgsl gpubusy " + (new File(kgsl + "gpubusy").canRead() ? "readable" : "refused"));
}
private static String orNone(String path) {
return path != null ? path : "none";
}
/**
@@ -0,0 +1,110 @@
package com.droiddeck.launcher.session
import android.os.SystemClock
import android.util.Log
import com.droiddeck.launcher.core.SessionPart
import java.io.File
/**
* A `/proc/stat` that moves.
*
* Android has refused apps `/proc/stat` since 8.0, and the session's stand-in for it was one
* `cpu0` line of zeros - enough for glibc and Steam to start, but every CPU load in the session
* read 0 % (the performance overlay showed one core, idle). The kernel still lets apps read each
* core's idle residency (`cpuidle/stateN/time`, µs, the same label as the cpufreq files), which is
* how overlays on Android work out CPU load: a core's busy time is the time that passed less the
* time it spent idle. This writes `/proc/stat` from that, one line per core, refreshed twice a
* second. Counters only grow - a sample's idle is never more than the time that passed - so a
* reader taking the difference of two reads never sees a negative. Where `/proc/stat` is readable,
* or the idle times are not, nothing is done.
*/
class CpuStatComponent(val file: File) : SessionPart() {
@Volatile private var running = false
private var thread: Thread? = null
private val cores = (0 until coreCount()).toList()
private val busy = LongArray(cores.size)
private val idle = LongArray(cores.size)
private val lastIdleUs = LongArray(cores.size)
private var lastMs = 0L
private val bootSeconds = (System.currentTimeMillis() - SystemClock.elapsedRealtime()) / 1000
/** Writes the first sample; true when it should be bound over `/proc/stat`. */
fun prepare(): Boolean {
if (File("/proc/stat").canRead() || cores.isEmpty() || idleUs(0) == null) return false
lastMs = SystemClock.elapsedRealtime()
for (i in cores) lastIdleUs[i] = idleUs(i) ?: 0L
write()
Log.i(TAG, "hud: /proc/stat from the cores' idle times (${cores.size} cores)")
return true
}
override fun start() {
running = true
thread = Thread({
while (running) {
try { Thread.sleep(PERIOD_MS) } catch (e: InterruptedException) { break }
if (!running) break
sample()
write()
}
}, "cpu-stat").apply { isDaemon = true; start() }
}
override fun stop() {
running = false
thread?.interrupt()
thread = null
}
private fun sample() {
val now = SystemClock.elapsedRealtime()
val elapsed = (now - lastMs) / MS_PER_TICK
if (elapsed <= 0) return
lastMs += elapsed * MS_PER_TICK
for (i in cores) {
// An offline core has no idle residency to count and runs nothing: all idle.
val us = idleUs(i)
val idleTicks = if (us == null) elapsed else ((us - lastIdleUs[i]) / US_PER_TICK).coerceIn(0, elapsed)
if (us != null) lastIdleUs[i] = us
idle[i] += idleTicks
busy[i] += elapsed - idleTicks
}
}
/** The whole file at once, renamed into place: a reader opens either the last sample or this one. */
private fun write() {
val text = StringBuilder()
text.append("cpu ${busy.sum()} 0 0 ${idle.sum()} 0 0 0 0 0 0\n")
for (i in cores) text.append("cpu$i ${busy[i]} 0 0 ${idle[i]} 0 0 0 0 0 0\n")
text.append("intr 0\nctxt 0\nbtime $bootSeconds\nprocesses 1\nprocs_running 1\nprocs_blocked 0\n")
try {
val tmp = File(file.parentFile, file.name + ".tmp")
tmp.writeText(text.toString())
if (!tmp.renameTo(file)) tmp.delete()
} catch (e: Exception) {
}
}
companion object {
private const val TAG = "SessionService"
private const val PERIOD_MS = 500L
/** /proc/stat counts in USER_HZ, 100 a second on every Linux the guest runs. */
private const val MS_PER_TICK = 10L
private const val US_PER_TICK = 10_000L
private fun coreCount(): Int =
File("/sys/devices/system/cpu").list()?.count { it.matches(Regex("cpu\\d+")) } ?: 0
/** The core's total idle residency in µs, or null when it has none to read (offline). */
private fun idleUs(core: Int): Long? {
val states = File("/sys/devices/system/cpu/cpu$core/cpuidle").listFiles { f -> f.name.startsWith("state") }
?: return null
if (states.isEmpty()) return null
var total = 0L
for (state in states) {
total += runCatching { File(state, "time").readText().trim().toLong() }.getOrNull() ?: return null
}
return total
}
}
}
@@ -0,0 +1,118 @@
package com.droiddeck.launcher.session
import android.util.Log
import com.droiddeck.launcher.core.SessionPart
import java.io.File
/**
* The GPU memory in use, for the performance overlay's VRAM and PVRAM lines.
*
* Valve's mangoapp reads it from the kernel's gpu_mem_total tracepoint, which no Android app may
* reach, so both lines read 0. Our libtracefs for mangoapp (tools/mangoapp/libtracefs-shim.c) hands
* it what this writes to [file] - `/run/droiddeck-hud/gpu-mem` in the guest - every few seconds, in
* bytes, from the best of what KGSL lets the app read:
*
* - `/sys/class/kgsl/kgsl/page_alloc`, every GPU page on the device - what the tracepoint's total
* counts, where the policy allows it;
* - else KGSL's own accounting of each of the session's processes
* (`/sys/class/kgsl/kgsl/proc/<pid>/gpumem_mapped` and `gpumem_unmapped`);
* - else the size of the KGSL mappings in each of the session's processes' `maps` - only the
* buffers the CPU has mapped, so less than the GPU holds (about 40 % of KGSL's own count in a
* game on an AYN Thor), but always readable: every process the app can see in `/proc` is one of
* its own. `maps` rather than `smaps`, which gives the same sizes but walks page tables to do it:
* in a game, 180,000 lines and most of a second of CPU a pass.
*
* Sampled only while the overlay shows (OverlayShown).
*/
class GpuMemComponent(private val file: File, private val root: File) : SessionPart() {
@Volatile private var running = false
private var thread: Thread? = null
private var source = ""
/** True when there is a source to write from. */
fun prepare(): Boolean {
val bytes = sample() ?: return false
write(bytes)
Log.i(TAG, "hud: gpu memory from $source (${bytes / (1024 * 1024)} MiB now)")
return true
}
override fun start() {
running = true
thread = Thread({
while (running) {
try { Thread.sleep(PERIOD_MS) } catch (e: InterruptedException) { break }
if (running && OverlayShown.check(root)) sample()?.let { write(it) }
}
}, "gpu-mem").apply { isDaemon = true; start() }
}
override fun stop() {
running = false
thread?.interrupt()
thread = null
file.delete()
}
private fun sample(): Long? {
readLong("$KGSL_ROOT/page_alloc")?.let { source = "$KGSL_ROOT/page_alloc"; return it }
val pids = sessionPids()
var perProcess = 0L
var anyProcess = false
for (pid in pids) {
val mapped = readLong("$KGSL_ROOT/proc/$pid/gpumem_mapped") ?: continue
perProcess += mapped + (readLong("$KGSL_ROOT/proc/$pid/gpumem_unmapped") ?: 0L)
anyProcess = true
}
if (anyProcess) { source = "$KGSL_ROOT/proc/<pid>/gpumem_*"; return perProcess }
var mappedKb = 0L
var anyMap = false
for (pid in pids) {
val kb = kgslMappedKb(pid) ?: continue
mappedKb += kb
anyMap = true
}
if (anyMap) { source = "/proc/<pid>/maps (KGSL mappings)"; return mappedKb * 1024 }
return null
}
/** The processes this app may see in /proc: Android hides every other uid's. */
private fun sessionPids(): List<String> =
File("/proc").list()?.filter { it.isNotEmpty() && it.all(Char::isDigit) }.orEmpty()
/** Size of a process's mappings of the KGSL device, in kB; null where its maps cannot be read. */
private fun kgslMappedKb(pid: String): Long? = runCatching {
var bytes = 0L
File("/proc/$pid/maps").forEachLine { line ->
if (line.endsWith("kgsl-3d0") || line.contains("/dev/dri/renderD")) {
val dash = line.indexOf('-')
val space = line.indexOf(' ')
if (dash > 0 && space > dash) {
bytes += line.substring(dash + 1, space).toLong(16) - line.substring(0, dash).toLong(16)
}
}
}
bytes / 1024
}.getOrNull()
/** Whole, renamed into place: the reader opens either the last value or this one. */
private fun write(bytes: Long) {
try {
file.parentFile?.mkdirs()
val tmp = File(file.parentFile, file.name + ".tmp")
tmp.writeText("$bytes\n")
if (!tmp.renameTo(file)) tmp.delete()
} catch (e: Exception) {
}
}
companion object {
private const val TAG = "SessionService"
private const val KGSL_ROOT = "/sys/class/kgsl/kgsl"
/** GPU memory moves slowly; a pass over every process's maps is ~40 ms of CPU in a game. */
private const val PERIOD_MS = 5_000L
private fun readLong(path: String): Long? =
runCatching { File(path).readText().trim().toLong() }.getOrNull()
}
}
@@ -0,0 +1,176 @@
package com.droiddeck.launcher.session
import android.util.Log
import com.droiddeck.launcher.core.SessionPart
import com.droiddeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.io.RandomAccessFile
/**
* The Adreno's stats as mangoapp (Deck mode's performance overlay) reads them.
*
* MangoHud takes an Adreno's load, clock and temperature from `gpu_busy_percentage`, `clock_mhz`
* and `temp` in `/sys/class/kgsl/kgsl-3d0`, and ends the process on one of them it may not read -
* not on one that is missing, which it skips. An enforcing SELinux policy (every retail phone)
* refuses apps some or all of KGSL's sysfs, each vendor differently, so mangoapp died on start and
* the session ran with no overlay. Where any of the three is refused, a directory of our own is
* bound there instead, each value fed twice a second from the first source this device lets the
* app read - the places other Android overlays (GameNative, WinNative, Bannerlator) read an
* Adreno's numbers from, across Snapdragon generations and vendor kernels. A value with no
* readable source is left out, and the overlay shows no line for it. Where all three are
* readable, nothing is done.
*
* Valve's build also reads load and clock where a mainline msm kernel has them (debugfs
* `perf_now`, the GPU's devfreq `cur_freq` in Hz) - the paths LinuxRuntime.bindAdrenoStats points
* at KGSL's own files when it may - so the same values are fed there too, in the same formats
* ([binds]).
*/
class GpuStatsComponent(val dir: File) : SessionPart() {
@Volatile private var running = false
private var thread: Thread? = null
private val feeds = ArrayList<Feed>()
/** One of MangoHud's files and where its value comes from. */
private class Feed(
val file: File, val source: String, val read: () -> Long?, val format: String, val percent: Boolean = false,
)
/** Where Valve's msm reader looks, and the feed for it, as host:guest binds. */
fun binds(): List<String> {
val list = mutableListOf(dir.path + ":" + KGSL)
if (File(dir, "perf_now").exists()) list.add(File(dir, "perf_now").path + ":" + MSM_LOAD)
if (File(dir, "cur_freq").exists()) list.add(File(dir, "cur_freq").path + ":" + MSM_CLOCK)
return list
}
/** Writes the directory; true when it should be bound over [KGSL]. */
fun prepare(): Boolean {
if (!File(KGSL).exists() || MANGOHUD_FILES.none { File(KGSL, it).let { f -> f.exists() && !f.canRead() } }) return false
return try {
dir.mkdirs()
dir.listFiles()?.forEach { java.nio.file.Files.deleteIfExists(it.toPath()) }
feeds.clear()
loadFeed()?.let { load ->
feeds.add(load)
// KGSL's gpu_busy_percentage reads "37 %"; padded after, so a shorter value
// written over a longer one leaves no digits behind.
feeds.add(Feed(File(dir, "perf_now"), load.source, load.read, "%-6s\n", percent = true))
}
pick("clock_mhz", clockSources(), "%5d\n") { mhz(it) }?.let { clock ->
feeds.add(clock)
feeds.add(Feed(File(dir, "cur_freq"), clock.source, { clock.read()?.times(1_000_000) }, "%-10d\n"))
}
pick("temp", tempSources(), "%6d\n") { milliCelsius(it) }?.let { feeds.add(it) }
feeds.forEach { feed -> feed.read()?.let { write(feed, it) } }
Log.i(TAG, "hud: kgsl stats from the session's own: " + MANGOHUD_FILES.joinToString { name ->
"$name " + (feeds.firstOrNull { it.file.name == name }?.source ?: "none")
})
true
} catch (e: Exception) {
Log.w(TAG, "hud: could not stand in for kgsl stats: $e")
false
}
}
override fun start() {
if (feeds.isEmpty()) return
running = true
thread = Thread({
while (running) {
try { Thread.sleep(PERIOD_MS) } catch (e: InterruptedException) { break }
if (!running) break
for (feed in feeds) feed.read()?.let { write(feed, it) }
}
}, "gpu-stats").apply { isDaemon = true; start() }
}
override fun stop() {
running = false
thread?.interrupt()
thread = null
}
/** Load: a file that states it in percent, else KGSL's busy and total of the last sample. */
private fun loadFeed(): Feed? {
pick("gpu_busy_percentage", LOAD_SOURCES, "%3d %%\n") { it.coerceIn(0, 100) }?.let { return it }
val gpubusy = "$KGSL/gpubusy"
val read = {
runCatching {
val parts = File(gpubusy).readText().trim().split(Regex("\\s+"))
val total = parts[1].toLong()
if (total > 0) (parts[0].toLong() * 100 / total).coerceIn(0, 100) else 0L
}.getOrNull()
}
return if (read() != null) Feed(File(dir, "gpu_busy_percentage"), gpubusy, read, "%3d %%\n") else null
}
/** The first of [sources] that reads as a number, its value put in MangoHud's unit by [unit]. */
private fun pick(name: String, sources: List<String>, format: String, unit: (Long) -> Long?): Feed? {
for (source in sources) {
val read = { firstNumber(source)?.let(unit) }
if (read() != null) return Feed(File(dir, name), source, read, format)
}
return null
}
/** In place and at one length, so a reader that keeps the file open sees each value whole. */
private fun write(feed: Feed, value: Long) {
try {
val text = if (feed.percent) String.format(feed.format, "$value %") else String.format(feed.format, value)
RandomAccessFile(feed.file, "rw").use { it.seek(0); it.write(text.toByteArray()) }
} catch (e: Exception) {
}
}
companion object {
private const val TAG = "SessionService"
const val KGSL = "/sys/class/kgsl/kgsl-3d0"
/** Must match LinuxRuntime.bindAdrenoStats. */
const val MSM_LOAD = "/sys/kernel/debug/dri/0/perf_now"
const val MSM_CLOCK = "/sys/devices/platform/soc@0/3d00000.gpu/devfreq/3d00000.gpu/cur_freq"
private const val PERIOD_MS = 500L
private val MANGOHUD_FILES = listOf("gpu_busy_percentage", "clock_mhz", "temp")
/** Qualcomm's own GPU summary, beside KGSL in vendor kernels, under the plain sysfs label. */
private const val QCOM_GPU = "/sys/kernel/gpu"
private val LOAD_SOURCES = listOf(
"$KGSL/gpu_busy_percentage", "$KGSL/devfreq/gpu_load", "$KGSL/gpuload", "$QCOM_GPU/gpu_busy",
)
/** The GPU's devfreq node is named by its register address, which differs between Snapdragons. */
private val DEVFREQ_NODES = listOf(
"kgsl-3d0", "3d00000.qcom,kgsl-3d0", "2c00000.qcom,kgsl-3d0", "5000000.qcom,kgsl-3d0", "5900000.qcom,kgsl-3d0",
)
private fun clockSources(): List<String> {
val sources = mutableListOf(
"$KGSL/clock_mhz", "$KGSL/gpu_clock", "$KGSL/gpuclk", "$KGSL/devfreq/cur_freq", "$QCOM_GPU/gpu_clock",
)
DEVFREQ_NODES.forEach { sources.add("/sys/class/devfreq/$it/cur_freq") }
File("/sys/class/devfreq").list()?.sorted()?.forEach { node ->
if (node.contains("kgsl") || node.contains("gpu")) sources.add("/sys/class/devfreq/$node/cur_freq")
}
return sources.distinct()
}
private fun tempSources(): List<String> =
listOfNotNull("$KGSL/temp", "$KGSL/devfreq/temp", "$QCOM_GPU/temp", LinuxRuntime.gpuTempSource()).distinct()
/** The first integer in the file ("550", "37 %", "550000000"), or null. */
private fun firstNumber(path: String): Long? =
runCatching { Regex("-?\\d+").find(File(path).readText())?.value?.toLong() }.getOrNull()
/** Hz, kHz or MHz to MHz, as the files differ; a stopped clock reads 0. */
private fun mhz(raw: Long): Long? = when {
raw < 0 -> null
raw > 10_000_000 -> raw / 1_000_000
raw > 10_000 -> raw / 1_000
else -> raw
}
/** °C or m°C to MangoHud's m°C; nothing a GPU reads at is below 1 °C or above 1000. */
private fun milliCelsius(raw: Long): Long? = when {
raw <= 0 -> null
raw < 1000 -> raw * 1000
else -> raw
}
}
}
@@ -0,0 +1,141 @@
package com.droiddeck.launcher.session
import android.os.SystemClock
import android.system.Os
import android.util.Log
import com.droiddeck.launcher.core.SessionPart
import com.droiddeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.io.RandomAccessFile
/**
* The session's `/sys/class/hwmon`, as mangoapp (Deck mode's performance overlay) looks there.
*
* MangoHud finds a CPU's temperature by an hwmon sensor named `cpuN_thermal`, and an Adreno's
* temperature, fan and power by one whose name has `gpuss` in it - a mainline Arm kernel's names.
* An Android kernel has neither (its hwmon holds the PMIC's sensors, or nothing), and where the
* app may not list hwmon at all the overlay logged an error twice a second. So the overlay showed
* no CPU temperature and no fan. This is two sensors named so:
*
* - `hwmon0` (`cpu0_thermal`): `temp1_input` linked to a CPU thermal zone the app can read;
* - `hwmon1` (`gpuss0_thermal`): `temp1_input` linked to the GPU's zone, and `fan1_input` with
* the device's fan speed where it has a fan the app can read - an AYN handheld's tachometer
* (`/sys/class/gpio5_pwm2/speed`), or a `fan*_input` in the device's own hwmon;
* - `hwmon2` (`slg4ax46073v`): the same fan, for Valve's own Qualcomm reader (msm.cpp), which
* looks for its fan controller by that name and halves what it reads - that part reports twice
* the real speed - so this one holds twice it.
*
* Nothing in a phone's own hwmon is read by the overlay, so standing in for it loses nothing.
*/
class HwmonComponent(val dir: File, private val root: File) : SessionPart() {
@Volatile private var running = false
private var thread: Thread? = null
private var fanSource: String? = null
private val fanFile = File(dir, "hwmon1/fan1_input")
private val valveFanFile = File(dir, "hwmon2/fan1_input")
private var lastRpm = 0L
private var lastRpmAt = 0L
/** Writes the sensors; true when the directory should be bound over `/sys/class/hwmon`. */
fun prepare(): Boolean = try {
dir.mkdirs()
clear(dir)
val cpu = LinuxRuntime.cpuTempSource()
if (cpu != null) sensor("hwmon0", "cpu0_thermal").also { link(cpu, File(it, "temp1_input")) }
val gpu = LinuxRuntime.gpuTempSource()
fanSource = FAN_SOURCES().firstOrNull { readNumber(it) != null }
if (gpu != null || fanSource != null) {
val sensor = sensor("hwmon1", "gpuss0_thermal")
if (gpu != null) link(gpu, File(sensor, "temp1_input"))
if (fanSource != null) {
sensor("hwmon2", "slg4ax46073v")
writeFan(sampleFan())
}
}
Log.i(TAG, "hud: hwmon cpu temp ${cpu ?: "none"}, gpu temp ${gpu ?: "none"}, fan ${fanSource ?: "none"}")
cpu != null || gpu != null || fanSource != null
} catch (e: Exception) {
Log.w(TAG, "hud: could not write the session's hwmon: $e")
false
}
override fun start() {
if (fanSource == null) return
running = true
thread = Thread({
while (running) {
try { Thread.sleep(FAN_PERIOD_MS) } catch (e: InterruptedException) { break }
// A tachometer read busy-waits 100 ms in the kernel: only while the overlay shows.
if (running && OverlayShown.check(root)) writeFan(sampleFan())
}
}, "hwmon-fan").apply { isDaemon = true; start() }
}
override fun stop() {
running = false
thread?.interrupt()
thread = null
}
/**
* The fan's speed in RPM. An AYN tachometer counts pulses over a short window per read and
* often catches none, reading 0 while the fan turns; a reading is held until a newer one
* comes or [FAN_HOLD_MS] passes with only zeros, when the fan is taken to have stopped.
*/
private fun sampleFan(): Long {
val now = SystemClock.elapsedRealtime()
val rpm = fanSource?.let { readNumber(it) } ?: 0L
if (rpm > 0) {
lastRpm = rpm
lastRpmAt = now
} else if (now - lastRpmAt > FAN_HOLD_MS) {
lastRpm = 0
}
return lastRpm
}
/** In place and at one length, so a reader that keeps the file open sees each value whole. */
private fun writeFan(rpm: Long) {
for ((file, value) in listOf(fanFile to rpm, valveFanFile to rpm * 2)) {
try {
RandomAccessFile(file, "rw").use { it.seek(0); it.write(String.format("%6d\n", value).toByteArray()) }
} catch (e: Exception) {
}
}
}
private fun sensor(name: String, label: String): File =
File(dir, name).apply { mkdirs(); File(this, "name").writeText("$label\n") }
private fun link(target: String, link: File) {
java.nio.file.Files.deleteIfExists(link.toPath())
Os.symlink(target, link.path)
}
/** Only files and links are made here; deleting a link must not follow it into sysfs. */
private fun clear(root: File) {
root.listFiles()?.forEach { f ->
if (!java.nio.file.Files.isSymbolicLink(f.toPath()) && f.isDirectory) clear(f)
java.nio.file.Files.deleteIfExists(f.toPath())
}
}
companion object {
private const val TAG = "SessionService"
/** The AYN tachometer reads 0 if read again within a couple of seconds. */
private const val FAN_PERIOD_MS = 3_000L
private const val FAN_HOLD_MS = 10_000L
private val FAN_SOURCES = {
val sources = mutableListOf("/sys/class/gpio5_pwm2/speed", "/sys/class/gpio5_pwm/speed")
File("/sys/class/hwmon").listFiles()?.sortedBy { it.name }?.forEach { hwmon ->
hwmon.listFiles { f -> f.name.matches(Regex("fan\\d+_input")) }?.sortedBy { it.name }
?.forEach { sources.add(it.path) }
}
sources
}
private fun readNumber(path: String): Long? =
runCatching { File(path).readText().trim().toLong() }.getOrNull()?.takeIf { it >= 0 }
}
}
@@ -0,0 +1,17 @@
package com.droiddeck.launcher.session
import java.io.File
/**
* Whether Deck mode's performance overlay is on screen. The client sets its level by rewriting
* mangoapp's config (bannerlator-session makes it, `/tmp/mangohud.*`), and `no_display` is the
* level that hides it. The feeds whose reads cost something - the fan's tachometer busy-waits in
* the kernel, GPU memory walks every process's mappings - only run while it shows.
*/
object OverlayShown {
fun check(root: File): Boolean {
val config = File(root, "tmp").listFiles { f -> f.name.startsWith("mangohud.") }
?.maxByOrNull { it.lastModified() } ?: return false
return runCatching { !config.readText().contains("no_display") }.getOrDefault(false)
}
}
@@ -277,7 +277,7 @@ class SessionService : Service() {
// The desktop's Steam launchers start the client there (bannerlator-steam-launch), through the
// same set-up as a Steam session: it gets what the client and its games are started with.
val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP
addClientEnvironment(guest, runtimeDir, steamHere)
addClientEnvironment(guest, steamHere)
val pulse = startAudio(guest, sessionDir)
@@ -483,7 +483,7 @@ class SessionService : Service() {
}
/** The guest's base environment: paths, the display, the GL/Vulkan stack and the client's switches. */
private fun addClientEnvironment(guest: MutableList<String>, runtimeDir: File, steamHere: Boolean) {
private fun addClientEnvironment(guest: MutableList<String>, steamHere: Boolean) {
guest.add("/usr/bin/env")
guest.add("-i")
guest.add("HOME=/root")
@@ -494,7 +494,7 @@ class SessionService : Service() {
// Without this the session is UTC: the client's clock, its logs and every timestamp in a
// session bundle sit hours off the device's. Bannerlator carries the same line.
guest.add("TZ=" + java.util.TimeZone.getDefault().id)
guest.add("XDG_RUNTIME_DIR=" + runtimeDir.path)
guest.add("XDG_RUNTIME_DIR=" + LinuxRuntime.GUEST_RUNTIME_DIR)
guest.add("XDG_SESSION_TYPE=wayland")
guest.add("WAYLAND_DISPLAY=wayland-0")
guest.add("GAMESCOPE_FORCE_GENERAL_QUEUE=1")
@@ -689,6 +689,33 @@ class SessionService : Service() {
battery.attach(this)
components.add(battery)
binds.add(battery.dir.path + ":/sys/class/power_supply")
// The overlay's CPU and GPU temperatures and the fan, as hwmon sensors it knows by name.
val hwmon = HwmonComponent(File(filesDir, "session/sys/hwmon"), LinuxRuntime.rootDir(this))
if (hwmon.prepare()) {
hwmon.attach(this)
components.add(hwmon)
binds.add(hwmon.dir.path + ":/sys/class/hwmon")
}
// CPU load for everything in the session that reads /proc/stat, the overlay among them.
val cpuStat = CpuStatComponent(File(filesDir, "session/proc-stat"))
if (cpuStat.prepare()) {
cpuStat.attach(this)
components.add(cpuStat)
binds.add(cpuStat.file.path + ":/proc/stat")
}
// The GPU memory in use for the overlay's VRAM lines, which it would read from tracefs.
val gpuMem = GpuMemComponent(File(LinuxRuntime.rootDir(this), "run/droiddeck-hud/gpu-mem"), LinuxRuntime.rootDir(this))
if (gpuMem.prepare()) {
gpuMem.attach(this)
components.add(gpuMem)
}
// The GPU's load and temperature for the performance overlay, where KGSL's sysfs is refused.
val gpuStats = GpuStatsComponent(File(filesDir, "session/sys/kgsl-3d0"))
if (gpuStats.prepare()) {
gpuStats.attach(this)
components.add(gpuStats)
binds.addAll(gpuStats.binds())
}
// Rumble for the on-screen pad: the fake evdev layer sends force-feedback effects to this
// listener, which drives the phone's vibrator (see RumbleComponent).
if (controllersOn) components.add(RumbleComponent().also { it.attach(this) })
+5 -1
View File
@@ -221,9 +221,13 @@ while read -r package_sha256 package_url; do
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"
for library in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1 libtracefs.so.1; do
for library in libfmt.so.10 libspdlog.so.1.13 libglfw.so.3 libtraceevent.so.1; do
cp -L "${mango_pkgs}/usr/lib/${library}" "${mango_dir}/${library}"
done
# Ours, not the package's: GPU memory without tracefs (tools/mangoapp/libtracefs-shim.c).
docker run --rm --platform linux/amd64 --user "$(id -u):$(id -g)" -v "${repo_root}:/src" -w /src "${image_name}" \
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -Wl,-soname,libtracefs.so.1 \
-o app/src/main/assets/linuxfs/usr/local/lib/mangoapp/libtracefs.so.1 tools/mangoapp/libtracefs-shim.c
mkdir -p "${linuxfs_dir}/usr/local/bin"
install -m644 "${repo_root}/tools/mangoapp/mangoapp" "${linuxfs_dir}/usr/local/bin/mangoapp"
+8
View File
@@ -39,5 +39,13 @@ library list, before anything is published.
then focus is handed over again. `GAMESCOPE_RESTORE_FOCUS_WINDOW` on the root window asks for the
same restore from outside (the session script's resume watcher).
- `0113-steam-overlay-keeps-game-keyboard-focus.patch` - this app: the Steam client opens the Quick
Access Menu and Steam menu over a game with `STEAM_INPUT_FOCUS` 1, which moves X keyboard focus to
its overlay; wine then deactivates the game and a fullscreen game minimizes itself, freezing
behind the menu instead of running on as on a Steam Deck. Steam's own overlay taking input now
keeps keyboard focus on the game, as mode 2 does; its input comes from the controller through
Steam Input, not the X keyboard. The pointer warps gamescope makes as input moves to Steam and
back are skipped around it, since the game - still taking input - saw them as a mouse jump.
Sixteen more of Armada's patches are DRM/lease/HDR-on-KMS work for a native display, which this
app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the runtime has.
@@ -0,0 +1,104 @@
From: DroidDeck
Subject: [PATCH] steamcompmgr: keep the game's keyboard focus under Steam's overlay
The Steam client opens its Quick Access Menu and Steam menu over a game by setting
STEAM_OVERLAY 1 and STEAM_INPUT_FOCUS 1 on its window. Mode 1 moves X keyboard focus to the
overlay, so the game's window gets FocusOut: wine moves the foreground to the desktop, a fullscreen
game deactivates and minimizes itself (WM_CHANGE_STATE IconicState), and it stops rendering behind
the menu - frozen, all threads asleep, until the menu closes (Alan Wake's American Nightmare,
Proton Experimental ARM64). On a Steam Deck the game runs on behind the menu.
Mode 2 already means "input to the overlay, keyboard focus stays on the focus window". Treat
Steam's own overlay taking input in mode 1 the same way at the three places mode 2 is honoured.
The overlay's input does not need X keyboard focus: the controller drives it through Steam Input.
Measured by rewriting the property to 2 by hand: the game kept running behind the menu and the
controller still moved only the menu.
With the game keeping focus, the pointer resets gamescope makes when input moves to Steam (warp
to the centre, park in the corner) and back (warp to the centre) reached it as a sudden mouse
move: a mouse-look camera spun every time the menu opened and closed. Skip them while Steam's
overlay opens over a game that keeps focus, and as it closes - by then the overlay has dropped its
STEAM_INPUT_FOCUS, so whether the opening kept focus is remembered.
diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp
index 372a82c..3fab3c6 100644
--- a/src/steamcompmgr.cpp
+++ b/src/steamcompmgr.cpp
@@ -1321,6 +1321,22 @@ window_is_steam( steamcompmgr_win_t *w )
return w && ( w->isSteamLegacyBigPicture || w->appID == 769 );
}
+// Whether keyboard focus stays on the focus window while this window has input focus.
+// STEAM_INPUT_FOCUS 2 asks for that. The Steam client also opens its Quick Access Menu and Steam
+// menu over a game with 1, which moves X keyboard focus to the overlay: wine then deactivates the
+// game, a fullscreen game minimizes itself, and it froze behind the menu instead of running on as
+// on a Steam Deck. The overlay's own input does not need X keyboard focus (the controller drives
+// it through Steam Input), so Steam's overlay keeps the game's keyboard focus too.
+static bool
+window_keeps_game_keyboard_focus( steamcompmgr_win_t *w )
+{
+ if ( !w )
+ return false;
+ if ( w->inputFocusMode == 2 )
+ return true;
+ return w->inputFocusMode == 1 && w->isOverlay && window_is_steam( w );
+}
+
static bool
window_is_vr_scene_app( steamcompmgr_win_t *w )
{
@@ -4728,7 +4744,7 @@ void xwayland_ctx_t::DetermineAndApplyFocus( const std::vector< steamcompmgr_win
if ( gamescope::VirtualConnectorIsSingleOutput() )
{
- if ( inputFocus && inputFocus->inputFocusMode == 2 )
+ if ( window_keeps_game_keyboard_focus( inputFocus ) )
keyboardFocusWin = ctx->focus.focusWindow;
}
else
@@ -4834,12 +4850,26 @@ void xwayland_ctx_t::DetermineAndApplyFocus( const std::vector< steamcompmgr_win
if ( ctx->focus.inputFocusWindow != inputFocus ||
ctx->focus.inputFocusMode != inputFocus->inputFocusMode )
{
+ // Steam's overlay opening over a game, or closing again, while the game keeps keyboard focus
+ // (window_keeps_game_keyboard_focus): the game is still taking input, so a pointer warp
+ // reaches it as a sudden mouse move - the camera of a mouse-look game spun as the Quick
+ // Access Menu opened and as it closed. The overlay is driven by the controller and the
+ // cursor is hidden, so leave the pointer where it is. Closing, the overlay has already
+ // dropped its STEAM_INPUT_FOCUS, so whether the opening kept focus is remembered.
+ static bool s_bOverlayKeptGameFocus = false;
+ bool bOverlayKeepsGameFocus = window_keeps_game_keyboard_focus( inputFocus ) &&
+ ctx->focus.focusWindow && !window_is_steam( ctx->focus.focusWindow );
+ bool bSkipPointerReset = bOverlayKeepsGameFocus || s_bOverlayKeptGameFocus;
+ s_bOverlayKeptGameFocus = bOverlayKeepsGameFocus;
+
// If the window doesn't want focus when hidden, move it away
// as we are going to hide it straight after.
// otherwise, if we switch from wanting it to not
// (steam -> game)
// put us back in the centre of the screen.
- if (window_wants_no_focus_when_mouse_hidden(inputFocus))
+ if ( bSkipPointerReset )
+ ;
+ else if (window_wants_no_focus_when_mouse_hidden(inputFocus))
ctx->focus.bResetToCorner = true;
else if ( window_wants_no_focus_when_mouse_hidden(inputFocus) != window_wants_no_focus_when_mouse_hidden(ctx->focus.inputFocusWindow) )
ctx->focus.bResetToCenter = true;
@@ -5340,7 +5370,7 @@ determine_and_apply_focus( global_focus_t *pFocus )
pFocus->inputFocusMode = pFocus->inputFocusWindow->inputFocusMode;
if ( gamescope::VirtualConnectorIsSingleOutput() &&
- pFocus->inputFocusMode == 2 )
+ window_keeps_game_keyboard_focus( pFocus->inputFocusWindow ) )
{
pFocus->keyboardFocusWindow = pFocus->overrideWindow
? pFocus->overrideWindow
@@ -6485,7 +6515,7 @@ handle_wl_surface_id(xwayland_ctx_t *ctx, steamcompmgr_win_t *w, uint32_t surfac
steamcompmgr_win_t *keyboardFocusWindow = pCurrentFocus->inputFocusWindow;
if ( gamescope::VirtualConnectorIsSingleOutput() &&
- keyboardFocusWindow && keyboardFocusWindow->inputFocusMode == 2 )
+ window_keeps_game_keyboard_focus( keyboardFocusWindow ) )
keyboardFocusWindow = pCurrentFocus->focusWindow;
if ( w == keyboardFocusWindow )
+2 -2
View File
@@ -1,2 +1,2 @@
GAMESCOPE_TAG=gamescope-3.16.29-p4
GAMESCOPE_SHA256=e750fb34864bc2eb1160aeed29593e402bf9c8153fafd9fd18c77450bdd50574
GAMESCOPE_TAG=gamescope-3.16.29-p5
GAMESCOPE_SHA256=188144e5564e020d0bc485a1f4f90e1c5fd61d0d1d6dd29db21e8b019036a5c6
@@ -172,6 +172,8 @@ if [ -z "${BL_INSIDE:-}" ]; then
# HDR10: the app opened its compositor's colour-management gate for this session (BL_HDR=1);
# gamescope then offers HDR to its clients through the same wp_color_manager_v1 it binds on ours.
[ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled)
# Extra gamescope arguments for an experiment (--debug-focus, say), from Download/droiddeck-env.
[ -n "${BL_GAMESCOPE_ARGS:-}" ] && read -r -a _bl_extra <<<"$BL_GAMESCOPE_ARGS" && args+=("${_bl_extra[@]}")
# Deck mode's performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by
# mangoapp, which gamescope starts beside the client; the client sets the level by rewriting
# MANGOHUD_CONFIGFILE. The apk stages Valve's aarch64 build (tools/mangoapp) - without it the menu
+144
View File
@@ -0,0 +1,144 @@
// libtracefs.so.1 for mangoapp: the GPU memory Valve's overlay reads, without the kernel's tracing.
//
// Valve's mangoapp (msm.cpp, ftrace_gpumem.c) takes the GPU's memory in use - the VRAM and PVRAM
// lines - from the gpu_mem_total tracepoint (pid 0, the device's total) through libtracefs. An
// Android app may not use tracefs at all: the instance could not be made, and both lines read 0.
// This stands in for the libtracefs calls mangoapp makes, and hands its record walker one
// gpu_mem_total record carrying what the app writes to /run/droiddeck-hud/gpu-mem (bytes; the
// session's GpuMemComponent, from KGSL's own accounting). Only mangoapp loads this copy.
//
// The walker reads libtraceevent's structures directly, so the ones below are libtraceevent
// 1.8.2's (event-parse.h), the version tools/mangoapp/packages.txt pins; tep_find_event_by_record
// and tep_read_number are answered here too, ahead of the real libtraceevent in mangoapp's lookup.
#include <sched.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define EXPORT __attribute__((visibility("default")))
struct tep_handle;
struct tep_event;
struct tep_print_arg;
struct tep_print_parse;
struct tep_format_field {
struct tep_format_field *next;
struct tep_event *event;
char *type;
char *name;
char *alias;
int offset;
int size;
unsigned int arraylen;
unsigned int elementsize;
unsigned long flags;
};
struct tep_format {
int nr_common;
int nr_fields;
struct tep_format_field *common_fields;
struct tep_format_field *fields;
};
struct tep_print_fmt {
char *format;
struct tep_print_arg *args;
struct tep_print_parse *print_cache;
};
struct tep_event {
struct tep_handle *tep;
char *name;
int id;
int flags;
struct tep_format format;
struct tep_print_fmt print_fmt;
char *system;
void *handler;
void *context;
};
struct tep_record {
unsigned long long ts;
unsigned long long offset;
long long missed_events;
int record_size;
int size;
void *data;
int cpu;
int ref_count;
int locked;
void *priv;
};
// gpu_mem_total's record: gpu_id, pid, size - as the kernel lays it out after the common fields.
struct gpu_mem_total {
unsigned int gpu_id;
unsigned int pid;
unsigned long long size;
};
static struct tep_format_field size_field = {
.name = "size", .offset = offsetof(struct gpu_mem_total, size), .size = 8};
static struct tep_format_field pid_field = {
.next = &size_field, .name = "pid", .offset = offsetof(struct gpu_mem_total, pid), .size = 4};
static struct tep_event gpu_mem_event = {
.name = "gpu_mem_total", .system = "gpu_mem", .format = {.nr_fields = 2, .fields = &pid_field}};
// Never dereferenced by mangoapp: they only have to be non-null.
static char handle;
static char instance;
static unsigned long long gpu_mem_bytes(void) {
unsigned long long bytes = 0;
FILE *f = fopen("/run/droiddeck-hud/gpu-mem", "r");
if (f) {
if (fscanf(f, "%llu", &bytes) != 1) bytes = 0;
fclose(f);
}
return bytes;
}
EXPORT void *tracefs_instance_create(const char *name) { (void)name; return &instance; }
EXPORT char *tracefs_instance_get_dir(void *inst) { (void)inst; return strdup("/run/droiddeck-hud"); }
EXPORT void tracefs_put_tracing_file(char *name) { free(name); }
EXPORT struct tep_handle *tracefs_local_events(const char *dir) { (void)dir; return (struct tep_handle *)&handle; }
EXPORT int tracefs_event_enable(void *inst, const char *system, const char *event) { (void)inst; (void)system; (void)event; return 0; }
EXPORT int tracefs_event_disable(void *inst, const char *system, const char *event) { (void)inst; (void)system; (void)event; return 0; }
EXPORT bool tracefs_instance_is_new(void *inst) { (void)inst; return false; }
EXPORT int tracefs_instance_destroy(void *inst) { (void)inst; return 0; }
EXPORT void tracefs_instance_free(void *inst) { (void)inst; }
EXPORT int tracefs_iterate_raw_events(struct tep_handle *tep, void *inst, cpu_set_t *cpus, int cpu_size,
int (*callback)(struct tep_event *, struct tep_record *, int, void *),
void *context) {
(void)tep; (void)inst; (void)cpus; (void)cpu_size;
if (!callback) return -1;
struct gpu_mem_total data = {.gpu_id = 0, .pid = 0, .size = gpu_mem_bytes()};
struct tep_record record = {.size = sizeof(data), .record_size = sizeof(data), .data = &data};
callback(&gpu_mem_event, &record, 0, context);
return 0;
}
EXPORT struct tep_event *tep_find_event_by_record(struct tep_handle *tep, struct tep_record *record) {
(void)tep; (void)record;
return &gpu_mem_event;
}
EXPORT unsigned long long tep_read_number(struct tep_handle *tep, const void *ptr, int size) {
(void)tep;
switch (size) {
case 1: return *(const unsigned char *)ptr;
case 2: return *(const unsigned short *)ptr;
case 4: return *(const unsigned int *)ptr;
case 8: return *(const unsigned long long *)ptr;
default: return 0;
}
}
EXPORT void tep_free(struct tep_handle *tep) { (void)tep; }
+3 -2
View File
@@ -1,4 +1,4 @@
# Valve's aarch64 mangoapp (the Deck performance overlay) and the five libraries it needs that the
# Valve's aarch64 mangoapp (the Deck performance overlay) and the libraries it needs that the
# runtime does not have, at the versions it was built against (its .BUILDINFO: fmt 10.2.0,
# spdlog 1.13.0). CI fetches each, checks the sha256 and stages the files named below into the apk
# under usr/local/lib/mangoapp; SessionFiles copies them into the runtime at every launch.
@@ -7,5 +7,6 @@
ef7411229ea4e9b3ab4e36ac356a85b5072f362ee1de68b28d09c43f0b0ecc44 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/fmt-10.2.0-1-aarch64.pkg.tar.zst
c9bfd2c3f15071329d381b9c2061d07503c4787ceb7001b9dfbf72a23c85cb04 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/spdlog-1.13.0-1-aarch64.pkg.tar.zst
f9fae6c84a0382188f1cb20761424051593fd09b1456191cc250bd16e0a1d7a5 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/glfw-3.4-2-aarch64.pkg.tar.zst
# libtracefs is ours (libtracefs-shim.c), and reads libtraceevent's structures as 1.8.2 lays them
# out: moving libtraceevent off 1.8.2 means checking event-parse.h against the shim.
0cccdc9c92cb3cae1e37ebeb0ed20b93bb3b5000c8f955b63d0e07bd878c7018 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/libtraceevent-1%3A1.8.2-2-aarch64.pkg.tar.zst
47ffd6a953959d6a0df44539bef30a67d0b8c95cce0053273908d9d70ef7abc4 https://steamdeck-packages.steamos.cloud/archlinux-deckard/archlinux/mash-20240705.2/extra/os/aarch64/libtracefs-1.8.0-1-aarch64.pkg.tar.zst