diff --git a/app/src/main/cpp/gpuclock/kgsl_busy.c b/app/src/main/cpp/gpuclock/kgsl_busy.c index a61114793..cfa66f2c7 100644 --- a/app/src/main/cpp/gpuclock/kgsl_busy.c +++ b/app/src/main/cpp/gpuclock/kgsl_busy.c @@ -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 #include #include @@ -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); } diff --git a/app/src/main/cpp/gpuclock/kgsl_busy.h b/app/src/main/cpp/gpuclock/kgsl_busy.h index 2179cf533..f91cb5ec2 100644 --- a/app/src/main/cpp/gpuclock/kgsl_busy.h +++ b/app/src/main/cpp/gpuclock/kgsl_busy.h @@ -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 diff --git a/app/src/main/cpp/gpuclock/kgsl_clock.c b/app/src/main/cpp/gpuclock/kgsl_clock.c index b021208b4..e70a1d537 100644 --- a/app/src/main/cpp/gpuclock/kgsl_clock.c +++ b/app/src/main/cpp/gpuclock/kgsl_clock.c @@ -16,21 +16,18 @@ #include #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; diff --git a/app/src/main/cpp/gpuclock/kgsl_layout.h b/app/src/main/cpp/gpuclock/kgsl_layout.h new file mode 100644 index 000000000..f486ddfdb --- /dev/null +++ b/app/src/main/cpp/gpuclock/kgsl_layout.h @@ -0,0 +1,27 @@ +#ifndef DROIDDECK_KGSL_LAYOUT_H +#define DROIDDECK_KGSL_LAYOUT_H +#include + +// 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 diff --git a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt index a3c217e4b..46a56ff0a 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/hud/DroidDeckHudHost.kt @@ -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") diff --git a/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java b/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java index 759e01351..04fe7a3ef 100644 --- a/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java +++ b/app/src/main/java/com/droiddeck/launcher/hud/HudMetrics.java @@ -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"); diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt index 019d3b3df..ce36a9b00 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuStatsComponent.kt @@ -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("/")) diff --git a/app/src/main/java/com/droiddeck/launcher/session/KgslBusy.kt b/app/src/main/java/com/droiddeck/launcher/session/KgslBusy.kt index dad62121a..d3962dd76 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/KgslBusy.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/KgslBusy.kt @@ -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 } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/KgslClock.kt b/app/src/main/java/com/droiddeck/launcher/session/KgslClock.kt index 11c7367ef..a8089cb32 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/KgslClock.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/KgslClock.kt @@ -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 } diff --git a/app/src/main/java/com/droiddeck/launcher/session/KgslErrors.kt b/app/src/main/java/com/droiddeck/launcher/session/KgslErrors.kt new file mode 100644 index 000000000..819b62766 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/session/KgslErrors.kt @@ -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" + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/session/GpuReaderStatusTest.kt b/app/src/test/java/com/droiddeck/launcher/session/GpuReaderStatusTest.kt new file mode 100644 index 000000000..c7e6867cc --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/session/GpuReaderStatusTest.kt @@ -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)) + } +} diff --git a/tools/tests/kgsl_busy_harness.c b/tools/tests/kgsl_busy_harness.c index 14e96bf64..dd57ed223 100644 --- a/tools/tests/kgsl_busy_harness.c +++ b/tools/tests/kgsl_busy_harness.c @@ -3,6 +3,7 @@ #include #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; } diff --git a/tools/tests/test_kgsl_busy.py b/tools/tests/test_kgsl_busy.py index 7655b9873..6b75f56b5 100644 --- a/tools/tests/test_kgsl_busy.py +++ b/tools/tests/test_kgsl_busy.py @@ -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__':