mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-09 01:59:35 -10:00
fix(hud): GPU clock and load without root on Adreno 8xx
Adreno 8xx phones (OnePlus 13, Adreno 830) showed no GPU clock and only the partial Vulkan-timing GPU load, where kgsl sysfs is blocked: - The cycle-counter job read the A6xx/A7xx registers. On A8xx CP_ALWAYS_ON_COUNTER is 0x8e7, not 0x980 (gen8_reg.h, Mesa a6xx.xml), RBBM countable 0 is NEVER_COUNT (cycles are countable 1, Mesa a8xx_perfcntrs.xml), and CP protection leaves 0x000-0x3a3 readable (gen8_0_0_protected_regs). The layout is now picked from the KGSL chip id (top byte 0x44 on every A8xx); unknown later generations read nothing. - Every gen8 GPU runs the hardware scheduler, which never writes the memstore ringbuffer stamps; the GMU writes each context's own entry (gen8_hwsched_hfi.c send_context_pointers). Where the ringbuffer entries are all 0 the reader now watches the contexts that submit work (rescanned every 200 samples, at most 64). - The HUD diagnostics report says why each no-root GPU reader works or not, prints "none" instead of "null" for missing values, and labels the per-core CPU numbers as a clock estimate when /proc/stat is unreadable. Kernel facts checked in the OnePlus SM8750 kgsl source (LineageOS/android_kernel_oneplus_sm8750-modules @e05c031c).
This commit is contained in:
1 parent
e30e722f8b
commit
06be2c5fe8
13 files changed
+274
-24
No files matched your search
@@ -1,5 +1,6 @@
|
||||
// Whole-GPU busy share without root on Adreno (KGSL) devices whose kgsl sysfs is blocked for apps: a thread
|
||||
// samples the memstore ringbuffer timestamps (kgsl_busy.h) once a millisecond while running.
|
||||
// samples the memstore timestamps (kgsl_busy.h) once a millisecond while running. Ringbuffer entries are used where
|
||||
// the kernel writes them; on hardware-scheduled kernels, which leave them at 0, the contexts' own entries are used.
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
@@ -15,13 +16,21 @@
|
||||
#include "kgsl_busy.h"
|
||||
|
||||
#define SAMPLE_NS 1000000L
|
||||
// Context mode: how many samples between scans for the contexts that are submitting work.
|
||||
#define SCAN_EVERY 200
|
||||
|
||||
enum { MODE_RINGBUFFER = 1, MODE_CONTEXTS = 2 };
|
||||
|
||||
struct reader {
|
||||
int fd;
|
||||
void *map;
|
||||
size_t bytes;
|
||||
int first;
|
||||
int mode;
|
||||
uint32_t last[KGSL_BUSY_SLOTS];
|
||||
uint32_t *seen, *ctx_last;
|
||||
int live[KGSL_BUSY_MAX_LIVE];
|
||||
int live_count;
|
||||
pthread_mutex_t lock;
|
||||
pthread_t thread;
|
||||
int thread_on;
|
||||
@@ -32,9 +41,18 @@ struct reader {
|
||||
static void *sample_loop(void *arg) {
|
||||
struct reader *r = arg;
|
||||
const struct timespec gap = {0, SAMPLE_NS};
|
||||
int until_scan = 0;
|
||||
while (atomic_load(&r->run)) {
|
||||
int moved = 0;
|
||||
int busy = kgsl_busy_sample(r->map, r->first, r->last, &moved);
|
||||
int moved = 0, busy;
|
||||
if (r->mode == MODE_CONTEXTS) {
|
||||
if (until_scan-- <= 0) {
|
||||
r->live_count = kgsl_busy_scan_contexts(r->map, r->first, r->seen, r->live, KGSL_BUSY_MAX_LIVE);
|
||||
until_scan = SCAN_EVERY;
|
||||
}
|
||||
busy = kgsl_busy_sample_contexts(r->map, r->live, r->live_count, r->ctx_last, &moved);
|
||||
} else {
|
||||
busy = kgsl_busy_sample(r->map, r->first, r->last, &moved);
|
||||
}
|
||||
if (busy) atomic_fetch_add(&r->busy, 1);
|
||||
if (moved) atomic_fetch_add(&r->moves, 1);
|
||||
atomic_fetch_add(&r->samples, 1);
|
||||
@@ -62,6 +80,8 @@ static void reader_close(struct reader *r) {
|
||||
reader_run(r, 0);
|
||||
if (r->map && r->map != MAP_FAILED) munmap(r->map, r->bytes);
|
||||
if (r->fd >= 0) close(r->fd);
|
||||
free(r->seen);
|
||||
free(r->ctx_last);
|
||||
pthread_mutex_destroy(&r->lock);
|
||||
free(r);
|
||||
}
|
||||
@@ -85,7 +105,17 @@ static int reader_open(struct reader **out) {
|
||||
int moved = 0, started = 0;
|
||||
kgsl_busy_sample(r->map, r->first, r->last, &moved);
|
||||
for (int i = 0; i < KGSL_BUSY_SLOTS; i++) started |= r->last[i] != 0;
|
||||
if (!started) { reader_close(r); return -ENODATA; }
|
||||
if (started) {
|
||||
r->mode = MODE_RINGBUFFER;
|
||||
} else {
|
||||
r->seen = calloc((size_t)r->first, sizeof(uint32_t));
|
||||
r->ctx_last = calloc((size_t)r->first, sizeof(uint32_t));
|
||||
if (!r->seen || !r->ctx_last) { reader_close(r); return -ENOMEM; }
|
||||
r->live_count = kgsl_busy_scan_contexts(r->map, r->first, r->seen, r->live, KGSL_BUSY_MAX_LIVE);
|
||||
if (r->live_count == 0) { reader_close(r); return -ENODATA; }
|
||||
kgsl_busy_sample_contexts(r->map, r->live, r->live_count, r->ctx_last, &moved);
|
||||
r->mode = MODE_CONTEXTS;
|
||||
}
|
||||
*out = r;
|
||||
return 0;
|
||||
}
|
||||
@@ -103,6 +133,10 @@ JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeOpen(
|
||||
return e ? 0 : (jlong)(intptr_t)r;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeMode(JNIEnv *env, jclass cls, jlong handle) {
|
||||
return handle ? ((struct reader *)(intptr_t)handle)->mode : 0;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeRun(JNIEnv *env, jclass cls, jlong handle, jboolean on) {
|
||||
if (handle) reader_run((struct reader *)(intptr_t)handle, on ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -34,4 +34,40 @@ static inline int kgsl_busy_sample(const volatile uint32_t *memstore, int first,
|
||||
return busy;
|
||||
}
|
||||
|
||||
// Hardware-scheduled kernels (every gen8 GPU and some gen7 ones: .gpudev = *_hwsched_gpudev in adreno-gpulist.h)
|
||||
// leave the ringbuffer entries at 0. The GMU firmware writes each context's own entry instead
|
||||
// (gen8_hwsched_hfi.c send_context_pointers). Context ids run from 1 below the ringbuffer entries, lowest free first,
|
||||
// and kgsl zeroes an entry when its context is created or destroyed.
|
||||
#define KGSL_BUSY_MAX_LIVE 64
|
||||
|
||||
// Collects the contexts worth sampling: entries whose start stamp changed since the previous scan (seen[], one per
|
||||
// entry below first) or that are busy now. Returns how many ids went into live[].
|
||||
static inline int kgsl_busy_scan_contexts(const volatile uint32_t *memstore, int first, uint32_t *seen, int *live, int max_live) {
|
||||
int n = 0;
|
||||
for (int id = 1; id < first; id++) {
|
||||
const volatile uint32_t *entry = memstore + (size_t)id * KGSL_BUSY_ENTRY_WORDS;
|
||||
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
|
||||
int changed = sop != seen[id];
|
||||
seen[id] = sop;
|
||||
if ((changed || sop != eop) && n < max_live) live[n++] = id;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
// 1 when a live context has a submission between its start and end timestamps. *moved is set when a start
|
||||
// timestamp differs from last[] (indexed by context id), which then holds the new ones.
|
||||
static inline int kgsl_busy_sample_contexts(const volatile uint32_t *memstore, const int *live, int count, uint32_t *last, int *moved) {
|
||||
int busy = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const volatile uint32_t *entry = memstore + (size_t)live[i] * KGSL_BUSY_ENTRY_WORDS;
|
||||
uint32_t sop = entry[KGSL_BUSY_SOP], eop = entry[KGSL_BUSY_EOP];
|
||||
if (sop != eop) busy = 1;
|
||||
if (sop != last[live[i]]) {
|
||||
*moved = 1;
|
||||
last[live[i]] = sop;
|
||||
}
|
||||
}
|
||||
return busy;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -16,21 +16,18 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include "../adrenotools/src/hook/kgsl.h"
|
||||
#include "kgsl_layout.h"
|
||||
|
||||
#define CP_TYPE7_PKT 0x70000000u
|
||||
#define CP_NOP 0x10u
|
||||
#define CP_REG_TO_MEM 0x3eu
|
||||
#define CP_ALWAYS_ON_COUNTER 0x980u
|
||||
#define RBBM_ALWAYS_COUNT 0u
|
||||
// gen7 CP protection marks registers 0x000-0x4ff readable by command buffers (adreno-gpulist.h); a counter
|
||||
// kgsl places anywhere else is not read.
|
||||
#define READABLE_LAST 0x4ffu
|
||||
#define XO_HZ 19200000.0
|
||||
#define NOP_DWORDS 0x3fffu
|
||||
#define NOP_BLOCKS 4
|
||||
|
||||
struct sampler {
|
||||
int fd;
|
||||
const struct kgsl_layout *layout;
|
||||
unsigned context;
|
||||
unsigned counter_reg;
|
||||
struct kgsl_gpumem_alloc_id ib, out;
|
||||
@@ -60,7 +57,7 @@ static void sampler_close(struct sampler *s) {
|
||||
if (s->out.id) { struct kgsl_gpumem_free_id f = {.id = s->out.id}; ioctl(s->fd, IOCTL_KGSL_GPUMEM_FREE_ID, &f); }
|
||||
if (s->context) { struct kgsl_drawctxt_destroy d = {.drawctxt_id = s->context}; ioctl(s->fd, IOCTL_KGSL_DRAWCTXT_DESTROY, &d); }
|
||||
if (s->counter_reg) {
|
||||
struct kgsl_perfcounter_put p = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = RBBM_ALWAYS_COUNT};
|
||||
struct kgsl_perfcounter_put p = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = s->layout->cycles};
|
||||
ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_PUT, &p);
|
||||
}
|
||||
close(s->fd);
|
||||
@@ -69,7 +66,7 @@ static void sampler_close(struct sampler *s) {
|
||||
}
|
||||
|
||||
static void emit_reads(struct sampler *s, size_t *n, unsigned pass) {
|
||||
const unsigned regs[2] = {CP_ALWAYS_ON_COUNTER, s->counter_reg};
|
||||
const unsigned regs[2] = {s->layout->always_on, s->counter_reg};
|
||||
for (unsigned k = 0; k < 2; k++) {
|
||||
uint64_t dst = s->out.gpuaddr + (uint64_t)(pass * 2 + k) * sizeof(uint64_t);
|
||||
s->ib_map[(*n)++] = pkt7(CP_REG_TO_MEM, 3);
|
||||
@@ -85,10 +82,16 @@ static int sampler_open(struct sampler **out) {
|
||||
if (!s) return -ENOMEM;
|
||||
s->fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC);
|
||||
if (s->fd < 0) { int e = -errno; s->fd = -1; sampler_close(s); return e; }
|
||||
struct kgsl_perfcounter_get get = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = RBBM_ALWAYS_COUNT};
|
||||
struct kgsl_devinfo info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
struct kgsl_device_getproperty prop = {.type = KGSL_PROP_DEVICE_INFO, .value = &info, .sizebytes = sizeof(info)};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_DEVICE_GETPROPERTY, &prop)) { int e = -errno; sampler_close(s); return e; }
|
||||
s->layout = kgsl_layout_for(info.chip_id);
|
||||
if (!s->layout) { sampler_close(s); return -ENOTSUP; }
|
||||
struct kgsl_perfcounter_get get = {.groupid = KGSL_PERFCOUNTER_GROUP_RBBM, .countable = s->layout->cycles};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_GET, &get)) { int e = -errno; sampler_close(s); return e; }
|
||||
s->counter_reg = get.offset;
|
||||
if (!get.offset || get.offset > READABLE_LAST) { sampler_close(s); return -ERANGE; }
|
||||
if (!get.offset || get.offset > s->layout->readable_last) { sampler_close(s); return -ERANGE; }
|
||||
struct kgsl_drawctxt_create ctx = {.flags = KGSL_CONTEXT_NO_GMEM_ALLOC | KGSL_CONTEXT_PREAMBLE};
|
||||
if (ioctl(s->fd, IOCTL_KGSL_DRAWCTXT_CREATE, &ctx)) { int e = -errno; sampler_close(s); return e; }
|
||||
s->context = ctx.drawctxt_id;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef DROIDDECK_KGSL_LAYOUT_H
|
||||
#define DROIDDECK_KGSL_LAYOUT_H
|
||||
#include <stddef.h>
|
||||
|
||||
// Register layout per GPU generation. A counter kgsl places outside the range the kernel's CP protection lets
|
||||
// command buffers read (adreno-gpulist.h *_protected_regs) is not read.
|
||||
struct kgsl_layout {
|
||||
unsigned always_on; // CP_ALWAYS_ON_COUNTER
|
||||
unsigned cycles; // RBBM countable that counts GPU cycles
|
||||
unsigned readable_last; // last register of the readable range starting at 0
|
||||
};
|
||||
// A6xx/A7xx: CP_ALWAYS_ON_COUNTER 0x980, RBBM countable 0 = ALWAYS_COUNT, 0x000-0x4ff readable (gen7 list).
|
||||
static const struct kgsl_layout KGSL_LAYOUT_A7XX = {0x980u, 0u, 0x4ffu};
|
||||
// A8xx: CP_ALWAYS_ON_COUNTER 0x8e7 (gen8_reg.h), RBBM countable 1 = US_ALWAYS_COUNT (0 is NEVER_COUNT, Mesa
|
||||
// a8xx_perfcntrs.xml), 0x000-0x3a3 readable (gen8_0_0_protected_regs).
|
||||
static const struct kgsl_layout KGSL_LAYOUT_A8XX = {0x8e7u, 1u, 0x3a3u};
|
||||
|
||||
// The KGSL chip id's top byte: 0x44 on every A8xx (Adreno 810/829/830/840, Mesa freedreno_devices.py),
|
||||
// 0x43 or the generation number on A6xx/A7xx. Later generations are not known, so nothing is read there.
|
||||
static inline const struct kgsl_layout *kgsl_layout_for(unsigned chip_id) {
|
||||
unsigned core = chip_id >> 24;
|
||||
if (core == 0x44u) return &KGSL_LAYOUT_A8XX;
|
||||
if (core > 0x44u) return NULL;
|
||||
return &KGSL_LAYOUT_A7XX;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -193,11 +193,15 @@ class DroidDeckHudHost(context: Context) {
|
||||
metrics.snapshot()
|
||||
Thread.sleep(500)
|
||||
val mango = metrics.snapshot()
|
||||
fun shown(value: Any?, unit: String) = if (value == null) "none" else "$value$unit"
|
||||
val readers = com.droiddeck.launcher.session.GpuStatsComponent.readerStatus()
|
||||
file.writeText(metrics.also { it.setMangoReadings(null) }.buildDiagnosticsReport(context) + "\n== As the HUD shows them (MangoHud's sources) ==\n" +
|
||||
"CPU ${mango.cpuPercent}% ${mango.cpuTempC}°C, GPU ${mango.gpuPercent}% ${mango.gpuClockMhz}MHz ${mango.gpuTempC}°C, " +
|
||||
"CPU ${shown(mango.cpuPercent, "%")} ${shown(mango.cpuTempC, "°C")}, GPU ${shown(mango.gpuPercent, "%")} " +
|
||||
"${shown(mango.gpuClockMhz, "MHz")} ${shown(mango.gpuTempC, "°C")}, " +
|
||||
"RAM ${mango.ramUsedText()}/${mango.ramTotalText()}, VRAM ${mango.vramText()}, battery ${mango.battery.percent}% " +
|
||||
"${mango.battery.watts}W charging=${mango.battery.charging}\n" +
|
||||
"GPU feeds: ${com.droiddeck.launcher.session.GpuStatsComponent.sources.ifEmpty { "none (no session, or kgsl absent)" }}\n" +
|
||||
(if (readers.isNotEmpty()) "GPU readers without root: $readers\n" else "") +
|
||||
"CPU temperature zones: ${LinuxRuntime.cpuTempZones().joinToString { it.name }}\n" +
|
||||
"Frames: ${if (feed.engine.isNotEmpty() || feed.pid > 0) "gamescope (pid ${feed.pid}, ${feed.engine})" else "compositor"}, " +
|
||||
"fps ${feed.stats.fps}\n")
|
||||
|
||||
@@ -870,6 +870,8 @@ public class HudMetrics {
|
||||
|
||||
private long[] lastCoreTotal = null;
|
||||
private long[] lastCoreIdle = null;
|
||||
/** Whether the last per-core reading came from /proc/stat for every core (false: clock speed estimate). */
|
||||
private volatile boolean perCoreFromStat = false;
|
||||
|
||||
public int[] readPerCorePercent() {
|
||||
int cores = Runtime.getRuntime().availableProcessors();
|
||||
@@ -909,13 +911,16 @@ public class HudMetrics {
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
boolean fromStat = out.length > 0;
|
||||
for (int i = 0; i < out.length; i++) {
|
||||
if (out[i] >= 0) continue;
|
||||
fromStat = false;
|
||||
Long cur = readLongFromLine("/sys/devices/system/cpu/cpu" + i + "/cpufreq/scaling_cur_freq");
|
||||
Long max = readLongFromLine("/sys/devices/system/cpu/cpu" + i + "/cpufreq/cpuinfo_max_freq");
|
||||
if (cur != null && max != null && max > 0L)
|
||||
out[i] = clampPercent((int) ((Math.max(0L, Math.min(cur, max)) * 100L) / max));
|
||||
}
|
||||
perCoreFromStat = fromStat;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1009,7 +1014,8 @@ public class HudMetrics {
|
||||
cores.append('c').append(i).append('=').append(v < 0 ? "?" : (v + "%"));
|
||||
}
|
||||
if (s.perCorePercent.length > 0)
|
||||
metricHit(sb, "Per-core %", cores.toString(), "/proc/stat (per-cpu lines)");
|
||||
metricHit(sb, "Per-core %", cores.toString(), perCoreFromStat ? "/proc/stat (per-cpu lines)"
|
||||
: "scaling_cur_freq / cpuinfo_max_freq (clock estimate)");
|
||||
else
|
||||
metricMiss(sb, "Per-core %", "/proc/stat");
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ class GpuStatsComponent(val dir: File, private val root: File) : SessionPart() {
|
||||
|
||||
fun prepare(): Boolean {
|
||||
sources = ""
|
||||
KgslBusy.status = "not tried"
|
||||
KgslClock.status = "not tried"
|
||||
if (!File(KGSL).exists()) return false
|
||||
val unreadable = MANGOHUD_FILES.any { File(KGSL, it).let { f -> f.exists() && !f.canRead() } }
|
||||
// Where SELinux hides the kgsl nodes completely (exists() is false too), the layer and cycle-counter feeds stand in.
|
||||
@@ -96,6 +98,7 @@ class GpuStatsComponent(val dir: File, private val root: File) : SessionPart() {
|
||||
if (load.failed) {
|
||||
memstore?.close()
|
||||
memstore = null
|
||||
KgslBusy.status += ", but the stamps never moved"
|
||||
Log.i(TAG, "hud: kgsl memstore timestamps never moved; GPU load from Vulkan timing")
|
||||
} else if (load.working) {
|
||||
Log.i(TAG, "hud: GPU load from the kgsl memstore (all GPU work)")
|
||||
@@ -338,6 +341,12 @@ class GpuStatsComponent(val dir: File, private val root: File) : SessionPart() {
|
||||
|
||||
/** Where the running session's GPU feeds come from, for the HUD diagnostics export. */
|
||||
@Volatile var sources = ""
|
||||
|
||||
/** Why the no-root GPU readers work or not, for the HUD diagnostics export; empty where they were not needed. */
|
||||
fun readerStatus(): String =
|
||||
if (KgslBusy.status == "not tried" && KgslClock.status == "not tried") ""
|
||||
else "kgsl memstore ${KgslBusy.status}; GPU cycle counter ${KgslClock.status}"
|
||||
|
||||
private const val STALE_MS = 30_000L
|
||||
|
||||
fun guestLoadDir(root: File) = File(root, GUEST_LOAD_DIR.removePrefix("/"))
|
||||
|
||||
@@ -10,6 +10,7 @@ object KgslBusy {
|
||||
private const val TAG = "SessionService"
|
||||
|
||||
@JvmStatic private external fun nativeOpen(error: IntArray): Long
|
||||
@JvmStatic private external fun nativeMode(handle: Long): Int
|
||||
@JvmStatic private external fun nativeRun(handle: Long, on: Boolean)
|
||||
@JvmStatic private external fun nativeRead(handle: Long, out: LongArray): Boolean
|
||||
@JvmStatic private external fun nativeClose(handle: Long)
|
||||
@@ -24,7 +25,10 @@ object KgslBusy {
|
||||
if (handle != 0L) nativeRun(handle, on)
|
||||
}
|
||||
|
||||
/** Running totals: busy samples, samples, samples in which a ringbuffer started new work. */
|
||||
/** Which memstore entries are watched: [MODE_RINGBUFFER] or [MODE_CONTEXTS] (hardware-scheduled kernels). */
|
||||
val mode: Int = if (handle != 0L) runCatching { nativeMode(handle) }.getOrDefault(0) else 0
|
||||
|
||||
/** Running totals: busy samples, samples, samples in which a ringbuffer or context started new work. */
|
||||
@Synchronized fun counts(): LongArray? {
|
||||
if (handle == 0L) return null
|
||||
val out = LongArray(3)
|
||||
@@ -37,14 +41,27 @@ object KgslBusy {
|
||||
}
|
||||
}
|
||||
|
||||
const val MODE_RINGBUFFER = 1
|
||||
const val MODE_CONTEXTS = 2
|
||||
|
||||
/** What the last [open] found, for the HUD diagnostics export. */
|
||||
@Volatile var status = "not tried"
|
||||
|
||||
fun open(): Reader? {
|
||||
if (!loaded) return null
|
||||
if (!loaded) {
|
||||
status = "gpuclock library did not load"
|
||||
return null
|
||||
}
|
||||
val error = IntArray(1)
|
||||
val handle = runCatching { nativeOpen(error) }.getOrDefault(0L)
|
||||
if (handle == 0L) {
|
||||
status = "open failed: ${KgslErrors.describe(error[0])}"
|
||||
Log.i(TAG, "hud: no GPU load from the kgsl memstore (open error ${error[0]})")
|
||||
return null
|
||||
}
|
||||
return Reader(handle)
|
||||
val reader = Reader(handle)
|
||||
status = if (reader.mode == MODE_CONTEXTS) "per-context stamps (hardware-scheduled kernel)" else "ringbuffer stamps"
|
||||
Log.i(TAG, "hud: kgsl memstore reader uses $status")
|
||||
return reader
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,10 @@ object KgslClock {
|
||||
|
||||
class Sampler internal constructor(private var handle: Long) : AutoCloseable {
|
||||
/** MHz, or null when this sample was refused or failed its checks. */
|
||||
@Synchronized fun mhz(): Int? = if (handle == 0L) null else nativeSample(handle).takeIf { it > 0 }
|
||||
@Synchronized fun mhz(): Int? = sample().takeIf { it > 0 }
|
||||
|
||||
/** MHz, or a negative native code: -errno from kgsl or [RANGE_CHECK_FAILED]. */
|
||||
@Synchronized fun sample(): Int = if (handle == 0L) CLOSED else nativeSample(handle)
|
||||
|
||||
@Synchronized override fun close() {
|
||||
if (handle != 0L) nativeClose(handle)
|
||||
@@ -28,22 +31,41 @@ object KgslClock {
|
||||
}
|
||||
}
|
||||
|
||||
/** A sampler that already produced one good reading, or null (the reason goes to the log). */
|
||||
/** -EBADF, what the native side also returns for a closed handle. */
|
||||
private const val CLOSED = -9
|
||||
|
||||
/** The native sample code for a reading that failed its range checks (kgsl_clock.c sampler_mhz). */
|
||||
const val RANGE_CHECK_FAILED = -1000
|
||||
|
||||
/** What the last [open] found, for the HUD diagnostics export. */
|
||||
@Volatile var status = "not tried"
|
||||
|
||||
/** A sampler that already produced one good reading, or null (the reason goes to the log and [status]). */
|
||||
fun open(): Sampler? {
|
||||
if (!loaded) return null
|
||||
if (!loaded) {
|
||||
status = "gpuclock library did not load"
|
||||
return null
|
||||
}
|
||||
val error = IntArray(1)
|
||||
val handle = runCatching { nativeOpen(error) }.getOrDefault(0L)
|
||||
if (handle == 0L) {
|
||||
status = "open failed: ${KgslErrors.describe(error[0])}"
|
||||
Log.i(TAG, "hud: no GPU clock from the cycle counter (open error ${error[0]})")
|
||||
return null
|
||||
}
|
||||
val sampler = Sampler(handle)
|
||||
val first = (0 until 3).firstNotNullOfOrNull { sampler.mhz() }
|
||||
var code = 0
|
||||
val first = (0 until 3).firstNotNullOfOrNull {
|
||||
code = sampler.sample()
|
||||
code.takeIf { it > 0 }
|
||||
}
|
||||
if (first == null) {
|
||||
Log.i(TAG, "hud: GPU cycle counter gave no usable reading")
|
||||
status = "no usable reading: ${KgslErrors.describe(code)}"
|
||||
Log.i(TAG, "hud: GPU cycle counter gave no usable reading ($code)")
|
||||
sampler.close()
|
||||
return null
|
||||
}
|
||||
status = "working ($first MHz at start)"
|
||||
Log.i(TAG, "hud: GPU clock from the cycle counter ($first MHz now)")
|
||||
return sampler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
/** Readable text for the negative errno codes the gpuclock library returns. */
|
||||
internal object KgslErrors {
|
||||
fun describe(code: Int): String {
|
||||
if (code == 0) return "no error code"
|
||||
if (code == KgslClock.RANGE_CHECK_FAILED) return "sample out of range"
|
||||
val name = runCatching { android.system.OsConstants.errnoName(-code) }.getOrNull()
|
||||
return if (name != null) "$name ($code)" else "error $code"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class GpuReaderStatusTest {
|
||||
@After fun reset() {
|
||||
KgslBusy.status = "not tried"
|
||||
KgslClock.status = "not tried"
|
||||
}
|
||||
|
||||
@Test fun emptyWhereTheReadersWereNotNeeded() {
|
||||
assertEquals("", GpuStatsComponent.readerStatus())
|
||||
}
|
||||
|
||||
@Test fun namesBothReaders() {
|
||||
KgslBusy.status = "per-context stamps (hardware-scheduled kernel)"
|
||||
KgslClock.status = "working (680 MHz at start)"
|
||||
assertEquals(
|
||||
"kgsl memstore per-context stamps (hardware-scheduled kernel); GPU cycle counter working (680 MHz at start)",
|
||||
GpuStatsComponent.readerStatus(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test fun rangeCheckFailureIsNamed() {
|
||||
assertEquals("sample out of range", KgslErrors.describe(KgslClock.RANGE_CHECK_FAILED))
|
||||
assertEquals("no error code", KgslErrors.describe(0))
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "kgsl_busy.h"
|
||||
#include "kgsl_layout.h"
|
||||
|
||||
#define ENTRIES 819
|
||||
|
||||
@@ -52,5 +53,53 @@ int main(void) {
|
||||
assert(moved == 0);
|
||||
|
||||
puts("PASS: kgsl memstore busy");
|
||||
|
||||
// Hardware-scheduled kernels: ringbuffer entries stay 0, contexts carry the stamps.
|
||||
memset(memstore, 0, sizeof(memstore));
|
||||
static uint32_t seen[ENTRIES], ctx_last[ENTRIES];
|
||||
int live[KGSL_BUSY_MAX_LIVE];
|
||||
stamp(3, 50, 50);
|
||||
stamp(9, 21, 20);
|
||||
stamp(first - 1, 4, 4);
|
||||
int count = kgsl_busy_scan_contexts(memstore, first, seen, live, KGSL_BUSY_MAX_LIVE);
|
||||
assert(count == 3 && live[0] == 3 && live[1] == 9 && live[2] == first - 1);
|
||||
moved = 0;
|
||||
assert(kgsl_busy_sample_contexts(memstore, live, count, ctx_last, &moved) == 1);
|
||||
assert(moved == 1 && ctx_last[9] == 21);
|
||||
|
||||
// Idle and unchanged since the last scan: dropped. Busy or newly started: kept.
|
||||
stamp(9, 21, 21);
|
||||
stamp(5, 2, 1);
|
||||
count = kgsl_busy_scan_contexts(memstore, first, seen, live, KGSL_BUSY_MAX_LIVE);
|
||||
assert(count == 1 && live[0] == 5);
|
||||
moved = 0;
|
||||
assert(kgsl_busy_sample_contexts(memstore, live, count, ctx_last, &moved) == 1);
|
||||
assert(moved == 1);
|
||||
moved = 0;
|
||||
assert(kgsl_busy_sample_contexts(memstore, live, count, ctx_last, &moved) == 1);
|
||||
assert(moved == 0);
|
||||
stamp(5, 2, 2);
|
||||
assert(kgsl_busy_sample_contexts(memstore, live, count, ctx_last, &moved) == 0);
|
||||
|
||||
// Entry 0 (global) and the ringbuffer entries are not contexts; the live list is capped.
|
||||
memset(memstore, 0, sizeof(memstore));
|
||||
memset(seen, 0, sizeof(seen));
|
||||
stamp(0, 8, 7);
|
||||
stamp(first, 8, 7);
|
||||
assert(kgsl_busy_scan_contexts(memstore, first, seen, live, KGSL_BUSY_MAX_LIVE) == 0);
|
||||
for (int id = 1; id <= KGSL_BUSY_MAX_LIVE + 10; id++) stamp(id, 1, 1);
|
||||
assert(kgsl_busy_scan_contexts(memstore, first, seen, live, KGSL_BUSY_MAX_LIVE) == KGSL_BUSY_MAX_LIVE);
|
||||
puts("PASS: kgsl memstore contexts");
|
||||
|
||||
assert(kgsl_layout_for(0x44050001u) == &KGSL_LAYOUT_A8XX); // Adreno 830
|
||||
assert(kgsl_layout_for(0x44030a20u) == &KGSL_LAYOUT_A8XX); // Adreno 829
|
||||
assert(kgsl_layout_for(0x43020100u) == &KGSL_LAYOUT_A7XX); // Adreno 722
|
||||
assert(kgsl_layout_for(0x43050a01u) == &KGSL_LAYOUT_A7XX); // Adreno 740
|
||||
assert(kgsl_layout_for(0x07010000u) == &KGSL_LAYOUT_A7XX); // Adreno 710
|
||||
assert(kgsl_layout_for(0x06030001u) == &KGSL_LAYOUT_A7XX); // Adreno 630
|
||||
assert(kgsl_layout_for(0x45000000u) == NULL);
|
||||
assert(KGSL_LAYOUT_A8XX.always_on == 0x8e7u && KGSL_LAYOUT_A8XX.cycles == 1u);
|
||||
assert(KGSL_LAYOUT_A7XX.always_on == 0x980u && KGSL_LAYOUT_A7XX.cycles == 0u);
|
||||
puts("PASS: kgsl counter layout");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Exercise the HUD's kgsl memstore busy sampling (app/src/main/cpp/gpuclock/kgsl_busy.h)."""
|
||||
"""Exercise the HUD's kgsl memstore busy sampling and counter layout (app/src/main/cpp/gpuclock)."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
@@ -27,6 +27,8 @@ class KgslBusyTest(unittest.TestCase):
|
||||
result = subprocess.run([str(self.binary)], capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn('PASS: kgsl memstore busy', result.stdout)
|
||||
self.assertIn('PASS: kgsl memstore contexts', result.stdout)
|
||||
self.assertIn('PASS: kgsl counter layout', result.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user