diff --git a/app/src/main/cpp/waylandcomp/CMakeLists.txt b/app/src/main/cpp/waylandcomp/CMakeLists.txt index 33585818b..f8eb31291 100644 --- a/app/src/main/cpp/waylandcomp/CMakeLists.txt +++ b/app/src/main/cpp/waylandcomp/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(droiddeckwayland SHARED src/sc_layer.c src/ahb_swapchain.c src/waylandcomp_jni.c + src/gpu_clock_pin.c src/droiddeck_ext.c src/wl_dmabuf.c src/wl_clipboard.c diff --git a/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c b/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c new file mode 100644 index 000000000..5b079da97 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c @@ -0,0 +1,160 @@ +#include "gpu_clock_pin.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../adrenotools/src/hook/kgsl.h" + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_t worker; +static bool joinable; +static atomic_bool running; + +struct pin { + int fd; + unsigned int context; + unsigned int memory; + unsigned int timestamp; + unsigned long gpuaddr; + void *commands; + size_t size; +}; + +static struct pin pin = { .fd = -1 }; + +static void release_pin(void) { + if (pin.context) { + struct kgsl_drawctxt_destroy destroy = { .drawctxt_id = pin.context }; + ioctl(pin.fd, IOCTL_KGSL_DRAWCTXT_DESTROY, &destroy); + } + if (pin.commands) munmap(pin.commands, pin.size); + if (pin.memory) { + struct kgsl_gpumem_free_id free_memory = { .id = pin.memory }; + ioctl(pin.fd, IOCTL_KGSL_GPUMEM_FREE_ID, &free_memory); + } + if (pin.fd >= 0) close(pin.fd); + pin = (struct pin) { .fd = -1 }; +} + +static bool submit_pin(void) { + struct kgsl_command_object object = { + .gpuaddr = pin.gpuaddr, + .size = 2 * sizeof(uint32_t), + .flags = KGSL_CMDLIST_IB, + .id = pin.memory, + }; + struct kgsl_gpu_command command = { + .flags = KGSL_CMDBATCH_PWR_CONSTRAINT, + .cmdlist = (uintptr_t)&object, + .cmdsize = sizeof(object), + .numcmds = 1, + .context_id = pin.context, + }; + if (ioctl(pin.fd, IOCTL_KGSL_GPU_COMMAND, &command) < 0) return false; + pin.timestamp = command.timestamp; + return true; +} + +static bool prepare_pin(void) { + pin.fd = open("/dev/kgsl-3d0", O_RDWR | O_CLOEXEC); + if (pin.fd < 0) return false; + struct kgsl_drawctxt_create context = { + .flags = KGSL_CONTEXT_NO_GMEM_ALLOC | KGSL_CONTEXT_PREAMBLE | KGSL_CONTEXT_PWR_CONSTRAINT, + }; + if (ioctl(pin.fd, IOCTL_KGSL_DRAWCTXT_CREATE, &context) < 0) return false; + pin.context = context.drawctxt_id; + struct kgsl_device_constraint_pwrlevel level = { .level = KGSL_CONSTRAINT_PWR_MAX }; + struct kgsl_device_constraint constraint = { + .type = KGSL_CONSTRAINT_PWRLEVEL, + .context_id = pin.context, + .data = &level, + .size = sizeof(level), + }; + struct kgsl_device_getproperty property = { + .type = KGSL_PROP_PWR_CONSTRAINT, + .value = &constraint, + .sizebytes = sizeof(constraint), + }; + if (ioctl(pin.fd, IOCTL_KGSL_SETPROPERTY, &property) < 0) return false; + struct kgsl_gpumem_alloc_id memory = { .size = 4096 }; + if (ioctl(pin.fd, IOCTL_KGSL_GPUMEM_ALLOC_ID, &memory) < 0) return false; + pin.memory = memory.id; + pin.gpuaddr = memory.gpuaddr; + pin.size = memory.mmapsize; + void *commands = mmap(NULL, pin.size, PROT_READ | PROT_WRITE, MAP_SHARED, pin.fd, + (off_t)pin.memory << 12); + if (commands == MAP_FAILED) return false; + pin.commands = commands; + // Type-7 CP_NOP, one payload dword; both odd-parity bits are zero. + uint32_t *words = commands; + words[0] = 0x70100001; + words[1] = 0; + struct kgsl_gpumem_sync_cache sync = { .id = pin.memory, .op = KGSL_GPUMEM_CACHE_CLEAN }; + if (ioctl(pin.fd, IOCTL_KGSL_GPUMEM_SYNC_CACHE, &sync) < 0) return false; + return submit_pin(); +} + +static void *hold_pin(void *unused) { + (void)unused; + while (atomic_load(&running)) { + struct kgsl_device_waittimestamp_ctxtid wait = { + .context_id = pin.context, + .timestamp = pin.timestamp, + .timeout = 100, + }; + if (ioctl(pin.fd, IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID, &wait) < 0) { + if (errno == EINTR || errno == ETIMEDOUT) continue; + break; + } + // Constraints expire after idle_timer (normally 80 ms); renew with a NOP, keeping devfreq alive. + usleep(25000); + if (atomic_load(&running) && !submit_pin()) break; + } + if (atomic_exchange(&running, false)) + __android_log_print(ANDROID_LOG_WARN, "GpuClockPin", "constraint stopped: %s", strerror(errno)); + release_pin(); + return NULL; +} + +bool gpu_clock_pin_set(bool on) { + pthread_mutex_lock(&lock); + if (joinable) { + if (on && atomic_load(&running)) { + pthread_mutex_unlock(&lock); + return true; + } + atomic_store(&running, false); + pthread_join(worker, NULL); + joinable = false; + } + bool ok = true; + if (on) { + ok = prepare_pin(); + if (ok) { + atomic_store(&running, true); + int error = pthread_create(&worker, NULL, hold_pin, NULL); + if (error) { + atomic_store(&running, false); + errno = error; + ok = false; + } else { + joinable = true; + } + } + if (!ok) { + int error = errno; + release_pin(); + errno = error; + } + } + pthread_mutex_unlock(&lock); + return ok; +} diff --git a/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.h b/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.h new file mode 100644 index 000000000..5fe17d102 --- /dev/null +++ b/app/src/main/cpp/waylandcomp/src/gpu_clock_pin.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +bool gpu_clock_pin_set(bool on); diff --git a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c index 6a407088b..bc349cdf6 100644 --- a/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c +++ b/app/src/main/cpp/waylandcomp/src/waylandcomp_jni.c @@ -16,6 +16,7 @@ #include "effects_chain.h" #include "cardboard.h" #include "droiddeck_color.h" +#include "gpu_clock_pin.h" extern int droiddeck_wayland_run(void); extern void droiddeck_wayland_set_scale_mode(int mode, int alignment); @@ -634,8 +635,14 @@ Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeTextInputDelete(JNIE extern void adrenotools_set_turbo(bool turbo); JNIEXPORT void JNICALL Java_com_droiddeck_launcher_wayland_WaylandCompositor_nativeSetGpuTurbo(JNIEnv *env, jclass clazz, jboolean on) { - (void)env; (void)clazz; - adrenotools_set_turbo(on == JNI_TRUE); + (void)clazz; + if (!gpu_clock_pin_set(on == JNI_TRUE)) { + int saved_errno = errno; + jclass error = (*env)->FindClass(env, "java/lang/IllegalStateException"); + if (error) (*env)->ThrowNew(env, error, strerror(saved_errno)); + } + // Clear a legacy global pin when recovering its marker after an app upgrade or crash. + if (on != JNI_TRUE) adrenotools_set_turbo(false); } JNIEXPORT jint JNICALL diff --git a/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt b/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt index 7855c4959..40135f08d 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GpuClockPin.kt @@ -26,6 +26,7 @@ object GpuClockPin { pinned = true "gpu clock: held at its top" } catch (t: Throwable) { + File(context.filesDir, MARKER).delete() Log.w(TAG, "pin", t) "gpu clock: pin failed" } diff --git a/tools/tests/test_gpu_clock_pin.c b/tools/tests/test_gpu_clock_pin.c new file mode 100644 index 000000000..8e3f0b6a4 --- /dev/null +++ b/tools/tests/test_gpu_clock_pin.c @@ -0,0 +1,172 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../app/src/main/cpp/adrenotools/src/hook/kgsl.h" +#include "../../app/src/main/cpp/waylandcomp/src/gpu_clock_pin.h" + +enum { NONE, OPEN, CONTEXT, CONSTRAINT, MEMORY, MAP, SYNC, SUBMIT, THREAD, WAIT }; +static atomic_int failure, descriptors, contexts, memories, mappings, submissions; +static atomic_bool timeout_wait; +static uint32_t commands[1024]; + +int __android_log_print(int priority, const char *tag, const char *format, ...) { + (void)priority; (void)tag; (void)format; + return 0; +} + +int __wrap_open(const char *path, int flags, ...) { + (void)flags; + assert(!strcmp(path, "/dev/kgsl-3d0")); + if (atomic_load(&failure) == OPEN) { errno = EACCES; return -1; } + assert(atomic_fetch_add(&descriptors, 1) == 0); + return 91; +} + +int __wrap_close(int fd) { + assert(fd == 91); + assert(atomic_fetch_sub(&descriptors, 1) == 1); + errno = EBADF; + return 0; +} + +void *__wrap_mmap(void *address, size_t size, int protection, int flags, int fd, off_t offset) { + (void)address; (void)protection; (void)flags; + assert(fd == 91 && size == sizeof(commands) && offset == (23 << 12)); + if (atomic_load(&failure) == MAP) { errno = EACCES; return MAP_FAILED; } + assert(atomic_fetch_add(&mappings, 1) == 0); + return commands; +} + +int __wrap_munmap(void *address, size_t size) { + assert(address == commands && size == sizeof(commands)); + assert(atomic_fetch_sub(&mappings, 1) == 1); + return 0; +} + +#ifdef __ANDROID__ +int __wrap_ioctl(int fd, int request, ...) { +#else +int __wrap_ioctl(int fd, unsigned long request, ...) { +#endif + assert(fd == 91); + va_list args; + va_start(args, request); + void *arg = va_arg(args, void *); + va_end(args); + int step = NONE; + switch ((unsigned int)request) { + case IOCTL_KGSL_DRAWCTXT_CREATE: step = CONTEXT; break; + case IOCTL_KGSL_SETPROPERTY: step = CONSTRAINT; break; + case IOCTL_KGSL_GPUMEM_ALLOC_ID: step = MEMORY; break; + case IOCTL_KGSL_GPUMEM_SYNC_CACHE: step = SYNC; break; + case IOCTL_KGSL_GPU_COMMAND: step = SUBMIT; break; + case IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: step = WAIT; break; + } + if (step && atomic_load(&failure) == step) { errno = EACCES; return -1; } + switch ((unsigned int)request) { + case IOCTL_KGSL_DRAWCTXT_CREATE: { + struct kgsl_drawctxt_create *context = arg; + assert(context->flags & KGSL_CONTEXT_PWR_CONSTRAINT); + context->drawctxt_id = 17; + atomic_fetch_add(&contexts, 1); + break; + } + case IOCTL_KGSL_SETPROPERTY: { + struct kgsl_device_getproperty *property = arg; + assert(property->type == KGSL_PROP_PWR_CONSTRAINT); + struct kgsl_device_constraint *constraint = property->value; + assert(constraint->context_id == 17 && constraint->type == KGSL_CONSTRAINT_PWRLEVEL); + assert(((struct kgsl_device_constraint_pwrlevel *)constraint->data)->level == KGSL_CONSTRAINT_PWR_MAX); + break; + } + case IOCTL_KGSL_GPUMEM_ALLOC_ID: { + struct kgsl_gpumem_alloc_id *memory = arg; + memory->id = 23; memory->gpuaddr = 0x100000; memory->mmapsize = sizeof(commands); + atomic_fetch_add(&memories, 1); + break; + } + case IOCTL_KGSL_GPUMEM_SYNC_CACHE: + assert(((struct kgsl_gpumem_sync_cache *)arg)->op == KGSL_GPUMEM_CACHE_CLEAN); + break; + case IOCTL_KGSL_GPU_COMMAND: { + struct kgsl_gpu_command *command = arg; + struct kgsl_command_object *object = (void *)(uintptr_t)command->cmdlist; + assert(command->context_id == 17 && command->numcmds == 1); + assert(command->flags & KGSL_CMDBATCH_PWR_CONSTRAINT); + assert(object->id == 23 && object->gpuaddr == 0x100000 && object->size == 8); + assert(commands[0] == 0x70100001 && commands[1] == 0); + command->timestamp = atomic_fetch_add(&submissions, 1) + 1; + break; + } + case IOCTL_KGSL_DEVICE_WAITTIMESTAMP_CTXTID: + if (atomic_load(&timeout_wait)) { errno = ETIMEDOUT; return -1; } + break; + case IOCTL_KGSL_DRAWCTXT_DESTROY: + assert(atomic_fetch_sub(&contexts, 1) == 1); + break; + case IOCTL_KGSL_GPUMEM_FREE_ID: + assert(atomic_fetch_sub(&memories, 1) == 1); + break; + default: assert(!"unexpected ioctl"); + } + return 0; +} + +int __real_pthread_create(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *); +int __wrap_pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start)(void *), void *arg) { + if (atomic_load(&failure) == THREAD) return EAGAIN; + return __real_pthread_create(thread, attr, start, arg); +} + +static void assert_released(void) { + assert(!atomic_load(&descriptors) && !atomic_load(&contexts)); + assert(!atomic_load(&memories) && !atomic_load(&mappings)); +} + +int main(void) { + assert(gpu_clock_pin_set(false)); + assert_released(); + for (int step = OPEN; step <= THREAD; ++step) { + atomic_store(&failure, step); + assert(!gpu_clock_pin_set(true)); + assert(errno == (step == THREAD ? EAGAIN : EACCES)); + assert_released(); + } + atomic_store(&failure, NONE); + atomic_store(&submissions, 0); + assert(gpu_clock_pin_set(true)); + assert(gpu_clock_pin_set(true)); + usleep(80000); + assert(atomic_load(&submissions) > 1); + assert(gpu_clock_pin_set(false)); + assert(gpu_clock_pin_set(false)); + assert_released(); + + atomic_store(&timeout_wait, true); + atomic_store(&submissions, 0); + assert(gpu_clock_pin_set(true)); + usleep(80000); + assert(atomic_load(&submissions) == 1); + assert(gpu_clock_pin_set(false)); + assert_released(); + atomic_store(&timeout_wait, false); + + atomic_store(&failure, WAIT); + assert(gpu_clock_pin_set(true)); + for (int i = 0; i < 100 && atomic_load(&descriptors); ++i) usleep(10000); + assert_released(); + atomic_store(&failure, NONE); + assert(gpu_clock_pin_set(true)); + assert(gpu_clock_pin_set(false)); + assert_released(); + puts("gpu clock pin: failure cleanup, renewal, bounded queue and restart passed"); + return 0; +} diff --git a/tools/tests/test_gpu_clock_pin.py b/tools/tests/test_gpu_clock_pin.py new file mode 100644 index 000000000..f6f466871 --- /dev/null +++ b/tools/tests/test_gpu_clock_pin.py @@ -0,0 +1,30 @@ +import pathlib +import shutil +import subprocess +import sys +import tempfile +import unittest + + +ROOT = pathlib.Path(__file__).resolve().parents[2] + + +class GpuClockPinTest(unittest.TestCase): + @unittest.skipUnless(sys.platform.startswith('linux') and shutil.which('cc'), 'requires a Linux C compiler') + def test_constraint_lifecycle(self): + with tempfile.TemporaryDirectory() as tmp: + directory = pathlib.Path(tmp) + (directory / 'android').mkdir() + (directory / 'android/log.h').write_text( + '#define ANDROID_LOG_WARN 5\n' + 'int __android_log_print(int, const char *, const char *, ...);\n') + binary = directory / 'gpu-clock-pin-test' + subprocess.run([ + 'cc', '-std=c11', '-D_DEFAULT_SOURCE', '-D__user=', '-Wall', '-Wextra', '-Werror', + '-I', str(directory), '-pthread', + str(ROOT / 'tools/tests/test_gpu_clock_pin.c'), + str(ROOT / 'app/src/main/cpp/waylandcomp/src/gpu_clock_pin.c'), + '-Wl,--wrap=open,--wrap=close,--wrap=mmap,--wrap=munmap,--wrap=ioctl,--wrap=pthread_create', + '-o', str(binary), + ], check=True) + subprocess.run([str(binary)], check=True, timeout=10)