feat(hud): GPU load and clock without root where Android blocks kgsl (#1969)

On some Adreno devices (the Retroid Pocket Duo) SELinux hides kgsl's sysfs
nodes from apps, so the HUD showed no GPU % and no GPU clock. Devices whose
nodes are readable keep using them; nothing changes there or on GPUs
without kgsl.

Where the nodes are blocked:

- GPU %: a native reader maps kgsl's memstore read-only (any kgsl client
  may) and samples the ringbuffer start/end timestamps once a millisecond
  while a HUD is shown. That covers all GPU work, Android's own
  composition included.
- Backup for GPU %: an implicit Vulkan layer in the guest
  (tools/gpuload) and the compositor time their own GPU work with
  timestamp queries. It runs until the memstore stamps are seen moving,
  and for the whole session if they never move.
- GPU clock: a small GPU job of our own reads the always-on and RBBM
  cycle counters (libgpuclock).

The HUD diagnostics export lists which source each GPU value came from.
tools/gpuload/build.sh takes the Vulkan headers from build_local.sh's NDK
first and keeps NDK paths with spaces intact.
This commit is contained in:
The412Banner authored and GitHub committed 2026-11-01 20:22:51 +00:00
1 parent 6cd844ea51
commit 8cb5c2168e
30 files changed
+1891 -10

No files matched your search

