mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
fix: keep the performance overlay running and fed under an enforcing SELinux policy
This commit is contained in:
1 parent
6782e5a717
commit
48054a86ab
3 files changed
+202
-2
No files matched your search
@@ -238,6 +238,7 @@ public final class LinuxRuntime {
|
||||
bindGpuNode(context, cmd);
|
||||
bindAdrenoStats(cmd);
|
||||
bindCpuTemps(cmd, root);
|
||||
bindCpuHwmon(context.getCacheDir(), cmd);
|
||||
if (extraBinds != null) {
|
||||
for (String spec : extraBinds) bind(cmd, spec);
|
||||
}
|
||||
@@ -282,12 +283,94 @@ 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");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MangoHud's CPU temperature: it looks first in {@code /sys/class/hwmon} for a sensor named
|
||||
* {@code cpuN_thermal} (a mainline Arm kernel's), then at thermal zones of a type it knows; an
|
||||
* Android kernel has neither - its hwmon holds the PMIC's sensors and its zones are named by
|
||||
* the vendor (cpu-1-0-usr) - so the overlay showed no CPU temperature, and where the app may
|
||||
* not list hwmon the overlay logged an error twice a second. The session's hwmon is one sensor
|
||||
* named so, reading a CPU zone the app can read. Nothing else in a phone's hwmon is read by it.
|
||||
*/
|
||||
private static void bindCpuHwmon(File cacheDir, List<String> cmd) {
|
||||
String zone = null;
|
||||
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()) {
|
||||
zone = temp.getPath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (zone == null) return;
|
||||
File sensor = new File(cacheDir, "hwmon/hwmon0");
|
||||
try {
|
||||
if (!sensor.isDirectory() && !sensor.mkdirs()) return;
|
||||
Files.write(new File(sensor, "name").toPath(), "cpu0_thermal\n".getBytes(StandardCharsets.US_ASCII));
|
||||
File input = new File(sensor, "temp1_input");
|
||||
Files.deleteIfExists(input.toPath());
|
||||
Os.symlink(zone, input.getPath());
|
||||
} catch (IOException | ErrnoException e) {
|
||||
return;
|
||||
}
|
||||
bind(cmd, sensor.getParent() + ":/sys/class/hwmon");
|
||||
android.util.Log.i("LinuxRuntime", "hud: cpu temp from " + zone);
|
||||
}
|
||||
|
||||
/** 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");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,9 +383,9 @@ public final class LinuxRuntime {
|
||||
*/
|
||||
private static void bindAdrenoStats(List<String> cmd) {
|
||||
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"},
|
||||
@@ -312,6 +395,16 @@ public final class LinuxRuntime {
|
||||
for (String[] stat : stats) {
|
||||
if (stat[0] != null) 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,100 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
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 Adreno's stats as mangoapp (Deck mode's performance overlay) reads them.
|
||||
*
|
||||
* MangoHud takes an Adreno's load and temperature from `/sys/class/kgsl/kgsl-3d0` and ends the
|
||||
* process on a file there it may not read - not a file that is missing, which it skips. Under an
|
||||
* enforcing SELinux policy (every retail phone) KGSL's sysfs is refused to apps, so mangoapp died
|
||||
* on start and the session ran with no overlay. Where that is so, a directory of our own is bound
|
||||
* there instead: `gpu_busy_percentage` worked out from `gpubusy` (busy and total cycles of the
|
||||
* last sample) and `clock_mhz` from `gpuclk` - files with labels of their own that policies often
|
||||
* leave to apps for GPU profilers - when they are readable, and `temp` linked to the GPU's
|
||||
* thermal zone when there is one. What is not readable is left out, and the overlay shows no line
|
||||
* for it. Where KGSL's own files are readable, nothing is done.
|
||||
*/
|
||||
class GpuStatsComponent(val dir: File) : SessionPart() {
|
||||
@Volatile private var running = false
|
||||
private var thread: Thread? = null
|
||||
private val load = File(dir, "gpu_busy_percentage")
|
||||
private val clock = File(dir, "clock_mhz")
|
||||
|
||||
/** Writes the directory; true when it should be bound over [KGSL]. */
|
||||
fun prepare(): Boolean {
|
||||
if (!File(KGSL).exists() || File(KGSL, "gpu_busy_percentage").canRead()) return false
|
||||
return try {
|
||||
dir.mkdirs()
|
||||
dir.listFiles()?.forEach { java.nio.file.Files.deleteIfExists(it.toPath()) }
|
||||
val busy = File(KGSL, "gpubusy").canRead()
|
||||
if (busy) writeLoad(0)
|
||||
val clk = File(KGSL, "gpuclk").canRead()
|
||||
if (clk) readClock()?.let { write(clock, String.format("%5d\n", it)) }
|
||||
val temp = LinuxRuntime.gpuTempSource()
|
||||
if (temp != null) Os.symlink(temp, File(dir, "temp").path)
|
||||
Log.i(TAG, "hud: kgsl stats from the session's own: load " + (if (busy) "from gpubusy" else "none") +
|
||||
", clock " + (if (clk) "from gpuclk" else "none") + ", temp " + (temp ?: "none"))
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "hud: could not stand in for kgsl stats: $e")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
if (!load.exists() && !clock.exists()) return
|
||||
running = true
|
||||
thread = Thread({
|
||||
while (running) {
|
||||
try { Thread.sleep(PERIOD_MS) } catch (e: InterruptedException) { break }
|
||||
if (!running) break
|
||||
if (load.exists()) readBusy()?.let { writeLoad(it) }
|
||||
if (clock.exists()) readClock()?.let { write(clock, String.format("%5d\n", it)) }
|
||||
}
|
||||
}, "gpu-stats").apply { isDaemon = true; start() }
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
running = false
|
||||
thread?.interrupt()
|
||||
thread = null
|
||||
}
|
||||
|
||||
private fun readBusy(): Int? = try {
|
||||
val parts = File(KGSL, "gpubusy").readText().trim().split(Regex("\\s+"))
|
||||
val busy = parts[0].toLong()
|
||||
val total = parts[1].toLong()
|
||||
if (total > 0) (busy * 100 / total).toInt().coerceIn(0, 100) else 0
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
/** The GPU clock in MHz; gpuclk is in Hz. */
|
||||
private fun readClock(): Int? = try {
|
||||
(File(KGSL, "gpuclk").readText().trim().toLong() / 1_000_000).toInt()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun writeLoad(percent: Int) = write(load, String.format("%3d %%\n", percent))
|
||||
|
||||
/** In place and at one length, so a reader that keeps the file open sees each value whole. */
|
||||
private fun write(file: File, text: String) {
|
||||
try {
|
||||
RandomAccessFile(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"
|
||||
private const val PERIOD_MS = 500L
|
||||
}
|
||||
}
|
||||
@@ -688,6 +688,13 @@ class SessionService : Service() {
|
||||
battery.attach(this)
|
||||
components.add(battery)
|
||||
binds.add(battery.dir.path + ":/sys/class/power_supply")
|
||||
// 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.add(gpuStats.dir.path + ":" + GpuStatsComponent.KGSL)
|
||||
}
|
||||
// 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) })
|
||||
|
||||
Reference in new issue
Block a user