mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Merge branch 'main' into feat/performance-fixes-2
This commit is contained in:
11 files changed
+827
-10
No files matched your search
@@ -211,9 +211,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"
|
||||
|
||||
@@ -231,12 +231,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 +285,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 +360,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 +372,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)
|
||||
}
|
||||
}
|
||||
@@ -710,6 +710,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) })
|
||||
|
||||
@@ -224,9 +224,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"
|
||||
|
||||
|
||||
@@ -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; }
|
||||
@@ -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
|
||||
Reference in new issue
Block a user