+2
View File
@@ -37,6 +37,8 @@ target_link_libraries(sdnative log)
add_library(deviceinfo SHARED deviceinfo/vkinfo.c)
target_link_libraries(deviceinfo dl)
add_library(gpuclock SHARED gpuclock/kgsl_clock.c gpuclock/kgsl_busy.c)
add_library(termux SHARED termux/termux.c)
add_library(ddvenc SHARED venc/venc_server.cpp)
+121
View File
@@ -0,0 +1,121 @@
// 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.
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include "../adrenotools/src/hook/kgsl.h"
#include "kgsl_busy.h"
#define SAMPLE_NS 1000000L
struct reader {
int fd;
void *map;
size_t bytes;
int first;
uint32_t last[KGSL_BUSY_SLOTS];
pthread_mutex_t lock;
pthread_t thread;
int thread_on;
atomic_int run;
atomic_uint_fast64_t busy, samples, moves;
};
static void *sample_loop(void *arg) {
struct reader *r = arg;
const struct timespec gap = {0, SAMPLE_NS};
while (atomic_load(&r->run)) {
int moved = 0;
int 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);
nanosleep(&gap, NULL);
}
return NULL;
}
static void reader_run(struct reader *r, int on) {
pthread_mutex_lock(&r->lock);
if (on && !r->thread_on) {
atomic_store(&r->run, 1);
if (pthread_create(&r->thread, NULL, sample_loop, r) == 0) r->thread_on = 1;
else atomic_store(&r->run, 0);
} else if (!on && r->thread_on) {
atomic_store(&r->run, 0);
pthread_join(r->thread, NULL);
r->thread_on = 0;
}
pthread_mutex_unlock(&r->lock);
}
static void reader_close(struct reader *r) {
if (!r) return;
reader_run(r, 0);
if (r->map && r->map != MAP_FAILED) munmap(r->map, r->bytes);
if (r->fd >= 0) close(r->fd);
pthread_mutex_destroy(&r->lock);
free(r);
}
// Returns 0 or a negative errno; *out is set on success.
static int reader_open(struct reader **out) {
struct reader *r = calloc(1, sizeof(*r));
if (!r) return -ENOMEM;
pthread_mutex_init(&r->lock, NULL);
r->fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC);
if (r->fd < 0) { int e = -errno; reader_close(r); return e; }
struct kgsl_shadowprop shadow;
memset(&shadow, 0, sizeof(shadow));
struct kgsl_device_getproperty prop = {.type = KGSL_PROP_DEVICE_SHADOW, .value = &shadow, .sizebytes = sizeof(shadow)};
if (ioctl(r->fd, IOCTL_KGSL_DEVICE_GETPROPERTY, &prop)) { int e = -errno; reader_close(r); return e; }
r->bytes = shadow.size;
r->first = kgsl_busy_first_slot(shadow.size);
if (r->first < 0) { reader_close(r); return -ERANGE; }
r->map = mmap(NULL, r->bytes, PROT_READ, MAP_SHARED, r->fd, (off_t)shadow.gpuaddr);
if (r->map == MAP_FAILED) { int e = -errno; reader_close(r); return e; }
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; }
*out = r;
return 0;
}
#ifndef KGSL_BUSY_NO_JNI
#include <jni.h>
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeOpen(JNIEnv *env, jclass cls, jintArray error) {
struct reader *r = NULL;
int e = reader_open(&r);
if (e && error && (*env)->GetArrayLength(env, error) > 0) {
jint v = e;
(*env)->SetIntArrayRegion(env, error, 0, 1, &v);
}
return e ? 0 : (jlong)(intptr_t)r;
}
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);
}
JNIEXPORT jboolean JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeRead(JNIEnv *env, jclass cls, jlong handle, jlongArray out) {
if (!handle || !out || (*env)->GetArrayLength(env, out) < 3) return JNI_FALSE;
struct reader *r = (struct reader *)(intptr_t)handle;
jlong v[3] = {(jlong)atomic_load(&r->busy), (jlong)atomic_load(&r->samples), (jlong)atomic_load(&r->moves)};
(*env)->SetLongArrayRegion(env, out, 0, 3, v);
return JNI_TRUE;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslBusy_nativeClose(JNIEnv *env, jclass cls, jlong handle) {
reader_close((struct reader *)(intptr_t)handle);
}
#endif
+37
View File
@@ -0,0 +1,37 @@
#ifndef DROIDDECK_KGSL_BUSY_H
#define DROIDDECK_KGSL_BUSY_H
// kgsl lets any client map its memstore read-only (KGSL_PROP_DEVICE_SHADOW). The CP writes a ringbuffer's
// start-of-pipe timestamp there before each submission's command buffers and its end-of-pipe timestamp after them
// (adreno_gen7_ringbuffer.c gen7_ringbuffer_addcmds), so start != end means that ringbuffer has work on the GPU.
// Ringbuffer entries start at KGSL_MEMSTORE_MAX: entries - 2 - 4 levels, or entries - 1 - 4 on kernels without the
// LPAC slot. Entries - 6 .. entries - 2 cover both; a context entry in that range follows the same start/end rule.
#include <stddef.h>
#include <stdint.h>
#define KGSL_BUSY_ENTRY_WORDS 10
#define KGSL_BUSY_SOP 0
#define KGSL_BUSY_EOP 2
#define KGSL_BUSY_SLOTS 5
static inline int kgsl_busy_first_slot(size_t bytes) {
size_t entries = bytes / (KGSL_BUSY_ENTRY_WORDS * sizeof(uint32_t));
return entries < 8 ? -1 : (int)(entries - 6);
}
// 1 when a watched slot has a submission between its start and end timestamps. *moved is set when a start
// timestamp differs from last[], which then holds the new ones.
static inline int kgsl_busy_sample(const volatile uint32_t *memstore, int first, uint32_t last[KGSL_BUSY_SLOTS], int *moved) {
int busy = 0;
for (int i = 0; i < KGSL_BUSY_SLOTS; i++) {
const volatile uint32_t *entry = memstore + (size_t)(first + 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[i]) {
*moved = 1;
last[i] = sop;
}
}
return busy;
}
#endif
+156
View File
@@ -0,0 +1,156 @@
// GPU clock without root on Adreno (KGSL) devices whose kgsl sysfs is blocked for apps.
//
// kgsl lets any app reserve a performance counter (IOCTL_KGSL_PERFCOUNTER_GET) but refuses to read it back while
// /sys/class/kgsl/kgsl-3d0/perfcounter is 0, and clears the counters whenever the GPU switches to another process.
// So the counter is read by the GPU itself: a small command buffer copies CP_ALWAYS_ON_COUNTER (19.2 MHz) and the
// RBBM ALWAYS_COUNT counter (GPU core cycles) to memory, runs a block of NOP packets, and copies both again.
// clock = cycles / always-on ticks * 19.2 MHz. Both reads sit in one command buffer, so a counter clear between
// them only happens if the buffer is preempted; such samples fail the range checks and are dropped.
#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#include "../adrenotools/src/hook/kgsl.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;
unsigned context;
unsigned counter_reg;
struct kgsl_gpumem_alloc_id ib, out;
uint32_t *ib_map;
volatile uint64_t *out_map;
size_t ib_bytes;
};
static unsigned odd_parity(unsigned v) {
v ^= v >> 16;
v ^= v >> 8;
v ^= v >> 4;
v &= 0xf;
return (~0x6996u >> v) & 1u;
}
static uint32_t pkt7(unsigned op, unsigned count) {
return CP_TYPE7_PKT | count | odd_parity(count) << 15 | (op & 0x7f) << 16 | odd_parity(op) << 23;
}
static void sampler_close(struct sampler *s) {
if (!s) return;
if (s->fd >= 0) {
if (s->ib_map && s->ib_map != MAP_FAILED) munmap(s->ib_map, s->ib.mmapsize);
if (s->out_map && s->out_map != MAP_FAILED) munmap((void *)s->out_map, s->out.mmapsize);
if (s->ib.id) { struct kgsl_gpumem_free_id f = {.id = s->ib.id}; ioctl(s->fd, IOCTL_KGSL_GPUMEM_FREE_ID, &f); }
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};
ioctl(s->fd, IOCTL_KGSL_PERFCOUNTER_PUT, &p);
}
close(s->fd);
}
free(s);
}
static void emit_reads(struct sampler *s, size_t *n, unsigned pass) {
const unsigned regs[2] = {CP_ALWAYS_ON_COUNTER, 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);
s->ib_map[(*n)++] = regs[k] | (2u << 18) | (1u << 30); // two dwords, 64-bit read
s->ib_map[(*n)++] = (uint32_t)dst;
s->ib_map[(*n)++] = (uint32_t)(dst >> 32);
}
}
// Returns 0 or a negative errno; *out is set on success.
static int sampler_open(struct sampler **out) {
struct sampler *s = calloc(1, sizeof(*s));
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};
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; }
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;
s->ib_bytes = (16 + NOP_BLOCKS * (NOP_DWORDS + 1)) * sizeof(uint32_t);
s->ib.size = (s->ib_bytes + 4095) & ~(size_t)4095;
s->out.size = 4096;
if (ioctl(s->fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &s->ib)) { int e = -errno; s->ib.id = 0; sampler_close(s); return e; }
if (ioctl(s->fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &s->out)) { int e = -errno; s->out.id = 0; sampler_close(s); return e; }
s->ib_map = mmap(NULL, s->ib.mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, (off_t)s->ib.id << 12);
s->out_map = mmap(NULL, s->out.mmapsize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, (off_t)s->out.id << 12);
if (s->ib_map == MAP_FAILED || s->out_map == MAP_FAILED) { int e = -errno; sampler_close(s); return e; }
size_t n = 0;
emit_reads(s, &n, 0);
for (unsigned b = 0; b < NOP_BLOCKS; b++) {
s->ib_map[n++] = pkt7(CP_NOP, NOP_DWORDS);
memset(&s->ib_map[n], 0, NOP_DWORDS * sizeof(uint32_t));
n += NOP_DWORDS;
}
emit_reads(s, &n, 1);
s->ib_bytes = n * sizeof(uint32_t);
*out = s;
return 0;
}
// MHz, or a negative value: -errno from kgsl, or -1000 for a sample that failed the range checks.
static int sampler_mhz(struct sampler *s) {
for (int i = 0; i < 4; i++) s->out_map[i] = 0;
struct kgsl_command_object cmd = {.gpuaddr = s->ib.gpuaddr, .size = s->ib_bytes, .flags = KGSL_CMDLIST_IB};
struct kgsl_gpu_command submit = {
.cmdlist = (uint64_t)(uintptr_t)&cmd, .cmdsize = sizeof(cmd), .numcmds = 1, .context_id = s->context,
};
if (ioctl(s->fd, IOCTL_KGSL_GPU_COMMAND, &submit)) return -errno;
struct kgsl_device_waittimestamp_ctxtid wait = {.context_id = s->context, .timestamp = submit.timestamp, .timeout = 250};
if (ioctl(s->fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait)) return -errno;
uint64_t xo0 = s->out_map[0], cyc0 = s->out_map[1], xo1 = s->out_map[2], cyc1 = s->out_map[3];
if (xo1 <= xo0 || cyc1 <= cyc0) return -1000;
uint64_t xo = xo1 - xo0, cycles = cyc1 - cyc0;
// 5 us .. 50 ms of always-on time, and a clock between 50 MHz and 2 GHz.
if (xo < 96 || xo > 960000) return -1000;
double mhz = (double)cycles / (double)xo * XO_HZ / 1e6;
if (mhz < 50 || mhz > 2000) return -1000;
return (int)(mhz + 0.5);
}
#ifndef KGSL_CLOCK_NO_JNI
#include <jni.h>
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeOpen(JNIEnv *env, jclass cls, jintArray error) {
struct sampler *s = NULL;
int r = sampler_open(&s);
if (r && error && (*env)->GetArrayLength(env, error) > 0) {
jint e = r;
(*env)->SetIntArrayRegion(env, error, 0, 1, &e);
}
return r ? 0 : (jlong)(intptr_t)s;
}
JNIEXPORT jint JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeSample(JNIEnv *env, jclass cls, jlong handle) {
return handle ? sampler_mhz((struct sampler *)(intptr_t)handle) : -EBADF;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_session_KgslClock_nativeClose(JNIEnv *env, jclass cls, jlong handle) {
sampler_close((struct sampler *)(intptr_t)handle);
}
#endif
@@ -18,6 +18,7 @@ add_library(droiddeckwayland SHARED
src/wl_color_mgmt.c
src/hdr_compose.c
src/blend_pass.c
src/gpu_busy.c
generated/color-management-v1-protocol.c
generated/xdg-shell-protocol.c
generated/linux-dmabuf-v1-protocol.c
@@ -43,6 +44,7 @@ target_include_directories(droiddeckwayland PRIVATE
prebuilt/include
src
${CMAKE_CURRENT_SOURCE_DIR}/../shaders
${CMAKE_CURRENT_SOURCE_DIR}/../../../../../tools/gpuload/src
)
set(DROIDDECK_FRAMEGEN_LSFG_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../framegen/lsfg)
@@ -0,0 +1,89 @@
#include "gpu_busy.h"
#include <jni.h>
#include <pthread.h>
#include <stdatomic.h>
#include "gpuload_ring.h"
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static struct gpuload_vk g_v;
static struct gpuload_ring g_ring;
static uint32_t g_family, g_bits;
static PFN_vkQueueSubmit g_submit;
static int g_was_on;
static _Atomic int g_on, g_lost, g_timing;
static _Atomic uint64_t g_busy_ns;
static VkResult VKAPI_CALL timed_submit(VkQueue queue, uint32_t count, const VkSubmitInfo *infos, VkFence fence) {
if (!count || !atomic_load(&g_on) || atomic_load(&g_lost)) return g_submit(queue, count, infos, fence);
pthread_mutex_lock(&g_lock);
if (!atomic_load(&g_on) || atomic_load(&g_lost)) {
pthread_mutex_unlock(&g_lock);
return g_submit(queue, count, infos, fence);
}
if (g_ring.state == 0 && gpuload_ring_setup(&g_ring, &g_v, g_family, g_bits)) atomic_store(&g_timing, 1);
VkSubmitInfo *mine = NULL;
uint32_t used = 0;
if (g_ring.state == 1) {
uint64_t ns = gpuload_ring_harvest(&g_ring, &g_v);
if (g_was_on) atomic_fetch_add(&g_busy_ns, ns); // the first harvest after a pause holds work timed before it
g_was_on = 1;
mine = gpuload_ring_wrap(&g_ring, count, infos, &used);
}
VkResult r = g_submit(queue, count, mine ? mine : infos, fence);
if (mine && r != VK_SUCCESS) gpuload_ring_unwind(&g_ring, used);
pthread_mutex_unlock(&g_lock);
free(mine);
return r;
}
void vkp_gpu_busy_device(VkPhysicalDevice pd, VkDevice dev, uint32_t family) {
pthread_mutex_lock(&g_lock);
memset(&g_ring, 0, sizeof(g_ring));
g_was_on = 0;
atomic_store(&g_timing, 0);
atomic_store(&g_lost, 0);
PFN_vkGetDeviceProcAddr gdpa = g_vk.GetDeviceProcAddr;
g_v = (struct gpuload_vk){
.device = dev,
.create_pool = g_vk.CreateCommandPool,
.destroy_pool = (PFN_vkDestroyCommandPool)gdpa(dev, "vkDestroyCommandPool"),
.alloc_cmds = g_vk.AllocateCommandBuffers,
.begin = g_vk.BeginCommandBuffer,
.end = g_vk.EndCommandBuffer,
.cmd_reset = (PFN_vkCmdResetQueryPool)gdpa(dev, "vkCmdResetQueryPool"),
.cmd_timestamp = (PFN_vkCmdWriteTimestamp)gdpa(dev, "vkCmdWriteTimestamp"),
.create_queries = (PFN_vkCreateQueryPool)gdpa(dev, "vkCreateQueryPool"),
.destroy_queries = (PFN_vkDestroyQueryPool)gdpa(dev, "vkDestroyQueryPool"),
.results = (PFN_vkGetQueryPoolResults)gdpa(dev, "vkGetQueryPoolResults"),
};
VkPhysicalDeviceProperties props;
g_vk.GetPhysicalDeviceProperties(pd, &props);
gpuload_vk_period(&g_v, props.limits.timestampPeriod);
VkQueueFamilyProperties families[16];
uint32_t n = 16;
g_vk.GetPhysicalDeviceQueueFamilyProperties(pd, &n, families);
g_family = family;
g_bits = family < n ? families[family].timestampValidBits : 0;
// vk_loader_load_device has just put the driver's function back.
if (g_vk.QueueSubmit != timed_submit) g_submit = g_vk.QueueSubmit;
if (g_submit) g_vk.QueueSubmit = timed_submit;
pthread_mutex_unlock(&g_lock);
}
void vkp_gpu_busy_device_lost(void) {
atomic_store(&g_lost, 1);
atomic_store(&g_timing, 0);
}
JNIEXPORT jlong JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeGpuBusyNs(JNIEnv *env, jclass cls) {
return atomic_load(&g_timing) ? (jlong)atomic_load(&g_busy_ns) : -1;
}
JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTiming(JNIEnv *env, jclass cls, jboolean on) {
pthread_mutex_lock(&g_lock);
if (!on) g_was_on = 0;
atomic_store(&g_on, on ? 1 : 0);
pthread_mutex_unlock(&g_lock);
}
@@ -0,0 +1,9 @@
#ifndef GPU_BUSY_H
#define GPU_BUSY_H
#include "vk_loader.h"
// Times the compositor's own GPU work for the HUD (tools/gpuload/src/gpuload_ring.h); call after the device and its
// functions are loaded. Off until WaylandCompositor.nativeSetGpuTiming(true).
void vkp_gpu_busy_device(VkPhysicalDevice pd, VkDevice dev, uint32_t family);
void vkp_gpu_busy_device_lost(void);
#endif
@@ -7,6 +7,7 @@
#include "framegen_bridge.h"
#include "hdr_compose.h"
#include "blend_pass.h"
#include "gpu_busy.h"
#include "droiddeck_color.h"
#include "quest_device.h"
#include <stdio.h>
@@ -517,6 +518,7 @@ static int dev_init(void) {
}
vk_loader_load_device(g_dev);
g_vk.GetDeviceQueue(g_dev, g_qfam, 0, &g_queue);
vkp_gpu_busy_device(g_pd, g_dev, g_qfam);
if (want_sem_fd) {
g_import_sem_fd = (PFN_vkImportSemaphoreFdKHR)g_vk.GetDeviceProcAddr(g_dev, "vkImportSemaphoreFdKHR");
VkSemaphoreCreateInfo wsci = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
@@ -1311,6 +1313,7 @@ static void device_lost(const char *where) {
g_dev_state = -2;
g_base_black = 0;
vkp_framegen_device_lost();
vkp_gpu_busy_device_lost();
droiddeck_log("error", "GPU device lost (VK_ERROR_DEVICE_LOST in %s): the compositor has stopped presenting; "
"restart the session", where);
for (uint32_t i = 0; i < g_nimg; i++) blendp_forget_image(g_images[i]);
@@ -213,6 +213,7 @@ class DroidDeckHudHost(context: Context) {
"CPU ${mango.cpuPercent}% ${mango.cpuTempC}°C, GPU ${mango.gpuPercent}% ${mango.gpuClockMhz}MHz ${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" +
"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")
@@ -7,10 +7,16 @@ import com.droiddeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.io.RandomAccessFile
class GpuStatsComponent(val dir: File) : SessionPart() {
class GpuStatsComponent(val dir: File, private val root: File) : SessionPart() {
@Volatile private var running = false
private var thread: Thread? = null
private val feeds = ArrayList<Feed>()
private var layerLoad: LayerLoad? = null
private var memstore: KgslBusy.Reader? = null
private var memstoreLoad: MemstoreLoad? = null
private var memstoreState: String? = null
private var cycleClock: KgslClock.Sampler? = null
@Volatile private var shown = false
private class Feed(
val file: File, val source: String, val read: () -> Long?, val format: String, val percent: Boolean = false,
@@ -24,9 +30,11 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
}
fun prepare(): Boolean {
sources = ""
if (!File(KGSL).exists()) return false
val unreadable = MANGOHUD_FILES.any { File(KGSL, it).let { f -> f.exists() && !f.canRead() } }
if (!unreadable && !File(GPUBUSY).canRead()) return false
// Where SELinux hides the kgsl nodes completely (exists() is false too), the layer and cycle-counter feeds stand in.
if (!unreadable && !File(GPUBUSY).canRead() && !loadBlocked()) return false
return try {
dir.mkdirs()
dir.listFiles()?.forEach { java.nio.file.Files.deleteIfExists(it.toPath()) }
@@ -34,19 +42,18 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
PASSTHROUGH_FILES.forEach { name ->
runCatching { File(dir, name).writeText(File(KGSL, name).readText()) }
}
loadFeed()?.let { load ->
(loadFeed() ?: blockedLoadFeed())?.let { load ->
feeds.add(load)
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 ->
(pick("clock_mhz", clockSources(), "%5d\n") { mhz(it) } ?: cycleClockFeed())?.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) }
refresh()
Log.i(TAG, "hud: kgsl stats from the session's own: " + MANGOHUD_FILES.joinToString { name ->
"$name " + (feeds.firstOrNull { it.file.name == name }?.source ?: "none")
})
describeSources()
Log.i(TAG, "hud: kgsl stats from the session's own: $sources")
true
} catch (e: Exception) {
Log.w(TAG, "hud: could not stand in for kgsl stats: $e")
@@ -68,17 +75,212 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
// kgsl's gpubusy can read 0 0 straight after a read, so feeds that share a reader share one read.
private fun refresh() {
if (layerLoad != null || cycleClock != null || memstore != null) {
shown = hudShown()
memstore?.run(shown)
if (!shown) memstoreLoad?.pause()
layerLoad?.wanted(shown && memstoreLoad?.working != true)
}
val values = HashMap<() -> Long?, Long?>()
for (feed in feeds) {
if (feed.read !in values) values[feed.read] = feed.read()
values[feed.read]?.let { write(feed, it) }
}
memstoreLoad?.let { load ->
val state = when {
load.working -> "working"
load.failed -> "failed"
else -> null
}
if (state != memstoreState) {
memstoreState = state
if (load.failed) {
memstore?.close()
memstore = null
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)")
}
describeSources()
}
}
}
private fun describeSources() {
sources = MANGOHUD_FILES.joinToString { name ->
val source = if (name == "gpu_busy_percentage" && memstoreLoad != null) memstoreSource()
else feeds.firstOrNull { it.file.name == name }?.source
"$name " + (source ?: "none")
}
}
private fun memstoreSource(): String = when {
memstoreLoad?.working == true -> "kgsl memstore ringbuffer timestamps (all GPU work)"
memstoreLoad?.failed == true -> LAYER_SOURCE
else -> "kgsl memstore once its timestamps move; until then $LAYER_SOURCE"
}
override fun stop() {
running = false
thread?.interrupt()
thread = null
memstore?.close()
memstore = null
memstoreLoad = null
memstoreState = null
layerLoad?.clear()
cycleClock?.close()
cycleClock = null
sources = ""
}
private fun hudShown(): Boolean = OverlayShown.check(root) || com.droiddeck.launcher.hud.DroidDeckHudHost.showing
// Android hides the load (kgsl sysfs blocked): the kgsl memstore gives the whole GPU's busy share. The in-game
// Vulkan layer and the compositor time their own GPU work until the memstore timestamps are seen moving, and
// for the whole session where they never move.
private fun blockedLoadFeed(): Feed? {
if (!loadBlocked()) return null
val load = LayerLoad(guestLoadDir(root), extra = ::compositorBusyNs, onWanted = ::compositorTiming)
.also { it.clear(); layerLoad = it }
val sampled = KgslBusy.open()?.let { reader ->
memstore = reader
MemstoreLoad(reader::counts).also { memstoreLoad = it }
}
val read = read@{
if (!shown) return@read null
val whole = sampled?.percent()
if (sampled?.working == true) whole else load.percent()
}
return Feed(File(dir, "gpu_busy_percentage"), LAYER_SOURCE, read, "%3d %%\n")
}
// The clock nodes are blocked too: read the GPU's cycle counter from a small GPU job of our own.
private fun cycleClockFeed(): Feed? {
val sampler = KgslClock.open() ?: return null
cycleClock = sampler
// clock_mhz and cur_freq both read this; one GPU job per refresh serves both.
var at = 0L
var last: Long? = null
val read = {
val now = SystemClock.elapsedRealtime()
if (now - at > PERIOD_MS / 2) { at = now; last = if (shown) sampler.mhz()?.toLong() else null }
last
}
return Feed(File(dir, "clock_mhz"), "GPU cycle counter (own GPU job)", read, "%5d\n")
}
private fun compositorBusyNs(): Long? =
runCatching { com.droiddeck.launcher.wayland.WaylandCompositor.nativeGpuBusyNs() }.getOrNull()?.takeIf { it >= 0 }
private fun compositorTiming(on: Boolean) {
runCatching { com.droiddeck.launcher.wayland.WaylandCompositor.nativeSetGpuTiming(on) }
}
/**
* Busy time the layer (tools/gpuload) writes per process, plus [extra] (the compositor's own GPU time), as a share
* of wall time between two reads.
*/
internal class LayerLoad(
private val dir: File,
private val clock: () -> Long = System::nanoTime,
private val extra: () -> Long? = { null },
private val onWanted: (Boolean) -> Unit = {},
) {
private val last = HashMap<String, Long>()
private var lastAt = 0L
private var on = false
fun wanted(on: Boolean) {
val flag = File(dir, "on")
if (on && !flag.exists()) runCatching { dir.mkdirs(); flag.createNewFile() }
if (!on && flag.exists()) { flag.delete(); last.clear(); lastAt = 0L }
if (on != this.on) onWanted(on)
this.on = on
}
fun percent(): Long? {
val now = clock()
val files = dir.listFiles { f -> f.name.isNotEmpty() && f.name.all(Char::isDigit) }.orEmpty()
var busy = 0L
val seen = HashSet<String>()
for (file in files) {
val total = runCatching { file.readText().trim().substringBefore(' ').toLong() }.getOrNull() ?: continue
seen += file.name
last[file.name]?.let { if (total >= it) busy += total - it }
last[file.name] = total
if (System.currentTimeMillis() - file.lastModified() > STALE_MS) file.delete()
}
extra()?.let { total ->
seen += EXTRA
last[EXTRA]?.let { if (total >= it) busy += total - it }
last[EXTRA] = total
}
last.keys.retainAll(seen)
val wall = now - lastAt
val first = lastAt == 0L
lastAt = now
if (first || wall <= 0) return null
return (busy * 100 / wall).coerceIn(0, 100)
}
fun clear() {
dir.listFiles()?.forEach { it.delete() }
last.clear()
lastAt = 0L
if (on) onWanted(false)
on = false
}
private companion object {
const val EXTRA = "compositor"
}
}
/**
* Busy share over the last [WINDOW] reads from [KgslBusy] running totals (busy samples, samples, samples with
* new ringbuffer work). Not [working] until new work has been seen; [failed] once [GIVE_UP_SAMPLES] samples
* passed without any.
*/
internal class MemstoreLoad(private val counts: () -> LongArray?) {
var working = false
private set
var failed = false
private set
private val window = ArrayDeque<LongArray>()
private var waited = 0L
fun percent(): Long? {
if (failed) return null
val now = counts() ?: return null
val previous = window.lastOrNull()
window.addLast(now)
while (window.size > WINDOW + 1) window.removeFirst()
if (previous == null) return null
val fresh = now[1] - previous[1]
if (fresh <= 0) return null
if (!working) {
if (now[2] > previous[2]) {
working = true
} else {
waited += fresh
if (waited >= GIVE_UP_SAMPLES) failed = true
return null
}
}
val base = window.first()
return ((now[0] - base[0]) * 100 / (now[1] - base[1])).coerceIn(0, 100)
}
/** The totals stop while sampling is off; the next reading starts a new window. */
fun pause() {
window.clear()
}
companion object {
const val GIVE_UP_SAMPLES = 5_000L
const val WINDOW = 2
}
}
private fun loadFeed(): Feed? {
@@ -134,6 +336,24 @@ class GpuStatsComponent(val dir: File) : SessionPart() {
private const val LOAD_HOLD_MS = 5_000L
private val MANGOHUD_FILES = listOf("gpu_busy_percentage", "clock_mhz", "temp")
private val PASSTHROUGH_FILES = listOf("gpu_model", "gpu_chipid")
const val GUEST_LOAD_DIR = "/run/droiddeck-hud/gpu-load"
private const val LAYER_SOURCE = "Vulkan timing: games ($GUEST_LOAD_DIR) + DroidDeck compositor"
/** Where the running session's GPU feeds come from, for the HUD diagnostics export. */
@Volatile var sources = ""
private const val STALE_MS = 30_000L
fun guestLoadDir(root: File) = File(root, GUEST_LOAD_DIR.removePrefix("/"))
/** True when this app can see kgsl but none of its GPU load nodes (SELinux-blocked, e.g. the Retroid Pocket Duo). */
fun loadBlocked(): Boolean = File(KGSL).exists() && !File(GPUBUSY).canRead() && LOAD_SOURCES.none { File(it).canRead() }
/** Turns on the in-game layer (tools/gpuload) where the load is blocked; empty elsewhere. */
fun guestEnv(root: File): List<String> {
if (!loadBlocked()) return emptyList()
guestLoadDir(root).mkdirs()
return listOf("DROIDDECK_GPU_LOAD=1", "DROIDDECK_GPU_LOAD_DIR=$GUEST_LOAD_DIR")
}
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",
@@ -0,0 +1,50 @@
package com.droiddeck.launcher.session
import android.util.Log
/**
* Whole-GPU busy share for Adreno devices whose kgsl load nodes are SELinux-blocked for apps
* (app/src/main/cpp/gpuclock/kgsl_busy.c). It covers every process's GPU work, Android's own composition included.
*/
object KgslBusy {
private const val TAG = "SessionService"
@JvmStatic private external fun nativeOpen(error: IntArray): Long
@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)
private val loaded: Boolean by lazy {
runCatching { System.loadLibrary("gpuclock") }.onFailure { Log.w(TAG, "hud: gpuclock library: $it") }.isSuccess
}
class Reader internal constructor(private var handle: Long) : AutoCloseable {
/** Samples once a millisecond while on. */
@Synchronized fun run(on: Boolean) {
if (handle != 0L) nativeRun(handle, on)
}
/** Running totals: busy samples, samples, samples in which a ringbuffer started new work. */
@Synchronized fun counts(): LongArray? {
if (handle == 0L) return null
val out = LongArray(3)
return if (nativeRead(handle, out)) out else null
}
@Synchronized override fun close() {
if (handle != 0L) nativeClose(handle)
handle = 0L
}
}
fun open(): Reader? {
if (!loaded) return null
val error = IntArray(1)
val handle = runCatching { nativeOpen(error) }.getOrDefault(0L)
if (handle == 0L) {
Log.i(TAG, "hud: no GPU load from the kgsl memstore (open error ${error[0]})")
return null
}
return Reader(handle)
}
}
@@ -0,0 +1,50 @@
package com.droiddeck.launcher.session
import android.util.Log
/**
* GPU clock from the GPU's own cycle counter, for Adreno devices whose kgsl clock nodes are SELinux-blocked for
* apps (app/src/main/cpp/gpuclock/kgsl_clock.c). Each [mhz] call runs one tiny GPU job, so callers sample only
* while a HUD is shown.
*/
object KgslClock {
private const val TAG = "SessionService"
@JvmStatic private external fun nativeOpen(error: IntArray): Long
@JvmStatic private external fun nativeSample(handle: Long): Int
@JvmStatic private external fun nativeClose(handle: Long)
private val loaded: Boolean by lazy {
runCatching { System.loadLibrary("gpuclock") }.onFailure { Log.w(TAG, "hud: gpuclock library: $it") }.isSuccess
}
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 override fun close() {
if (handle != 0L) nativeClose(handle)
handle = 0L
}
}
/** A sampler that already produced one good reading, or null (the reason goes to the log). */
fun open(): Sampler? {
if (!loaded) return null
val error = IntArray(1)
val handle = runCatching { nativeOpen(error) }.getOrDefault(0L)
if (handle == 0L) {
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() }
if (first == null) {
Log.i(TAG, "hud: GPU cycle counter gave no usable reading")
sampler.close()
return null
}
Log.i(TAG, "hud: GPU clock from the cycle counter ($first MHz now)")
return sampler
}
}
@@ -160,6 +160,11 @@ object SessionFiles {
) + handModels).map { "usr/local/lib/droiddeck-vr/$it" to "usr/local/lib/droiddeck-vr/$it" } +
listOf("usr/local/lib/droiddeck-vr/layer/VK_LAYER_DROIDDECK_vr_implicit.json" to
"usr/share/vulkan/implicit_layer.d/droiddeck_vr.json")
// HUD GPU load where Android hides it (GpuStatsComponent.guestEnv turns the layer on).
val gpuLoad = listOf(
"usr/local/lib/droiddeck-gpuload/libVkLayer_droiddeck_gpuload.so" to "usr/local/lib/droiddeck-gpuload/libVkLayer_droiddeck_gpuload.so",
"usr/local/lib/droiddeck-gpuload/VK_LAYER_DROIDDECK_gpu_load.json" to "usr/share/vulkan/implicit_layer.d/droiddeck_gpu_load.json",
)
val fexPreloads = listOf("x86_64", "i386").flatMap { arch ->
listOf("libblsession.so", "libfakeinput.so", "libfaultreport.so").map { "$arch/$it" to "usr/local/lib/droiddeck-fex/$arch/$it" }
} + listOf("libvulkan-thunk.so", "libwebkitinput.so").map { "x86_64/$it" to "usr/local/lib/droiddeck-fex/x86_64/$it" }
@@ -171,7 +176,7 @@ object SessionFiles {
"usr/local/lib/droiddeck-pipewire.tzst" to "usr/local/lib/droiddeck-pipewire.tzst",
"usr/local/share/licenses/uruntime/LICENSE" to "usr/local/share/licenses/uruntime/LICENSE",
"usr/local/lib/droiddeck-dirt/ws2_32.dll" to "usr/local/lib/droiddeck-dirt/ws2_32.dll",
) + mangoapp + msitools.map { it to it } + vr + fexPreloads).filter { (asset, _) ->
) + mangoapp + msitools.map { it to it } + vr + gpuLoad + fexPreloads).filter { (asset, _) ->
val dir = asset.substringBeforeLast('/')
runCatching { context.assets.list("linuxfs/$dir")?.contains(asset.substringAfterLast('/')) == true }.getOrDefault(false)
}
@@ -808,6 +808,7 @@ class SessionService : Service() {
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
guest.add("MESA_DISK_CACHE_DATABASE=1")
guest.addAll(GpuStatsComponent.guestEnv(LinuxRuntime.rootDir(this)))
guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216")
if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0"))
SessionState.steamClientArch = if (steamHere) SessionPrefs.steamClientArch(this) else null
@@ -994,7 +995,7 @@ class SessionService : Service() {
gpuMem.attach(this)
components.add(gpuMem)
}
val gpuStats = GpuStatsComponent(File(filesDir, "session/sys/kgsl-3d0"))
val gpuStats = GpuStatsComponent(File(filesDir, "session/sys/kgsl-3d0"), LinuxRuntime.rootDir(this))
if (gpuStats.prepare()) {
gpuStats.attach(this)
components.add(gpuStats)
@@ -148,6 +148,10 @@ public final class WaylandCompositor {
public static native void nativeStart(String xdgRuntimeDir);
/** Nanoseconds of the compositor's own GPU work while timing is on, or -1 when it is not timed. */
public static native long nativeGpuBusyNs();
public static native void nativeSetGpuTiming(boolean on);
public static native void nativeStartWithSurface(Surface surface, String xdgRuntimeDir,
String driverPath, String libraryName,
String nativeLibDir);
@@ -0,0 +1,114 @@
package com.droiddeck.launcher.session
import java.io.File
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class GpuLayerLoadTest {
@get:Rule val tmp = TemporaryFolder()
private var now = 0L
private fun load(dir: File) = GpuStatsComponent.LayerLoad(dir, clock = { now })
private fun write(dir: File, pid: Int, busyNs: Long) = File(dir, "$pid").writeText("%20d %-32s\n".format(busyNs, "game"))
@Test fun busyTimeOfAllProcessesOverWallTime() {
val dir = tmp.newFolder("gpu-load")
val load = load(dir)
write(dir, 100, 1_000_000_000L)
write(dir, 200, 5_000_000_000L)
now = 10_000_000_000L
assertNull(load.percent())
write(dir, 100, 1_300_000_000L)
write(dir, 200, 5_100_000_000L)
now += 1_000_000_000L
assertEquals(40L, load.percent())
}
@Test fun aNewProcessStartsFromItsOwnTotalAndAGoneOneStopsCounting() {
val dir = tmp.newFolder("gpu-load")
val load = load(dir)
write(dir, 100, 2_000_000_000L)
now = 1L
load.percent()
File(dir, "100").delete()
write(dir, 300, 9_000_000_000L)
now += 500_000_000L
assertEquals(0L, load.percent())
write(dir, 300, 9_250_000_000L)
now += 500_000_000L
assertEquals(50L, load.percent())
}
@Test fun capsAtFullAndIgnoresOtherFiles() {
val dir = tmp.newFolder("gpu-load")
val load = load(dir)
File(dir, "on").createNewFile()
write(dir, 1, 0L)
write(dir, 2, 0L)
now = 1L
load.percent()
write(dir, 1, 900_000_000L)
write(dir, 2, 900_000_000L)
now += 1_000_000_000L
assertEquals(100L, load.percent())
}
@Test fun theOnFlagFollowsTheHudAndHidingDropsTheBaseline() {
val dir = tmp.newFolder("gpu-load")
val load = load(dir)
load.wanted(true)
assertTrue(File(dir, "on").exists())
write(dir, 1, 100L)
now = 1L
load.percent()
load.wanted(false)
assertFalse(File(dir, "on").exists())
load.wanted(true)
now += 1_000L
assertNull(load.percent())
}
@Test fun theCompositorsOwnGpuTimeAddsToTheGames() {
val dir = tmp.newFolder("gpu-load")
var compositor: Long? = 7_000_000_000L
val load = GpuStatsComponent.LayerLoad(dir, clock = { now }, extra = { compositor })
write(dir, 100, 1_000_000_000L)
now = 1L
assertNull(load.percent())
write(dir, 100, 1_600_000_000L)
compositor = 7_150_000_000L
now += 1_000_000_000L
assertEquals(75L, load.percent())
compositor = null
write(dir, 100, 1_900_000_000L)
now += 1_000_000_000L
assertEquals(30L, load.percent())
}
@Test fun onlyTheCompositorCountsWhenNoGameReportsYet() {
val dir = File(tmp.root, "never-created")
var compositor = 0L
val load = GpuStatsComponent.LayerLoad(dir, clock = { now }, extra = { compositor })
now = 1L
load.percent()
compositor = 200_000_000L
now += 1_000_000_000L
assertEquals(20L, load.percent())
}
@Test fun compositorTimingFollowsTheHudOnlyOnChanges() {
val dir = tmp.newFolder("gpu-load")
val calls = mutableListOf<Boolean>()
val load = GpuStatsComponent.LayerLoad(dir, clock = { now }, onWanted = { calls += it })
load.wanted(true)
load.wanted(true)
load.wanted(false)
load.wanted(true)
load.clear()
assertEquals(listOf(true, false, true, false), calls)
}
}
@@ -0,0 +1,74 @@
package com.droiddeck.launcher.session
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class GpuMemstoreLoadTest {
private var busy = 0L
private var samples = 0L
private var moves = 0L
private var available = true
private val load = GpuStatsComponent.MemstoreLoad { if (available) longArrayOf(busy, samples, moves) else null }
private fun tick(newSamples: Long, newBusy: Long, newMoves: Long = 1) {
samples += newSamples
busy += newBusy
moves += newMoves
}
@Test fun notTrustedUntilNewWorkIsSeen() {
assertNull(load.percent())
tick(500, 400, newMoves = 0)
assertNull(load.percent())
assertFalse(load.working)
tick(500, 300)
assertEquals(70L, load.percent())
assertTrue(load.working)
}
@Test fun givesUpWhenTheTimestampsNeverMove() {
load.percent()
repeat(10) {
tick(500, 250, newMoves = 0)
assertNull(load.percent())
}
assertTrue(load.failed)
tick(500, 250)
assertNull(load.percent())
assertFalse(load.working)
}
@Test fun averagesOverTheLastTwoReads() {
load.percent()
tick(500, 500)
assertEquals(100L, load.percent())
tick(500, 0)
assertEquals(50L, load.percent())
tick(500, 0, newMoves = 0)
assertEquals(0L, load.percent())
}
@Test fun pausingStartsANewWindow() {
load.percent()
tick(500, 500)
load.percent()
load.pause()
tick(0, 0, newMoves = 0)
assertNull(load.percent())
tick(500, 100)
assertEquals(20L, load.percent())
}
@Test fun noReadingWithoutNewSamplesOrCounts() {
load.percent()
tick(500, 100)
load.percent()
assertNull(load.percent())
available = false
tick(500, 100)
assertNull(load.percent())
}
}
+7
View File
@@ -181,6 +181,13 @@ if ! restore_step vr "${vr_key}" "${vr_out}"; then
save_step vr "${vr_key}" "${vr_out}"
fi
gpuload_out="${linuxfs_dir}/usr/local/lib/droiddeck-gpuload"
gpuload_key=$(inputs_hash "${image_id} ${ndk_version} ${VK_INCLUDE:-}" tools/gpuload)
if ! restore_step gpuload "${gpuload_key}" "${gpuload_out}"; then
DROIDDECK_BUILD_IMAGE="${image_name}" "${repo_root}/tools/gpuload/build.sh" "${gpuload_out}"
save_step gpuload "${gpuload_key}" "${gpuload_out}"
fi
github_repo=${DROIDDECK_GITHUB_REPOSITORY:-}
if [[ -z "${github_repo}" ]]; then
origin_url=$(git -C "${repo_root}" remote get-url origin)
+3 -1
View File
@@ -14,6 +14,7 @@
"tools/msitools",
"tools/pipewire",
"tools/vr",
"tools/gpuload",
"app/src/main/cpp/waylandcomp/generated",
"app/src/main/cpp/waylandcomp/src/headpose_shm.h",
"app/src/main/cpp/waylandcomp/src/spatialpose_shm.h",
@@ -21,7 +22,8 @@
],
"outputs": [
"app/src/main/assets/linuxfs/usr/local/bin/gamescope",
"app/src/main/assets/linuxfs/usr/local/lib/droiddeck-vr/droiddeck/bin/linuxarm64/driver_droiddeck.so"
"app/src/main/assets/linuxfs/usr/local/lib/droiddeck-vr/droiddeck/bin/linuxarm64/driver_droiddeck.so",
"app/src/main/assets/linuxfs/usr/local/lib/droiddeck-gpuload/libVkLayer_droiddeck_gpuload.so"
],
"ndk": true,
"packages": [
+2
View File
@@ -4,6 +4,8 @@ D=app/src/main/assets/linuxfs
mkdir -p "$D"
VK_INCLUDE=$(ls -d "${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1) \
tools/vr/build.sh --native "$D/usr/local/lib/droiddeck-vr"
VK_INCLUDE=$(ls -d "${ANDROID_NDK_LATEST_HOME:-${ANDROID_NDK_HOME:-$ANDROID_NDK_ROOT}}"/toolchains/llvm/prebuilt/*/sysroot/usr/include | head -1) \
tools/gpuload/build.sh --native "$D/usr/local/lib/droiddeck-gpuload"
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
install -Dm644 "$script" "$D/usr/local/bin/$(basename "$script")"
done
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Builds VK_LAYER_DROIDDECK_gpu_load for the arm64 guest: tools/gpuload/build.sh [--native] OUT
# OUT gets libVkLayer_droiddeck_gpuload.so and its implicit-layer manifest.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
repo=$(cd "$here/../.." && pwd)
. "$repo/tools/docker_platform.sh"
native=0
if [ "${1:-}" = --native ]; then native=1; shift; fi
out=${1:?usage: tools/gpuload/build.sh [--native] OUT}
case "$out" in /*) ;; *) out="$PWD/$out" ;; esac
vkinc=${VK_INCLUDE:-}
if [ -z "$vkinc" ]; then
ndks=("${NDK:-}" "${ANDROID_NDK_HOME:-}" "${ANDROID_NDK_LATEST_HOME:-}")
sdk_ndks=("${ANDROID_HOME:-$HOME/Library/Android/sdk}"/ndk/*)
for ((i = ${#sdk_ndks[@]} - 1; i >= 0; i--)); do ndks+=("${sdk_ndks[i]}"); done
for ndk in "${ndks[@]}"; do
[ -n "$ndk" ] && [ -d "$ndk" ] || continue
for cand in "$ndk"/toolchains/llvm/prebuilt/*/sysroot/usr/include; do
if [ -f "$cand/vulkan/vk_layer.h" ]; then vkinc=$cand; break 2; fi
done
done
fi
[ -n "$vkinc" ] && [ -f "$vkinc/vulkan/vulkan_core.h" ] || { echo "tools/gpuload/build.sh: no Vulkan headers (set VK_INCLUDE)" >&2; exit 1; }
work="$repo/build/gpuload"
rm -rf "$work" && mkdir -p "$work/include" "$work/out"
cp -R "$vkinc/vulkan" "$work/include/"
if [ -d "$vkinc/vk_video" ]; then cp -R "$vkinc/vk_video" "$work/include/"; fi
cmd=(sh -c '
set -e
cc -std=c11 -O2 -Wall -Wextra -Werror -Itools/gpuload/src -o build/gpuload/test_gpuload_busy tools/gpuload/tests/test_gpuload_busy.c
build/gpuload/test_gpuload_busy
cc -std=c11 -O2 -Wall -Wextra -Werror -Ibuild/gpuload/include -Itools/gpuload/src -o build/gpuload/test_gpuload_ring tools/gpuload/tests/test_gpuload_ring.c
build/gpuload/test_gpuload_ring
aarch64-linux-gnu-gcc -std=c11 -shared -fPIC -O2 -Wall -Wextra -Werror -Wno-unused-parameter -Wno-missing-field-initializers \
-fvisibility=hidden -pthread -Wl,-z,defs -Ibuild/gpuload/include -Itools/gpuload/src \
-o build/gpuload/out/libVkLayer_droiddeck_gpuload.so tools/gpuload/src/vk_layer_droiddeck_gpuload.c
aarch64-linux-gnu-strip --strip-unneeded build/gpuload/out/libVkLayer_droiddeck_gpuload.so')
if [ "$native" = 1 ]; then
(cd "$repo" && "${cmd[@]}")
else
docker run --rm --platform "${DROIDDECK_DOCKER_PLATFORM:-linux/amd64}" --user "$(id -u):$(id -g)" -v "$repo:/src" -w /src \
"${DROIDDECK_BUILD_IMAGE:-droiddeck-local-cross:24.04-v3}" "${cmd[@]}"
fi
cp "$here/layer/VK_LAYER_DROIDDECK_gpu_load.json" "$work/out/"
mkdir -p "$out"
cp "$work/out/libVkLayer_droiddeck_gpuload.so" "$work/out/VK_LAYER_DROIDDECK_gpu_load.json" "$out/"
echo "built $out"
@@ -0,0 +1,20 @@
{
"file_format_version": "1.2.0",
"layer": {
"name": "VK_LAYER_DROIDDECK_gpu_load",
"type": "GLOBAL",
"library_path": "/usr/local/lib/droiddeck-gpuload/libVkLayer_droiddeck_gpuload.so",
"api_version": "1.3.0",
"implementation_version": "1",
"description": "Times this process's GPU work for DroidDeck's HUD where Android hides the GPU load; idle unless a HUD is shown",
"functions": {
"vkNegotiateLoaderLayerInterfaceVersion": "DroidDeckGpuLoad_NegotiateLoaderLayerInterfaceVersion"
},
"enable_environment": {
"DROIDDECK_GPU_LOAD": "1"
},
"disable_environment": {
"DROIDDECK_GPU_LOAD_DISABLE": "1"
}
}
}
+22
View File
@@ -0,0 +1,22 @@
// Busy-time bookkeeping shared by VK_LAYER_DROIDDECK_gpu_load and DroidDeck's compositor, kept free of Vulkan so it
// can be tested on its own.
#ifndef DROIDDECK_GPULOAD_BUSY_H
#define DROIDDECK_GPULOAD_BUSY_H
#include <stdint.h>
// GPU ticks between the timestamp written before a batch and the one written after it. Batches on one queue run one
// after the other, so these add up without overlap. Ticks are never compared across batches: on Adreno the always-on
// counter behind the timestamps restarts when the GPU powers down between batches (seen on the Retroid Pocket Duo).
// Anything backwards or longer than max_ticks is a bogus pair and counts as nothing.
static inline uint64_t gpuload_interval(uint64_t start, uint64_t end, uint64_t max_ticks) {
return end > start && end - start <= max_ticks ? end - start : 0;
}
// A timestamp query slot is reused once its end is available; until the GPU runs the slot's reset again the
// query still shows the previous end, so a result equal to the last one harvested is the old one.
static inline int gpuload_result_is_new(uint64_t start_avail, uint64_t end_avail, uint64_t end, uint64_t last_end) {
return start_avail && end_avail && end != last_end;
}
#endif
+215
View File
@@ -0,0 +1,215 @@
// Timestamp ring shared by VK_LAYER_DROIDDECK_gpu_load (guest) and DroidDeck's compositor (app): each submit batch
// that has command buffers gets one of the ring's command buffers in front (resets a query pair, writes a TOP_OF_PIPE
// timestamp) and one behind (BOTTOM_OF_PIPE timestamp). Finished pairs are read back without waiting.
// The start timestamp sits after the batch's semaphore waits only because Turnip and Qualcomm's driver on kgsl hold a
// whole batch until its waits signal; a driver that starts TOP_OF_PIPE work earlier would count the wait as busy.
// Callers serialize access to one ring (a queue's submits are externally synchronized anyway).
#ifndef DROIDDECK_GPULOAD_RING_H
#define DROIDDECK_GPULOAD_RING_H
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <vulkan/vulkan.h>
#include "gpuload_busy.h"
#define GPULOAD_RING 64
typedef VkResult(VKAPI_PTR *gpuload_set_data_fn)(VkDevice device, void *object);
struct gpuload_vk {
VkDevice device;
PFN_vkCreateCommandPool create_pool;
PFN_vkDestroyCommandPool destroy_pool;
PFN_vkAllocateCommandBuffers alloc_cmds;
PFN_vkBeginCommandBuffer begin;
PFN_vkEndCommandBuffer end;
PFN_vkCmdResetQueryPool cmd_reset;
PFN_vkCmdWriteTimestamp cmd_timestamp;
PFN_vkCreateQueryPool create_queries;
PFN_vkDestroyQueryPool destroy_queries;
PFN_vkGetQueryPoolResults results;
gpuload_set_data_fn set_data; // a layer must give the loader every command buffer it allocates; NULL elsewhere
double period_ns;
uint64_t max_ticks;
};
struct gpuload_slot {
VkCommandBuffer start, end;
uint64_t last_end;
};
struct gpuload_ring {
int state; // 0 not set up, 1 timing, -1 cannot time
VkCommandPool pool;
VkQueryPool queries;
uint64_t mask;
uint32_t next, oldest, inflight;
struct gpuload_slot slot[GPULOAD_RING];
};
#define GPULOAD_LONGEST_BATCH_NS 2000000000.0
static inline void gpuload_vk_period(struct gpuload_vk *v, float timestamp_period) {
v->period_ns = timestamp_period;
v->max_ticks = timestamp_period > 0 ? (uint64_t)(GPULOAD_LONGEST_BATCH_NS / timestamp_period) : 0;
}
static inline int gpuload_vk_complete(const struct gpuload_vk *v) {
return v->device && v->create_pool && v->destroy_pool && v->alloc_cmds && v->begin && v->end && v->cmd_reset &&
v->cmd_timestamp && v->create_queries && v->destroy_queries && v->results && v->period_ns > 0;
}
static inline void gpuload_ring_release(struct gpuload_ring *r, const struct gpuload_vk *v) {
if (r->queries) v->destroy_queries(v->device, r->queries, NULL);
if (r->pool) v->destroy_pool(v->device, r->pool, NULL);
r->queries = VK_NULL_HANDLE;
r->pool = VK_NULL_HANDLE;
r->next = r->oldest = r->inflight = 0;
}
static inline int gpuload_ring_record(struct gpuload_ring *r, const struct gpuload_vk *v, uint32_t i) {
const VkCommandBufferBeginInfo begin = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
struct gpuload_slot *s = &r->slot[i];
if (v->begin(s->start, &begin) != VK_SUCCESS) return 0;
v->cmd_reset(s->start, r->queries, 2 * i, 2);
v->cmd_timestamp(s->start, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, r->queries, 2 * i);
if (v->end(s->start) != VK_SUCCESS) return 0;
if (v->begin(s->end, &begin) != VK_SUCCESS) return 0;
v->cmd_timestamp(s->end, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, r->queries, 2 * i + 1);
return v->end(s->end) == VK_SUCCESS;
}
// Sets the ring up for a queue of this family; returns 1 when it can time it (r->state says the same afterwards).
static inline int gpuload_ring_setup(struct gpuload_ring *r, const struct gpuload_vk *v, uint32_t family,
uint32_t timestamp_bits) {
r->state = -1;
if (!timestamp_bits || !gpuload_vk_complete(v)) return 0;
r->mask = timestamp_bits >= 64 ? ~0ull : (1ull << timestamp_bits) - 1;
const VkCommandPoolCreateInfo pool = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, .queueFamilyIndex = family};
const VkQueryPoolCreateInfo queries = {.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
.queryType = VK_QUERY_TYPE_TIMESTAMP, .queryCount = 2 * GPULOAD_RING};
VkCommandBuffer cmds[2 * GPULOAD_RING];
if (v->create_pool(v->device, &pool, NULL, &r->pool) != VK_SUCCESS) { r->pool = VK_NULL_HANDLE; goto fail; }
if (v->create_queries(v->device, &queries, NULL, &r->queries) != VK_SUCCESS) { r->queries = VK_NULL_HANDLE; goto fail; }
const VkCommandBufferAllocateInfo alloc = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = r->pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = 2 * GPULOAD_RING};
if (v->alloc_cmds(v->device, &alloc, cmds) != VK_SUCCESS) goto fail;
for (uint32_t i = 0; i < 2 * GPULOAD_RING; i++)
if (v->set_data && v->set_data(v->device, cmds[i]) != VK_SUCCESS) goto fail;
for (uint32_t i = 0; i < GPULOAD_RING; i++) {
r->slot[i].start = cmds[2 * i];
r->slot[i].end = cmds[2 * i + 1];
r->slot[i].last_end = 0;
if (!gpuload_ring_record(r, v, i)) goto fail;
}
r->next = r->oldest = r->inflight = 0;
r->state = 1;
return 1;
fail:
gpuload_ring_release(r, v);
r->state = -1;
return 0;
}
// Nanoseconds of GPU time in the batches that finished since the last call. Slots are taken in ring order and a
// queue runs its batches in order, so the oldest slot finishes first.
static inline uint64_t gpuload_ring_harvest(struct gpuload_ring *r, const struct gpuload_vk *v) {
uint64_t ticks = 0;
while (r->inflight) {
struct gpuload_slot *s = &r->slot[r->oldest];
uint64_t q[4] = {0, 0, 0, 0};
VkResult res = v->results(v->device, r->queries, 2 * r->oldest, 2, sizeof(q), q, 2 * sizeof(uint64_t),
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
if (res != VK_SUCCESS && res != VK_NOT_READY) break;
uint64_t start = q[0] & r->mask, end = q[2] & r->mask;
if (!gpuload_result_is_new(q[1], q[3], end, s->last_end)) break;
s->last_end = end;
r->oldest = (r->oldest + 1) % GPULOAD_RING;
r->inflight--;
ticks += gpuload_interval(start, end, v->max_ticks);
}
return (uint64_t)((double)ticks * v->period_ns);
}
static inline int gpuload_plain_batch(const void *next) {
for (const VkBaseInStructure *s = (const VkBaseInStructure *)next; s; s = s->pNext) {
if (s->sType == VK_STRUCTURE_TYPE_DEVICE_GROUP_SUBMIT_INFO) return 0;
if (s->sType == VK_STRUCTURE_TYPE_PROTECTED_SUBMIT_INFO && ((const VkProtectedSubmitInfo *)s)->protectedSubmit)
return 0;
}
return 1;
}
static inline struct gpuload_slot *gpuload_ring_take(struct gpuload_ring *r) {
if (r->inflight >= GPULOAD_RING) return NULL;
struct gpuload_slot *s = &r->slot[r->next];
r->next = (r->next + 1) % GPULOAD_RING;
r->inflight++;
return s;
}
// Gives back the slots of a submit that failed.
static inline void gpuload_ring_unwind(struct gpuload_ring *r, uint32_t used) {
r->next = (r->next + GPULOAD_RING - used) % GPULOAD_RING;
r->inflight -= used;
}
// A copy of the batches with the ring's command buffers around each one that has command buffers (free() it after
// the submit), or NULL to submit the originals. *used counts the slots taken, for gpuload_ring_unwind.
static inline VkSubmitInfo *gpuload_ring_wrap(struct gpuload_ring *r, uint32_t count, const VkSubmitInfo *infos,
uint32_t *used) {
*used = 0;
size_t cmds = 0;
for (uint32_t i = 0; i < count; i++) cmds += infos[i].commandBufferCount + 2;
VkSubmitInfo *mine = (VkSubmitInfo *)malloc(count * sizeof(*mine) + cmds * sizeof(VkCommandBuffer));
if (!mine) return NULL;
VkCommandBuffer *list = (VkCommandBuffer *)(mine + count);
for (uint32_t i = 0; i < count; i++) {
uint32_t n = infos[i].commandBufferCount;
mine[i] = infos[i];
if (!n || !gpuload_plain_batch(infos[i].pNext)) continue;
struct gpuload_slot *s = gpuload_ring_take(r);
if (!s) continue;
list[0] = s->start;
memcpy(list + 1, infos[i].pCommandBuffers, n * sizeof(VkCommandBuffer));
list[n + 1] = s->end;
mine[i].pCommandBuffers = list;
mine[i].commandBufferCount = n + 2;
list += n + 2;
(*used)++;
}
return mine;
}
static inline VkSubmitInfo2 *gpuload_ring_wrap2(struct gpuload_ring *r, uint32_t count, const VkSubmitInfo2 *infos,
uint32_t *used) {
*used = 0;
size_t cmds = 0;
for (uint32_t i = 0; i < count; i++) cmds += infos[i].commandBufferInfoCount + 2;
VkSubmitInfo2 *mine = (VkSubmitInfo2 *)malloc(count * sizeof(*mine) + cmds * sizeof(VkCommandBufferSubmitInfo));
if (!mine) return NULL;
VkCommandBufferSubmitInfo *list = (VkCommandBufferSubmitInfo *)(mine + count);
for (uint32_t i = 0; i < count; i++) {
uint32_t n = infos[i].commandBufferInfoCount;
mine[i] = infos[i];
if (!n || (infos[i].flags & VK_SUBMIT_PROTECTED_BIT) || !gpuload_plain_batch(infos[i].pNext)) continue;
struct gpuload_slot *s = gpuload_ring_take(r);
if (!s) continue;
memset(&list[0], 0, sizeof(list[0]));
list[0].sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
list[0].commandBuffer = s->start;
memcpy(list + 1, infos[i].pCommandBufferInfos, n * sizeof(VkCommandBufferSubmitInfo));
memset(&list[n + 1], 0, sizeof(list[n + 1]));
list[n + 1].sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO;
list[n + 1].commandBuffer = s->end;
mine[i].pCommandBufferInfos = list;
mine[i].commandBufferInfoCount = n + 2;
list += n + 2;
(*used)++;
}
return mine;
}
#endif
@@ -0,0 +1,410 @@
// VK_LAYER_DROIDDECK_gpu_load: this process's GPU busy time for the HUD where Android keeps the GPU load from apps
// (kgsl sysfs SELinux-blocked, perfcounter reads refused while /sys/class/kgsl/kgsl-3d0/perfcounter is 0).
// The process total in nanoseconds goes to $DROIDDECK_GPU_LOAD_DIR/<pid>; nothing is timed while
// $DROIDDECK_GPU_LOAD_DIR/on is missing (the app removes it while no HUD is shown).
#define _GNU_SOURCE
#include <vulkan/vulkan.h>
#include <vulkan/vk_layer.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "gpuload_ring.h"
#define EXPORT __attribute__((visibility("default")))
#define MAX_FAMILIES 32
#define WANTED_CHECK_NS 500000000ull
#define WRITE_EVERY_NS 100000000ull
static const char *g_dir;
static void say(const char *what) {
// FEX leaves program_invocation_short_name empty for x86 programs.
fprintf(stderr, "[droiddeck-gpu-load] %s (pid %d): %s\n", program_invocation_short_name, (int)getpid(), what);
}
__attribute__((constructor)) static void init(void) {
const char *dir = getenv("DROIDDECK_GPU_LOAD_DIR");
g_dir = dir && *dir ? dir : NULL;
}
static uint64_t now_ns(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}
struct inst {
struct inst *next_in_list;
VkInstance handle;
PFN_vkGetInstanceProcAddr gipa;
PFN_vkDestroyInstance destroy;
PFN_vkGetPhysicalDeviceProperties props;
PFN_vkGetPhysicalDeviceQueueFamilyProperties families;
};
struct dev {
struct dev *next_in_list;
VkDevice handle;
PFN_vkGetDeviceProcAddr gdpa;
PFN_vkDestroyDevice destroy;
PFN_vkGetDeviceQueue get_queue;
PFN_vkGetDeviceQueue2 get_queue2;
PFN_vkQueueSubmit submit;
PFN_vkQueueSubmit2 submit2;
PFN_vkQueueSubmit2 submit2_khr;
struct gpuload_vk vk;
uint32_t family_count;
uint32_t timestamp_bits[MAX_FAMILIES];
};
struct queue {
struct queue *next_in_list;
VkQueue handle;
struct dev *dev;
uint32_t family;
uint32_t epoch;
struct gpuload_ring ring;
};
static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER;
static struct inst *g_insts;
static struct dev *g_devs;
static struct queue *g_queues;
static void *key(const void *obj) { return *(void *const *)obj; }
static struct inst *inst_for(const void *obj) {
struct inst *r;
pthread_mutex_lock(&g_lock);
for (r = g_insts; r && key(r->handle) != key(obj); r = r->next_in_list) {}
pthread_mutex_unlock(&g_lock);
return r;
}
static struct dev *dev_for(const void *obj) {
struct dev *r;
pthread_mutex_lock(&g_lock);
for (r = g_devs; r && key(r->handle) != key(obj); r = r->next_in_list) {}
pthread_mutex_unlock(&g_lock);
return r;
}
static struct queue *queue_for(VkQueue handle) {
struct queue *r;
pthread_mutex_lock(&g_lock);
for (r = g_queues; r && r->handle != handle; r = r->next_in_list) {}
pthread_mutex_unlock(&g_lock);
return r;
}
static void note_queue(struct dev *d, VkQueue handle, uint32_t family) {
if (!handle || queue_for(handle)) return;
struct queue *q = calloc(1, sizeof(*q));
if (!q) return;
q->handle = handle;
q->dev = d;
q->family = family;
pthread_mutex_lock(&g_lock);
q->next_in_list = g_queues;
g_queues = q;
pthread_mutex_unlock(&g_lock);
}
// ---- process total ---------------------------------------------------------------------------------------------
static _Atomic uint64_t g_busy_ns;
static _Atomic uint64_t g_wanted_at;
static _Atomic int g_wanted;
static _Atomic uint32_t g_epoch = 1; // bumped each time the HUD comes back
static pthread_mutex_t g_write_lock = PTHREAD_MUTEX_INITIALIZER;
static uint64_t g_written_at;
static int g_fd = -1;
static pid_t g_fd_pid;
static int g_announced;
static int wanted(uint64_t now) {
uint64_t at = atomic_load(&g_wanted_at);
if (at && now - at < WANTED_CHECK_NS) return atomic_load(&g_wanted);
atomic_store(&g_wanted_at, now);
char path[512];
snprintf(path, sizeof(path), "%s/on", g_dir);
int on = access(path, F_OK) == 0;
if (on && !atomic_exchange(&g_wanted, on)) atomic_fetch_add(&g_epoch, 1);
if (!on) atomic_store(&g_wanted, 0);
return on;
}
static void publish(uint64_t now) {
if (pthread_mutex_trylock(&g_write_lock)) return;
if (now - g_written_at >= WRITE_EVERY_NS) {
g_written_at = now;
pid_t pid = getpid();
struct stat st;
// The app deletes files that stopped changing; a forked child needs its own.
if (g_fd >= 0 && (g_fd_pid != pid || fstat(g_fd, &st) || st.st_nlink == 0)) { close(g_fd); g_fd = -1; }
if (g_fd < 0) {
char path[512];
snprintf(path, sizeof(path), "%s/%d", g_dir, (int)pid);
g_fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
g_fd_pid = pid;
}
if (g_fd >= 0) {
char line[80];
int n = snprintf(line, sizeof(line), "%20llu %-32.32s\n", (unsigned long long)atomic_load(&g_busy_ns),
program_invocation_short_name);
if (pwrite(g_fd, line, (size_t)n, 0) != n) { close(g_fd); g_fd = -1; }
}
}
pthread_mutex_unlock(&g_write_lock);
}
// The queue to time this submit on, or NULL to pass it through.
static struct queue *measuring(VkQueue handle) {
if (!g_dir) return NULL;
struct queue *q = queue_for(handle);
if (!q) return NULL;
uint64_t now = now_ns();
if (!wanted(now)) return NULL;
struct dev *d = q->dev;
if (q->ring.state == 0) {
uint32_t bits = q->family < d->family_count ? d->timestamp_bits[q->family] : 0;
if (gpuload_ring_setup(&q->ring, &d->vk, q->family, bits)) {
if (!__atomic_exchange_n(&g_announced, 1, __ATOMIC_RELAXED)) say("timing this process's GPU work for the HUD");
} else {
say(bits ? "cannot set up GPU timing on this queue" : "this queue family has no timestamps");
}
q->epoch = atomic_load(&g_epoch);
}
if (q->ring.state != 1) return NULL;
uint64_t ns = gpuload_ring_harvest(&q->ring, &d->vk);
uint32_t epoch = atomic_load(&g_epoch);
if (q->epoch == epoch) atomic_fetch_add(&g_busy_ns, ns);
else q->epoch = epoch; // timed before the HUD went away
publish(now);
return q;
}
static VkResult VKAPI_CALL QueueSubmit(VkQueue queue, uint32_t count, const VkSubmitInfo *infos, VkFence fence) {
struct dev *d = dev_for(queue);
if (!d) return VK_ERROR_DEVICE_LOST;
struct queue *q = count ? measuring(queue) : NULL;
uint32_t used = 0;
VkSubmitInfo *mine = q ? gpuload_ring_wrap(&q->ring, count, infos, &used) : NULL;
VkResult r = d->submit(queue, count, mine ? mine : infos, fence);
if (mine && r != VK_SUCCESS) gpuload_ring_unwind(&q->ring, used);
free(mine);
return r;
}
static VkResult submit2(PFN_vkQueueSubmit2 next, VkQueue queue, uint32_t count, const VkSubmitInfo2 *infos, VkFence fence) {
struct queue *q = count ? measuring(queue) : NULL;
uint32_t used = 0;
VkSubmitInfo2 *mine = q ? gpuload_ring_wrap2(&q->ring, count, infos, &used) : NULL;
VkResult r = next(queue, count, mine ? mine : infos, fence);
if (mine && r != VK_SUCCESS) gpuload_ring_unwind(&q->ring, used);
free(mine);
return r;
}
static VkResult VKAPI_CALL QueueSubmit2(VkQueue queue, uint32_t count, const VkSubmitInfo2 *infos, VkFence fence) {
struct dev *d = dev_for(queue);
return d ? submit2(d->submit2, queue, count, infos, fence) : VK_ERROR_DEVICE_LOST;
}
static VkResult VKAPI_CALL QueueSubmit2KHR(VkQueue queue, uint32_t count, const VkSubmitInfo2 *infos, VkFence fence) {
struct dev *d = dev_for(queue);
return d ? submit2(d->submit2_khr, queue, count, infos, fence) : VK_ERROR_DEVICE_LOST;
}
static void VKAPI_CALL GetDeviceQueue(VkDevice device, uint32_t family, uint32_t index, VkQueue *out) {
struct dev *d = dev_for(device);
if (!d) return;
d->get_queue(device, family, index, out);
if (out && *out) note_queue(d, *out, family);
}
static void VKAPI_CALL GetDeviceQueue2(VkDevice device, const VkDeviceQueueInfo2 *info, VkQueue *out) {
struct dev *d = dev_for(device);
if (!d) return;
d->get_queue2(device, info, out);
if (out && *out) note_queue(d, *out, info->queueFamilyIndex);
}
// ---- instance and device ---------------------------------------------------------------------------------------
static VkResult VKAPI_CALL CreateInstance(const VkInstanceCreateInfo *ci, const VkAllocationCallbacks *alloc, VkInstance *out) {
VkLayerInstanceCreateInfo *link = (VkLayerInstanceCreateInfo *)ci->pNext;
while (link && !(link->sType == VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO && link->function == VK_LAYER_LINK_INFO))
link = (VkLayerInstanceCreateInfo *)link->pNext;
if (!link) return VK_ERROR_INITIALIZATION_FAILED;
PFN_vkGetInstanceProcAddr next_gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
PFN_vkCreateInstance create = (PFN_vkCreateInstance)next_gipa(VK_NULL_HANDLE, "vkCreateInstance");
if (!create) return VK_ERROR_INITIALIZATION_FAILED;
struct inst *in = calloc(1, sizeof(*in));
if (!in) return VK_ERROR_OUT_OF_HOST_MEMORY;
VkResult r = create(ci, alloc, out);
if (r != VK_SUCCESS) { free(in); return r; }
in->handle = *out;
in->gipa = next_gipa;
in->destroy = (PFN_vkDestroyInstance)next_gipa(*out, "vkDestroyInstance");
in->props = (PFN_vkGetPhysicalDeviceProperties)next_gipa(*out, "vkGetPhysicalDeviceProperties");
in->families = (PFN_vkGetPhysicalDeviceQueueFamilyProperties)next_gipa(*out, "vkGetPhysicalDeviceQueueFamilyProperties");
pthread_mutex_lock(&g_lock);
in->next_in_list = g_insts;
g_insts = in;
pthread_mutex_unlock(&g_lock);
return VK_SUCCESS;
}
static void VKAPI_CALL DestroyInstance(VkInstance instance, const VkAllocationCallbacks *alloc) {
struct inst *in = inst_for(instance);
if (!in) return;
PFN_vkDestroyInstance destroy = in->destroy;
pthread_mutex_lock(&g_lock);
for (struct inst **p = &g_insts; *p; p = &(*p)->next_in_list)
if (*p == in) { *p = in->next_in_list; break; }
pthread_mutex_unlock(&g_lock);
free(in);
if (destroy) destroy(instance, alloc);
}
static VkLayerDeviceCreateInfo *device_chain(const VkDeviceCreateInfo *ci, VkLayerFunction function) {
VkLayerDeviceCreateInfo *c = (VkLayerDeviceCreateInfo *)ci->pNext;
while (c && !(c->sType == VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO && c->function == function))
c = (VkLayerDeviceCreateInfo *)c->pNext;
return c;
}
static VkResult VKAPI_CALL CreateDevice(VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, const VkAllocationCallbacks *alloc,
VkDevice *out) {
VkLayerDeviceCreateInfo *link = device_chain(ci, VK_LAYER_LINK_INFO);
if (!link) return VK_ERROR_INITIALIZATION_FAILED;
VkLayerDeviceCreateInfo *callback = device_chain(ci, VK_LOADER_DATA_CALLBACK);
PFN_vkGetInstanceProcAddr next_gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr;
PFN_vkGetDeviceProcAddr next_gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr;
link->u.pLayerInfo = link->u.pLayerInfo->pNext;
struct inst *in = inst_for(pd);
PFN_vkCreateDevice create = (PFN_vkCreateDevice)next_gipa(in ? in->handle : VK_NULL_HANDLE, "vkCreateDevice");
if (!create) return VK_ERROR_INITIALIZATION_FAILED;
struct dev *d = calloc(1, sizeof(*d));
if (!d) return VK_ERROR_OUT_OF_HOST_MEMORY;
VkResult r = create(pd, ci, alloc, out);
if (r != VK_SUCCESS) { free(d); return r; }
d->handle = *out;
d->gdpa = next_gdpa;
#define LOAD(field, type, name) d->field = (type)next_gdpa(*out, name)
LOAD(destroy, PFN_vkDestroyDevice, "vkDestroyDevice");
LOAD(get_queue, PFN_vkGetDeviceQueue, "vkGetDeviceQueue");
LOAD(get_queue2, PFN_vkGetDeviceQueue2, "vkGetDeviceQueue2");
LOAD(submit, PFN_vkQueueSubmit, "vkQueueSubmit");
LOAD(submit2, PFN_vkQueueSubmit2, "vkQueueSubmit2");
LOAD(submit2_khr, PFN_vkQueueSubmit2, "vkQueueSubmit2KHR");
LOAD(vk.create_pool, PFN_vkCreateCommandPool, "vkCreateCommandPool");
LOAD(vk.destroy_pool, PFN_vkDestroyCommandPool, "vkDestroyCommandPool");
LOAD(vk.alloc_cmds, PFN_vkAllocateCommandBuffers, "vkAllocateCommandBuffers");
LOAD(vk.begin, PFN_vkBeginCommandBuffer, "vkBeginCommandBuffer");
LOAD(vk.end, PFN_vkEndCommandBuffer, "vkEndCommandBuffer");
LOAD(vk.cmd_reset, PFN_vkCmdResetQueryPool, "vkCmdResetQueryPool");
LOAD(vk.cmd_timestamp, PFN_vkCmdWriteTimestamp, "vkCmdWriteTimestamp");
LOAD(vk.create_queries, PFN_vkCreateQueryPool, "vkCreateQueryPool");
LOAD(vk.destroy_queries, PFN_vkDestroyQueryPool, "vkDestroyQueryPool");
LOAD(vk.results, PFN_vkGetQueryPoolResults, "vkGetQueryPoolResults");
#undef LOAD
d->vk.device = *out;
d->vk.set_data = callback ? (gpuload_set_data_fn)callback->u.pfnSetDeviceLoaderData : NULL;
if (in && in->props && in->families) {
VkPhysicalDeviceProperties props;
in->props(pd, &props);
gpuload_vk_period(&d->vk, props.limits.timestampPeriod);
uint32_t n = 0;
in->families(pd, &n, NULL);
VkQueueFamilyProperties families[MAX_FAMILIES];
if (n > MAX_FAMILIES) n = MAX_FAMILIES;
in->families(pd, &n, families);
d->family_count = n;
for (uint32_t i = 0; i < n; i++) d->timestamp_bits[i] = families[i].timestampValidBits;
}
// Without the loader callback our command buffers would lack the loader's dispatch pointer.
if (!d->vk.set_data) d->family_count = 0;
pthread_mutex_lock(&g_lock);
d->next_in_list = g_devs;
g_devs = d;
pthread_mutex_unlock(&g_lock);
return VK_SUCCESS;
}
static void VKAPI_CALL DestroyDevice(VkDevice device, const VkAllocationCallbacks *alloc) {
struct dev *d = dev_for(device);
if (!d) return;
struct queue *mine = NULL;
pthread_mutex_lock(&g_lock);
for (struct queue **p = &g_queues; *p;) {
struct queue *q = *p;
if (q->dev == d) { *p = q->next_in_list; q->next_in_list = mine; mine = q; } else p = &q->next_in_list;
}
for (struct dev **p = &g_devs; *p; p = &(*p)->next_in_list)
if (*p == d) { *p = d->next_in_list; break; }
pthread_mutex_unlock(&g_lock);
while (mine) {
struct queue *q = mine;
mine = q->next_in_list;
gpuload_ring_release(&q->ring, &d->vk);
free(q);
}
PFN_vkDestroyDevice destroy = d->destroy;
free(d);
if (destroy) destroy(device, alloc);
}
// ---- dispatch --------------------------------------------------------------------------------------------------
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DroidDeckGpuLoad_GetDeviceProcAddr(VkDevice device, const char *name);
static PFN_vkVoidFunction device_hook(struct dev *d, const char *name) {
if (!g_dir) return NULL;
if (!strcmp(name, "vkGetDeviceQueue") && d->get_queue) return (PFN_vkVoidFunction)GetDeviceQueue;
if (!strcmp(name, "vkGetDeviceQueue2") && d->get_queue2) return (PFN_vkVoidFunction)GetDeviceQueue2;
if (!strcmp(name, "vkQueueSubmit") && d->submit) return (PFN_vkVoidFunction)QueueSubmit;
if (!strcmp(name, "vkQueueSubmit2") && d->submit2) return (PFN_vkVoidFunction)QueueSubmit2;
if (!strcmp(name, "vkQueueSubmit2KHR") && d->submit2_khr) return (PFN_vkVoidFunction)QueueSubmit2KHR;
return NULL;
}
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DroidDeckGpuLoad_GetInstanceProcAddr(VkInstance instance, const char *name) {
if (!strcmp(name, "vkCreateInstance")) return (PFN_vkVoidFunction)CreateInstance;
if (!strcmp(name, "vkDestroyInstance")) return (PFN_vkVoidFunction)DestroyInstance;
if (!strcmp(name, "vkCreateDevice")) return (PFN_vkVoidFunction)CreateDevice;
if (!strcmp(name, "vkGetInstanceProcAddr")) return (PFN_vkVoidFunction)DroidDeckGpuLoad_GetInstanceProcAddr;
if (!strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)DroidDeckGpuLoad_GetDeviceProcAddr;
if (!instance) return NULL;
struct inst *in = inst_for(instance);
return in ? in->gipa(instance, name) : NULL;
}
EXPORT VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DroidDeckGpuLoad_GetDeviceProcAddr(VkDevice device, const char *name) {
if (!strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)DroidDeckGpuLoad_GetDeviceProcAddr;
if (!strcmp(name, "vkDestroyDevice")) return (PFN_vkVoidFunction)DestroyDevice;
struct dev *d = dev_for(device);
if (!d) return NULL;
PFN_vkVoidFunction mine = device_hook(d, name);
return mine ? mine : d->gdpa(device, name);
}
EXPORT VKAPI_ATTR VkResult VKAPI_CALL DroidDeckGpuLoad_NegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface *v) {
if (v->loaderLayerInterfaceVersion > 2) v->loaderLayerInterfaceVersion = 2;
v->pfnGetInstanceProcAddr = DroidDeckGpuLoad_GetInstanceProcAddr;
v->pfnGetDeviceProcAddr = DroidDeckGpuLoad_GetDeviceProcAddr;
v->pfnGetPhysicalDeviceProcAddr = NULL;
return VK_SUCCESS;
}
+24
View File
@@ -0,0 +1,24 @@
#include <assert.h>
#include <stdio.h>
#include "gpuload_busy.h"
int main(void) {
const uint64_t max = 1000;
assert(gpuload_interval(10, 20, max) == 10);
// Each batch counts on its own, even when the counter restarted in between (later batch has smaller ticks).
assert(gpuload_interval(900000, 900400, max) == 400);
assert(gpuload_interval(50, 350, max) == 300);
// Backwards, empty or too long: nothing.
assert(gpuload_interval(60, 50, max) == 0);
assert(gpuload_interval(70, 70, max) == 0);
assert(gpuload_interval(100, 100 + max + 1, max) == 0);
assert(gpuload_interval(100, 100 + max, max) == max);
// Query results: both halves available and an end that differs from the last harvested one.
assert(gpuload_result_is_new(1, 1, 500, 400));
assert(!gpuload_result_is_new(1, 1, 400, 400));
assert(!gpuload_result_is_new(1, 0, 500, 400));
assert(!gpuload_result_is_new(0, 1, 500, 400));
puts("test_gpuload_busy: ok");
return 0;
}
+96
View File
@@ -0,0 +1,96 @@
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include "gpuload_ring.h"
// A fake device: query results come from this table, [start, start available, end, end available] per slot.
static uint64_t g_result[GPULOAD_RING][4];
static uintptr_t g_next_handle = 0x1000;
static VkResult VKAPI_CALL create_pool(VkDevice d, const VkCommandPoolCreateInfo *ci, const VkAllocationCallbacks *a, VkCommandPool *out) {
(void)d; (void)ci; (void)a; *out = (VkCommandPool)(g_next_handle++); return VK_SUCCESS;
}
static void VKAPI_CALL destroy_pool(VkDevice d, VkCommandPool p, const VkAllocationCallbacks *a) { (void)d; (void)p; (void)a; }
static VkResult VKAPI_CALL alloc_cmds(VkDevice d, const VkCommandBufferAllocateInfo *ai, VkCommandBuffer *out) {
(void)d;
for (uint32_t i = 0; i < ai->commandBufferCount; i++) out[i] = (VkCommandBuffer)(g_next_handle++);
return VK_SUCCESS;
}
static VkResult VKAPI_CALL begin(VkCommandBuffer c, const VkCommandBufferBeginInfo *bi) { (void)c; (void)bi; return VK_SUCCESS; }
static VkResult VKAPI_CALL end(VkCommandBuffer c) { (void)c; return VK_SUCCESS; }
static void VKAPI_CALL cmd_reset(VkCommandBuffer c, VkQueryPool q, uint32_t first, uint32_t n) { (void)c; (void)q; (void)first; (void)n; }
static void VKAPI_CALL cmd_timestamp(VkCommandBuffer c, VkPipelineStageFlagBits s, VkQueryPool q, uint32_t i) { (void)c; (void)s; (void)q; (void)i; }
static VkResult VKAPI_CALL create_queries(VkDevice d, const VkQueryPoolCreateInfo *ci, const VkAllocationCallbacks *a, VkQueryPool *out) {
(void)d; (void)a; assert(ci->queryCount == 2 * GPULOAD_RING); *out = (VkQueryPool)(g_next_handle++); return VK_SUCCESS;
}
static void VKAPI_CALL destroy_queries(VkDevice d, VkQueryPool q, const VkAllocationCallbacks *a) { (void)d; (void)q; (void)a; }
static VkResult VKAPI_CALL results(VkDevice d, VkQueryPool q, uint32_t first, uint32_t n, size_t size, void *data,
VkDeviceSize stride, VkQueryResultFlags flags) {
(void)d; (void)q; (void)size;
assert(n == 2 && stride == 2 * sizeof(uint64_t) && (flags & VK_QUERY_RESULT_WITH_AVAILABILITY_BIT));
uint64_t *out = data;
for (int k = 0; k < 4; k++) out[k] = g_result[first / 2][k];
return out[1] && out[3] ? VK_SUCCESS : VK_NOT_READY;
}
static void finish(uint32_t slot, uint64_t start, uint64_t end) {
g_result[slot][0] = start; g_result[slot][1] = 1; g_result[slot][2] = end; g_result[slot][3] = 1;
}
int main(void) {
struct gpuload_vk v = {
.device = (VkDevice)(uintptr_t)1, .create_pool = create_pool, .destroy_pool = destroy_pool, .alloc_cmds = alloc_cmds,
.begin = begin, .end = end, .cmd_reset = cmd_reset, .cmd_timestamp = cmd_timestamp,
.create_queries = create_queries, .destroy_queries = destroy_queries, .results = results,
};
struct gpuload_ring r = {0};
assert(!gpuload_ring_setup(&r, &v, 0, 64) && r.state == -1); // no timestamp period yet
gpuload_vk_period(&v, 52.083f);
assert(!gpuload_ring_setup(&r, &v, 0, 0) && r.state == -1); // family without timestamps
assert(gpuload_ring_setup(&r, &v, 0, 64) && r.state == 1);
VkCommandBuffer app[2] = {(VkCommandBuffer)(uintptr_t)0xa1, (VkCommandBuffer)(uintptr_t)0xa2};
VkProtectedSubmitInfo prot = {.sType = VK_STRUCTURE_TYPE_PROTECTED_SUBMIT_INFO, .protectedSubmit = VK_TRUE};
VkSubmitInfo batches[3] = {
{.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .commandBufferCount = 2, .pCommandBuffers = app},
{.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO}, // semaphores only
{.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO, .pNext = &prot, .commandBufferCount = 1, .pCommandBuffers = app},
};
uint32_t used = 0;
VkSubmitInfo *mine = gpuload_ring_wrap(&r, 3, batches, &used);
assert(mine && used == 1 && r.inflight == 1);
assert(mine[0].commandBufferCount == 4 && mine[0].pCommandBuffers[0] == r.slot[0].start &&
mine[0].pCommandBuffers[1] == app[0] && mine[0].pCommandBuffers[2] == app[1] &&
mine[0].pCommandBuffers[3] == r.slot[0].end);
assert(mine[1].commandBufferCount == 0 && mine[2].commandBufferCount == 1 && mine[2].pCommandBuffers == app);
free(mine);
assert(gpuload_ring_harvest(&r, &v) == 0 && r.inflight == 1); // not finished yet
finish(0, 1000, 1192); // 192 ticks = 10 us
assert(gpuload_ring_harvest(&r, &v) == 9999 && r.inflight == 0);
// A failed submit gives its slot back.
mine = gpuload_ring_wrap(&r, 1, batches, &used);
assert(used == 1 && r.inflight == 1);
gpuload_ring_unwind(&r, used);
assert(r.inflight == 0 && r.next == 1);
free(mine);
// Fill the ring: the 65th batch goes out untimed.
for (int i = 0; i < GPULOAD_RING; i++) { mine = gpuload_ring_wrap(&r, 1, batches, &used); assert(used == 1); free(mine); }
mine = gpuload_ring_wrap(&r, 1, batches, &used);
assert(used == 0 && mine[0].commandBufferCount == 2);
free(mine);
// Slot 0 comes round again: until the GPU resets it, it still shows the old pair, which must not count twice.
// Slots 1..63 finish after a counter restart (smaller ticks than slot 0's old pair); each still counts.
for (uint32_t s = 1; s < GPULOAD_RING; s++) finish(s, 10 * s, 10 * s + 4);
assert(gpuload_ring_harvest(&r, &v) == (uint64_t)(63 * 4 * 52.083f) && r.inflight == 1 && r.oldest == 0);
finish(0, 5, 9);
assert(gpuload_ring_harvest(&r, &v) == (uint64_t)(4 * 52.083f) && r.inflight == 0);
gpuload_ring_release(&r, &v);
puts("test_gpuload_ring: ok");
return 0;
}
+3
View File
@@ -26,6 +26,7 @@ SOURCE_PATHS = ['app/build.gradle', 'app/src/main/cpp/fakeinput_steam.cpp', 'app
'tools/mangoapp', 'tools/msitools', 'tools/pipewire', 'tools/comet', 'tools/xbox', 'tools/dirt-ws2', 'tools/components', 'tools/ci', 'tools/build_local.sh',
'tools/release/runtime_inputs.py', 'app/src/main/assets/desktop-removal',
'tools/quest', 'tools/vr/src', 'tools/vr/driver_droiddeck', 'tools/vr/layer', 'tools/vr/build.sh',
'tools/gpuload',
'app/src/main/cpp/waylandcomp/src/headpose_shm.h',
'app/src/main/cpp/venc/venc_proto.h',
'app/src/main/cpp/waylandcomp/src/spatialpose_shm.h',
@@ -92,6 +93,8 @@ def required():
'droiddeck/resources/settings/default.vrsettings', 'layer/libVkLayer_droiddeck_vr.so',
'layer/VK_LAYER_DROIDDECK_vr.json', 'layer/VK_LAYER_DROIDDECK_vr_implicit.json',
'layer/libdroiddeck_vr_scheduling.so']]
paths += ['assets/linuxfs/usr/local/lib/droiddeck-gpuload/' + name for name in
['libVkLayer_droiddeck_gpuload.so', 'VK_LAYER_DROIDDECK_gpu_load.json']]
for side in ['left', 'right']:
paths += [f'assets/linuxfs/usr/local/lib/droiddeck-vr/droiddeck/resources/rendermodels/droiddeck_hand_{side}/{name}'
for name in [f'droiddeck_hand_{side}.json', 'hand.mtl', 'hand.png', 'hand_open.obj', 'hand_fist.obj',
+56
View File
@@ -0,0 +1,56 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "kgsl_busy.h"
#define ENTRIES 819
static uint32_t memstore[ENTRIES * KGSL_BUSY_ENTRY_WORDS];
static void stamp(int entry, uint32_t sop, uint32_t eop) {
memstore[entry * KGSL_BUSY_ENTRY_WORDS + KGSL_BUSY_SOP] = sop;
memstore[entry * KGSL_BUSY_ENTRY_WORDS + KGSL_BUSY_EOP] = eop;
}
int main(void) {
assert(kgsl_busy_first_slot(8 * 4096) == 813);
assert(kgsl_busy_first_slot(8 * 16384) == 3270);
assert(kgsl_busy_first_slot(7 * 40) == -1);
int first = kgsl_busy_first_slot(sizeof(memstore));
uint32_t last[KGSL_BUSY_SLOTS] = {0};
int moved = 0;
stamp(first, 100, 100);
stamp(first + 2, 7, 7);
assert(kgsl_busy_sample(memstore, first, last, &moved) == 0);
assert(moved == 1 && last[0] == 100 && last[2] == 7);
moved = 0;
assert(kgsl_busy_sample(memstore, first, last, &moved) == 0);
assert(moved == 0);
stamp(first + 2, 8, 7);
assert(kgsl_busy_sample(memstore, first, last, &moved) == 1);
assert(moved == 1);
moved = 0;
stamp(first + 2, 8, 8);
assert(kgsl_busy_sample(memstore, first, last, &moved) == 0);
assert(moved == 0);
stamp(first + 4, 3, 2);
assert(kgsl_busy_sample(memstore, first, last, &moved) == 1);
memset(memstore, 0, sizeof(memstore));
stamp(first - 1, 5, 4);
stamp(first + KGSL_BUSY_SLOTS, 9, 8);
memset(last, 0, sizeof(last));
moved = 0;
assert(kgsl_busy_sample(memstore, first, last, &moved) == 0);
assert(moved == 0);
puts("PASS: kgsl memstore busy");
return 0;
}
+33
View File
@@ -0,0 +1,33 @@
"""Exercise the HUD's kgsl memstore busy sampling (app/src/main/cpp/gpuclock/kgsl_busy.h)."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
REPO = Path(__file__).resolve().parents[2]
class KgslBusyTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
compiler = shutil.which(os.environ.get('CC', 'cc'))
if compiler is None:
raise unittest.SkipTest('A host C compiler is required')
cls.temp = tempfile.TemporaryDirectory(prefix='droiddeck-kgsl-busy-')
cls.addClassCleanup(cls.temp.cleanup)
cls.binary = Path(cls.temp.name) / 'kgsl-busy-test'
subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror',
'-I', str(REPO / 'app/src/main/cpp/gpuclock'),
str(REPO / 'tools/tests/kgsl_busy_harness.c'), '-o', str(cls.binary)],
check=True, capture_output=True, text=True)
def test_ringbuffer_slots_and_busy_rule(self):
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)
if __name__ == '__main__':
unittest.main()