// vkshim.c — Vulkan loader interposer for jank-hunt (see // docs/BUGS/scroll-jank-120hz.md). // // Packaged INSIDE the app APK as lib//libvulkan.so. The app's linker // namespace searches the APK lib dir before /system/lib64, so when wgpu-hal // dlopens "libvulkan.so" it gets THIS library instead of the system loader. // We forward every call to the real /system/lib64/libvulkan.so (dlopened by // absolute path in the constructor) and time a curated set of device-level // calls. Slow ones are logged (with a backtrace) to // /sdcard/Download/.gallery_vk.log. // // This answers, for each slow frame: WHICH vk call blocked, with what // arguments (e.g. AcquireNextImage timeout), and from which code path // (stack: libgallery.so = Skia/wgpu/Slint; libvulkan.so/loader = ICD dispatch; // libmali = driver; libc futex = fence/poll wait). // // Threshold: 2 ms default, override with env VKSHIM_MS (milliseconds). #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include // We intentionally do NOT include (the NDK sysroot has it, // but a lean hand-rolled subset keeps this file self-contained and avoids any // ABI drift with the loader's actual structs — we only read fixed offsets). #ifndef VKAPI_PTR #define VKAPI_PTR #endif #ifndef VKAPI_ATTR #define VKAPI_ATTR #endif #ifndef VKAPI_CALL #define VKAPI_CALL #endif typedef void *VkInstance; typedef void *VkDevice; typedef void *VkQueue; typedef void *VkDeviceMemory; typedef void *VkSemaphore; typedef void *VkFence; typedef void *VkCommandBuffer; typedef void *VkSurfaceKHR; typedef void *VkSwapchainKHR; typedef uint32_t VkResult; typedef uint64_t VkDeviceSize; typedef uint32_t VkPipelineStageFlags; typedef void (VKAPI_PTR *PFN_vkVoidFunction)(void); // GetInstanceProcAddr / GetDeviceProcAddr shape: handle + name -> function ptr. typedef PFN_vkVoidFunction (*GetProcAddrFn)(void *handle, const char *name); // --------------------------------------------------------------------------- // Globals // --------------------------------------------------------------------------- static void *g_real = NULL; static PFN_vkVoidFunction g_real_CreateInstance; static PFN_vkVoidFunction g_real_CreateDevice; static GetProcAddrFn g_real_GetInstanceProcAddr; static GetProcAddrFn g_real_GetDeviceProcAddr; static int g_logfd = -1; static uint64_t g_threshold_us = 2000; // 2 ms // Per-frame aggregate (reset at each present). Answers "how much of a slow // frame was actually inside vk calls vs between them (CPU work / desched / // sub-threshold polling loops)". Single-threaded in this app (UI thread // drives all Vulkan calls), so plain statics suffice. static uint64_t now_ns(void); static uint32_t f_sub_n; static uint64_t f_sub_ns; static uint32_t f_acq_n; static uint64_t f_acq_ns; static uint32_t f_o_n; static uint64_t f_o_ns; static void f_count(int which, uint64_t ns) { switch (which) { case 0: f_sub_n++; f_sub_ns += ns; break; case 1: f_acq_n++; f_acq_ns += ns; break; default: f_o_n++; f_o_ns += ns; break; } } static void f_report(uint32_t pre_rc) { char b[160]; int n = snprintf(b, sizeof(b), "VKF fsub=%u/%llu facq=%u/%llu fo=%u/%llu ts=%llu pre_rc=%u\n", f_sub_n, (unsigned long long)f_sub_ns, f_acq_n, (unsigned long long)f_acq_ns, f_o_n, (unsigned long long)f_o_ns, (unsigned long long)now_ns(), pre_rc); if (g_logfd >= 0) { ssize_t r = write(g_logfd, b, (size_t)n); (void)r; } f_sub_n = f_sub_ns = f_acq_n = f_acq_ns = f_o_n = f_o_ns = 0; } 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; } // --------------------------------------------------------------------------- // Backtrace capture (libunwind) + module labeling via dladdr // --------------------------------------------------------------------------- struct bt_ctx { uintptr_t addrs[20]; int n; }; static _Unwind_Reason_Code bt_cb(struct _Unwind_Context *ctx, void *arg) { struct bt_ctx *c = (struct bt_ctx *)arg; uintptr_t ip = (uintptr_t)_Unwind_GetIP(ctx); if (ip) c->addrs[c->n++] = ip; if (c->n >= 20) return _URC_END_OF_STACK; return _URC_NO_REASON; } static void log_line(const char *name, uint64_t dt_us, uint32_t rc, const char *args, uint64_t calls, int with_stack) { char buf[2048]; int off = snprintf(buf, sizeof(buf), "VKC %-28s dt=%6llu rc=%u %s calls=%llu ts=%llu", name, (unsigned long long)dt_us, rc, args ? args : "", (unsigned long long)calls, (unsigned long long)now_ns()); if (with_stack) { struct bt_ctx c; memset(&c, 0, sizeof(c)); _Unwind_Backtrace(bt_cb, &c); for (int i = 0; i < c.n && off < (int)sizeof(buf) - 160; i++) { Dl_info info; const char *mod = "?"; uintptr_t base = 0; if (dladdr((void *)c.addrs[i], &info)) { if (info.dli_fname) { const char *b = strrchr(info.dli_fname, '/'); mod = b ? b + 1 : info.dli_fname; } base = (uintptr_t)info.dli_fbase; } off += snprintf(buf + off, sizeof(buf) - off, " %s+0x%llx", mod, (unsigned long long)(c.addrs[i] - base)); } } buf[off++] = '\n'; if (g_logfd >= 0) { ssize_t r = write(g_logfd, buf, (size_t)off); (void)r; } } // --------------------------------------------------------------------------- // Wrapped device-level functions. // All Vulkan functions we intercept return VkResult (uint32). // Argument structs: only FIXED OFFSETS are read (64-bit ABI): // VkSubmitInfo: [4]=waitCount [10]=commandBufferCount (size 64) // VkPresentInfoKHR: [4]=waitCount [8]=swapchainCount // --------------------------------------------------------------------------- #define INTERCEPT_TABLE \ X(vkQueueSubmit) \ X(vkQueuePresentKHR) \ X(vkAcquireNextImageKHR) \ X(vkQueueWaitIdle) \ X(vkDeviceWaitIdle) \ X(vkWaitForFences) \ X(vkAllocateMemory) \ X(vkMapMemory) \ X(vkCreateShaderModule) \ X(vkCreateGraphicsPipelines) \ X(vkCreateComputePipelines) \ X(vkCreateSwapchainKHR) \ X(vkDestroySwapchainKHR) \ X(vkResetCommandPool) struct intercept_entry { const char *name; PFN_vkVoidFunction real; uint64_t calls; void *wrap; }; // Forward declaration (the table itself is defined below the wrappers, which // need the per-entry call counters). static struct intercept_entry g_entries[14]; static PFN_vkVoidFunction g_real_vkQueueSubmit; static PFN_vkVoidFunction g_real_vkQueuePresentKHR; static PFN_vkVoidFunction g_real_vkAcquireNextImageKHR; static PFN_vkVoidFunction g_real_vkQueueWaitIdle; static PFN_vkVoidFunction g_real_vkDeviceWaitIdle; static PFN_vkVoidFunction g_real_vkWaitForFences; static PFN_vkVoidFunction g_real_vkAllocateMemory; static PFN_vkVoidFunction g_real_vkMapMemory; static PFN_vkVoidFunction g_real_vkCreateShaderModule; static PFN_vkVoidFunction g_real_vkCreateGraphicsPipelines; static PFN_vkVoidFunction g_real_vkCreateComputePipelines; static PFN_vkVoidFunction g_real_vkCreateSwapchainKHR; static PFN_vkVoidFunction g_real_vkDestroySwapchainKHR; static PFN_vkVoidFunction g_real_vkResetCommandPool; static uint32_t w_vkQueueSubmit(VkQueue q, uint32_t n, const void *pSubmits, void *fence) { uint32_t cmds = 0, waits = 0; if (pSubmits) for (uint32_t i = 0; i < n; i++) { const uint32_t *s = (const uint32_t *)((const char *)pSubmits + i * 64); cmds += s[10]; waits += s[4]; } char a[64]; snprintf(a, sizeof(a), "nSubmits=%u cmds=%u waits=%u", n, cmds, waits); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkQueue, uint32_t, const void *, void *))g_real_vkQueueSubmit)( q, n, pSubmits, fence); uint64_t dt = (now_ns() - t0) / 1000; f_count(0, dt * 1000); if (dt >= g_threshold_us) log_line("vkQueueSubmit", dt, rc, a, ++g_entries[0].calls, 1); return rc; } static uint32_t w_vkQueuePresentKHR(VkQueue q, const void *pPresent) { uint32_t swaps = 0; if (pPresent) swaps = ((const uint32_t *)pPresent)[8]; char a[32]; snprintf(a, sizeof(a), "swaps=%u", swaps); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkQueue, const void *))g_real_vkQueuePresentKHR)(q, pPresent); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkQueuePresentKHR", dt, rc, a, ++g_entries[1].calls, 1); f_report(rc); return rc; } static uint32_t w_vkAcquireNextImageKHR(VkDevice d, void *swap, uint64_t timeout, void *sem, void *fence, uint32_t *pIdx) { char a[64]; snprintf(a, sizeof(a), "timeout=%llu", (unsigned long long)timeout); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, void *, uint64_t, void *, void *, uint32_t *)) g_real_vkAcquireNextImageKHR)(d, swap, timeout, sem, fence, pIdx); uint64_t dt = (now_ns() - t0) / 1000; f_count(1, dt * 1000); if (dt >= g_threshold_us) log_line("vkAcquireNextImageKHR", dt, rc, a, ++g_entries[2].calls, 1); return rc; } static uint32_t w_vkQueueWaitIdle(VkQueue q) { uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkQueue))g_real_vkQueueWaitIdle)(q); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkQueueWaitIdle", dt, rc, "", ++g_entries[3].calls, 1); return rc; } static uint32_t w_vkDeviceWaitIdle(VkDevice d) { uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice))g_real_vkDeviceWaitIdle)(d); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkDeviceWaitIdle", dt, rc, "", ++g_entries[4].calls, 1); return rc; } static uint32_t w_vkWaitForFences(VkDevice d, uint32_t n, const void *fences, uint32_t waitAll, uint64_t timeout) { char a[64]; snprintf(a, sizeof(a), "n=%u all=%u timeout=%llu", n, waitAll, (unsigned long long)timeout); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, uint32_t, const void *, uint32_t, uint64_t)) g_real_vkWaitForFences)(d, n, fences, waitAll, timeout); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkWaitForFences", dt, rc, a, ++g_entries[5].calls, 1); return rc; } static uint32_t w_vkAllocateMemory(VkDevice d, const void *pInfo, const void *pAlloc, void *pMem) { char a[64]; // VkMemoryAllocateInfo: [0]=sType [8]=pNext [16]=allocationSize [24]=memoryTypeIndex if (pInfo) { const uint32_t *s = (const uint32_t *)pInfo; snprintf(a, sizeof(a), "typeIdx=%u size=%llu", s[6], (unsigned long long)(((const uint64_t *)pInfo)[2])); } else { a[0] = 0; } uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, const void *, const void *, void *)) g_real_vkAllocateMemory)(d, pInfo, pAlloc, pMem); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkAllocateMemory", dt, rc, a, ++g_entries[6].calls, 1); return rc; } static uint32_t w_vkMapMemory(VkDevice d, void *mem, VkDeviceSize off, VkDeviceSize size, uint32_t flags, void *ppData) { char a[64]; snprintf(a, sizeof(a), "off=%llu size=%llu", (unsigned long long)off, (unsigned long long)size); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, void *, VkDeviceSize, VkDeviceSize, uint32_t, void *)) g_real_vkMapMemory)(d, mem, off, size, flags, ppData); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkMapMemory", dt, rc, a, ++g_entries[7].calls, 1); return rc; } static uint32_t w_vkCreateShaderModule(VkDevice d, const void *pInfo, const void *pAlloc, void *pModule) { char a[48]; if (pInfo) { const uint64_t *s = (const uint64_t *)pInfo; snprintf(a, sizeof(a), "codeKB=%llu", (unsigned long long)(s[1] / 1024)); } else { a[0] = 0; } uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, const void *, const void *, void *)) g_real_vkCreateShaderModule)(d, pInfo, pAlloc, pModule); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkCreateShaderModule", dt, rc, a, ++g_entries[8].calls, 1); return rc; } static uint32_t w_vkCreateGraphicsPipelines(VkDevice d, void *cache, uint32_t n, const void *pInfos, const void *pAlloc, void *pPipelines) { char a[32]; snprintf(a, sizeof(a), "n=%u", n); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, void *, uint32_t, const void *, const void *, void *)) g_real_vkCreateGraphicsPipelines)(d, cache, n, pInfos, pAlloc, pPipelines); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkCreateGraphicsPipelines", dt, rc, a, ++g_entries[9].calls, 1); return rc; } static uint32_t w_vkCreateComputePipelines(VkDevice d, void *cache, uint32_t n, const void *pInfos, const void *pAlloc, void *pPipelines) { char a[32]; snprintf(a, sizeof(a), "n=%u", n); uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, void *, uint32_t, const void *, const void *, void *)) g_real_vkCreateComputePipelines)(d, cache, n, pInfos, pAlloc, pPipelines); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkCreateComputePipelines", dt, rc, a, ++g_entries[10].calls, 1); return rc; } static uint32_t w_vkCreateSwapchainKHR(VkDevice d, const void *pInfo, const void *pAlloc, void *pSwap) { char a[32]; if (pInfo) snprintf(a, sizeof(a), "images=%u", ((const uint32_t *)pInfo)[8]); else a[0] = 0; uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, const void *, const void *, void *)) g_real_vkCreateSwapchainKHR)(d, pInfo, pAlloc, pSwap); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkCreateSwapchainKHR", dt, rc, a, ++g_entries[11].calls, 1); return rc; } static uint32_t w_vkDestroySwapchainKHR(VkDevice d, void *swap, const void *pAlloc) { uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, void *, const void *))g_real_vkDestroySwapchainKHR)( d, swap, pAlloc); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkDestroySwapchainKHR", dt, rc, "", ++g_entries[12].calls, 1); return rc; } static uint32_t w_vkResetCommandPool(VkDevice d, void *pool, uint32_t flags) { uint64_t t0 = now_ns(); uint32_t rc = ((VkResult (*)(VkDevice, void *, uint32_t))g_real_vkResetCommandPool)(d, pool, flags); uint64_t dt = (now_ns() - t0) / 1000; if (dt >= g_threshold_us) log_line("vkResetCommandPool", dt, rc, "", ++g_entries[13].calls, 1); return rc; } // Table index order must match the g_entries[N] used above. static struct intercept_entry g_entries[14] = { {"vkQueueSubmit", NULL, 0, (void *)w_vkQueueSubmit}, {"vkQueuePresentKHR", NULL, 0, (void *)w_vkQueuePresentKHR}, {"vkAcquireNextImageKHR", NULL, 0, (void *)w_vkAcquireNextImageKHR}, {"vkQueueWaitIdle", NULL, 0, (void *)w_vkQueueWaitIdle}, {"vkDeviceWaitIdle", NULL, 0, (void *)w_vkDeviceWaitIdle}, {"vkWaitForFences", NULL, 0, (void *)w_vkWaitForFences}, {"vkAllocateMemory", NULL, 0, (void *)w_vkAllocateMemory}, {"vkMapMemory", NULL, 0, (void *)w_vkMapMemory}, {"vkCreateShaderModule", NULL, 0, (void *)w_vkCreateShaderModule}, {"vkCreateGraphicsPipelines", NULL, 0, (void *)w_vkCreateGraphicsPipelines}, {"vkCreateComputePipelines", NULL, 0, (void *)w_vkCreateComputePipelines}, {"vkCreateSwapchainKHR", NULL, 0, (void *)w_vkCreateSwapchainKHR}, {"vkDestroySwapchainKHR", NULL, 0, (void *)w_vkDestroySwapchainKHR}, {"vkResetCommandPool", NULL, 0, (void *)w_vkResetCommandPool}, }; // Pointers to the real-func slots, so shim_GetDeviceProcAddr can publish the // lazily-resolved real pointers into the globals the wrappers call. static PFN_vkVoidFunction *g_entry_real[14] = { &g_real_vkQueueSubmit, &g_real_vkQueuePresentKHR, &g_real_vkAcquireNextImageKHR, &g_real_vkQueueWaitIdle, &g_real_vkDeviceWaitIdle, &g_real_vkWaitForFences, &g_real_vkAllocateMemory, &g_real_vkMapMemory, &g_real_vkCreateShaderModule, &g_real_vkCreateGraphicsPipelines, &g_real_vkCreateComputePipelines, &g_real_vkCreateSwapchainKHR, &g_real_vkDestroySwapchainKHR, &g_real_vkResetCommandPool, }; // --------------------------------------------------------------------------- // Proc-addr dispatch // --------------------------------------------------------------------------- static PFN_vkVoidFunction shim_GetDeviceProcAddr(VkDevice dev, const char *name) { for (int i = 0; i < 14; i++) { if (strcmp(name, g_entries[i].name) == 0) { if (!g_entries[i].real) { g_entries[i].real = g_real_GetDeviceProcAddr(dev, name); *g_entry_real[i] = g_entries[i].real; } return (PFN_vkVoidFunction)g_entries[i].wrap; } } return g_real_GetDeviceProcAddr(dev, name); } static VkResult shim_CreateDevice(VkDevice d, const void *pInfo, const void *pAlloc, void *pDev) { uint64_t t0 = now_ns(); VkResult rc = ((VkResult (*)(VkDevice, const void *, const void *, void *))g_real_CreateDevice)( d, pInfo, pAlloc, pDev); uint64_t dt = (now_ns() - t0) / 1000; log_line("vkCreateDevice", dt, rc, "one-shot", 1, dt >= g_threshold_us); return rc; } static PFN_vkVoidFunction shim_GetInstanceProcAddr(VkInstance inst, const char *name) { if (!strcmp(name, "vkGetDeviceProcAddr")) return (PFN_vkVoidFunction)shim_GetDeviceProcAddr; if (!strcmp(name, "vkCreateDevice")) return (PFN_vkVoidFunction)shim_CreateDevice; return g_real_GetInstanceProcAddr(inst, name); } static VkResult shim_CreateInstance(const void *pInfo, const void *pAlloc, void *pInst) { uint64_t t0 = now_ns(); VkResult rc = ((VkResult (*)(const void *, const void *, void *))g_real_CreateInstance)( pInfo, pAlloc, pInst); uint64_t dt = (now_ns() - t0) / 1000; log_line("vkCreateInstance", dt, rc, "one-shot", 1, dt >= g_threshold_us); return rc; } // --------------------------------------------------------------------------- // Entry points exported to the app (ash dlsyms vkGetInstanceProcAddr; other // symbols may be dlsym'd directly by other tooling, so export the common ones). // --------------------------------------------------------------------------- VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance inst, const char *name) { return shim_GetInstanceProcAddr(inst, name); } VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(const void *pCreateInfo, const void *pAllocator, void *pInstance) { return shim_CreateInstance(pCreateInfo, pAllocator, pInstance); } VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(VkInstance instance, const void *pCreateInfo, const void *pAllocator, void *pDevice) { return shim_CreateDevice(instance, pCreateInfo, pAllocator, pDevice); } VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device, const char *pName) { return shim_GetDeviceProcAddr(device, pName); } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- __attribute__((constructor)) static void vkshim_init(void) { g_real = dlopen("/system/lib64/libvulkan.so", RTLD_NOW | RTLD_LOCAL); if (!g_real) { g_logfd = open("/sdcard/Download/.gallery_vk.log", O_WRONLY | O_CREAT | O_APPEND, 0644); const char m[] = "VKSHIM FAIL: could not dlopen /system/lib64/libvulkan.so\n"; if (g_logfd >= 0) write(g_logfd, m, sizeof(m) - 1); return; } g_real_CreateInstance = (PFN_vkVoidFunction)dlsym(g_real, "vkCreateInstance"); g_real_CreateDevice = (PFN_vkVoidFunction)dlsym(g_real, "vkCreateDevice"); g_real_GetInstanceProcAddr = (GetProcAddrFn)dlsym(g_real, "vkGetInstanceProcAddr"); g_real_GetDeviceProcAddr = (GetProcAddrFn)dlsym(g_real, "vkGetDeviceProcAddr"); const char *ms = getenv("VKSHIM_MS"); if (ms) g_threshold_us = (uint64_t)atoll(ms) * 1000; g_logfd = open("/sdcard/Download/.gallery_vk.log", O_WRONLY | O_CREAT | O_APPEND, 0644); char b[256]; int n = snprintf(b, sizeof(b), "VKSHIM init ok pid=%d real=%p thr_us=%llu logfd=%d\n", (int)getpid(), g_real, (unsigned long long)g_threshold_us, g_logfd); if (g_logfd >= 0) write(g_logfd, b, (size_t)n); }