// extmem_probe -- MobileGL disaggregation P0 spike B (plan-B §8.3, §11 P0). // // Question this program answers, per device: // Can the memory behind AcquirePersistentMap be shared with another process // and mapped there -- for BOTH backends -- and by which route? // // T1 server exports its own allocation (VkExportMemoryAllocateInfo + // vkGetMemoryFdKHR, opaque-fd and dma-buf, handed over SCM_RIGHTS; the // importer tries plain mmap() *and* a Vulkan import + vkMapMemory) // T1-gles the same exported fd imported into GLES (GL_EXT_memory_object // + GL_EXT_memory_object_fd: glCreateMemoryObjectsEXT + glImportMemoryFdEXT // + glBufferStorageMemEXT + glMapBufferRange(PERSISTENT|COHERENT)), // first in-process, then cross-process. DirectGLES ("Espryt") reaches // AcquirePersistentMap through a GL mapping, not a VkDeviceMemory, so the // Vulkan-only T1 answer does not decide the tier for it. // T0 server imports a client allocation (AHardwareBuffer BLOB sent over a // unix socket, imported into VkDeviceMemory via // VK_ANDROID_external_memory_android_hardware_buffer and into a GL buffer // via EGL_ANDROID_get_native_client_buffer + glBufferStorageExternalEXT) // T3 server imports a client host mapping (VK_EXT_external_memory_host); // both directions: the process that imports allocates the memfd, and -- // the direction that actually makes T3 a tier -- the *client* allocates // the memfd and the *server* imports the client's host pointer. // // Every route that can reach OK also takes a real GPU access (vkCmdCopyBuffer // out of the shared allocation + vkCmdFillBuffer into it, queue-idle, host-read // barrier), so an OK verdict means the tier survives GPU use and not merely a // successful map call. // // Verdict rule (deliberately strict): a route is OK only when every decisive // leg round-tripped bytes in both directions, PARTIAL when at least one decisive // leg did, FAIL otherwise -- and a FAIL always names the failing step and its // driver error code. // // SELINUX CAVEAT: run from `adb shell`, this executes in the `shell` domain, not // the `untrusted_app` domain MobileGL actually runs in. See README.md; the // summary repeats it. // // Standalone: depends on nothing from MobileGL. Build with the NDK toolchain // (see CMakeLists.txt / build_android.sh), push to /data/local/tmp and run. // // Process topology mirrors the target design (client spawns the server as a // separate process): the probe re-execs /proc/self/exe with --child= and // hands it one end of a socketpair on fd 3. A plain fork() without exec is not // usable here -- the Vulkan driver's own threads and device state do not // survive fork, and both routes need live Vulkan on both sides. #ifdef __ANDROID__ # define VK_USE_PLATFORM_ANDROID_KHR 1 # define PROBE_HAVE_AHB 1 #else // The probe is an Android deliverable; the host build exists only so the // T1/T1-gles/T3 harness itself can be validated against a driver that is known // to implement those routes (lavapipe/llvmpipe), which is what makes a // device-side FAIL attributable to the driver rather than to this program. // T0 is Android-only by nature. # define PROBE_HAVE_AHB 0 #endif #include #include #include #include #include // T4 row names, outside the platform guard because BOTH branches report them: the // Android branch measures, the host branch says what it cannot measure. static const char* kT4Row = "T4-image-import"; static const char* kT4HostRow = "T4-host-image"; #if PROBE_HAVE_AHB # include # include #else # define PROP_VALUE_MAX 92 #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // bionic declares memfd_create only from API 30; the trace APK (MOBILEGL_BUILD_EXTMEM_PROBE) // builds at the project's API 26, where the syscall itself exists. #if defined(__ANDROID__) && __ANDROID_API__ < 30 # include static int memfd_create(const char* name, unsigned int flags) { return (int)syscall(__NR_memfd_create, name, flags); } #endif // The NDK's libvulkan stub exports Vulkan 1.1 entry points only from API 28; below that // (the API-26 trace APK) the two 1.1 calls the probe makes go through the instance. static VkInstance gVk11ShimInstance = VK_NULL_HANDLE; #if defined(__ANDROID__) && __ANDROID_API__ < 28 static void probeGetPhysicalDeviceProperties2(VkPhysicalDevice pd, VkPhysicalDeviceProperties2* out) { auto fn = (PFN_vkGetPhysicalDeviceProperties2)vkGetInstanceProcAddr(gVk11ShimInstance, "vkGetPhysicalDeviceProperties2"); if (fn) fn(pd, out); } static void probeGetPhysicalDeviceExternalBufferProperties(VkPhysicalDevice pd, const VkPhysicalDeviceExternalBufferInfo* info, VkExternalBufferProperties* out) { auto fn = (PFN_vkGetPhysicalDeviceExternalBufferProperties)vkGetInstanceProcAddr( gVk11ShimInstance, "vkGetPhysicalDeviceExternalBufferProperties"); if (fn) fn(pd, info, out); } # define vkGetPhysicalDeviceProperties2 probeGetPhysicalDeviceProperties2 # define vkGetPhysicalDeviceExternalBufferProperties probeGetPhysicalDeviceExternalBufferProperties #endif // --------------------------------------------------------------------------- // tiny logging / result table // --------------------------------------------------------------------------- static const char* gRole = "parent"; // ro.* on Android, empty elsewhere static void getProp(const char* name, char* out, size_t n) { out[0] = 0; #if PROBE_HAVE_AHB __system_property_get(name, out); #else (void)name; (void)n; #endif } static void pr(const char* fmt, ...) __attribute__((format(printf, 1, 2))); static void pr(const char* fmt, ...) { char buf[4096]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); fprintf(stdout, "[%s] %s\n", gRole, buf); fflush(stdout); } struct RouteResult { std::string route; std::string status; // OK / PARTIAL / UNSUPPORTED / FAIL / SKIP std::string detail; }; static std::vector gResults; static void record(const char* route, const char* status, const std::string& detail) { gResults.push_back(RouteResult{route, status, detail}); pr("RESULT %-34s %-12s %s", route, status, detail.c_str()); } static std::string fmt(const char* f, ...) __attribute__((format(printf, 1, 2))); static std::string fmt(const char* f, ...) { char buf[1024]; va_list ap; va_start(ap, f); vsnprintf(buf, sizeof(buf), f, ap); va_end(ap); return std::string(buf); } // Returns a fresh std::string per call: several vkStr() results routinely appear // in one format call, and a shared static buffer would make all of them show the // last one. static std::string vkStr(VkResult r) { switch (r) { case VK_SUCCESS: return "VK_SUCCESS"; case VK_NOT_READY: return "VK_NOT_READY"; case VK_TIMEOUT: return "VK_TIMEOUT"; case VK_INCOMPLETE: return "VK_INCOMPLETE"; case VK_ERROR_OUT_OF_HOST_MEMORY: return "VK_ERROR_OUT_OF_HOST_MEMORY"; case VK_ERROR_OUT_OF_DEVICE_MEMORY: return "VK_ERROR_OUT_OF_DEVICE_MEMORY"; case VK_ERROR_INITIALIZATION_FAILED: return "VK_ERROR_INITIALIZATION_FAILED"; case VK_ERROR_DEVICE_LOST: return "VK_ERROR_DEVICE_LOST"; case VK_ERROR_MEMORY_MAP_FAILED: return "VK_ERROR_MEMORY_MAP_FAILED"; case VK_ERROR_LAYER_NOT_PRESENT: return "VK_ERROR_LAYER_NOT_PRESENT"; case VK_ERROR_EXTENSION_NOT_PRESENT: return "VK_ERROR_EXTENSION_NOT_PRESENT"; case VK_ERROR_FEATURE_NOT_PRESENT: return "VK_ERROR_FEATURE_NOT_PRESENT"; case VK_ERROR_INCOMPATIBLE_DRIVER: return "VK_ERROR_INCOMPATIBLE_DRIVER"; case VK_ERROR_TOO_MANY_OBJECTS: return "VK_ERROR_TOO_MANY_OBJECTS"; case VK_ERROR_FORMAT_NOT_SUPPORTED: return "VK_ERROR_FORMAT_NOT_SUPPORTED"; case VK_ERROR_FRAGMENTED_POOL: return "VK_ERROR_FRAGMENTED_POOL"; case VK_ERROR_UNKNOWN: return "VK_ERROR_UNKNOWN"; case VK_ERROR_OUT_OF_POOL_MEMORY: return "VK_ERROR_OUT_OF_POOL_MEMORY"; case VK_ERROR_INVALID_EXTERNAL_HANDLE: return "VK_ERROR_INVALID_EXTERNAL_HANDLE"; case VK_ERROR_FRAGMENTATION: return "VK_ERROR_FRAGMENTATION"; case VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS: return "VK_ERROR_INVALID_OPAQUE_CAPTURE_ADDRESS"; default: return fmt("VkResult(%d)", (int)r); } } static std::string glErrStr(GLenum e) { switch (e) { case GL_NO_ERROR: return "GL_NO_ERROR"; case GL_INVALID_ENUM: return "GL_INVALID_ENUM"; case GL_INVALID_VALUE: return "GL_INVALID_VALUE"; case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION"; case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY"; case GL_INVALID_FRAMEBUFFER_OPERATION: return "GL_INVALID_FRAMEBUFFER_OPERATION"; default: return fmt("GL(0x%04x)", (unsigned)e); } } // drains and returns the last error, so one failing call cannot be blamed on the // previous one static GLenum glDrain() { GLenum last = GL_NO_ERROR, e; while ((e = glGetError()) != GL_NO_ERROR) last = e; return last; } static std::string readSmallFile(const char* path) { int fd = open(path, O_RDONLY); if (fd < 0) return fmt("<%s: errno=%d>", path, errno); char buf[256]; ssize_t n = read(fd, buf, sizeof(buf) - 1); close(fd); if (n <= 0) return ""; buf[n] = 0; while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == 0)) buf[--n] = 0; return std::string(buf); } // --------------------------------------------------------------------------- // payload patterns // --------------------------------------------------------------------------- static const uint64_t kRegion = 4096; // bytes per verification region static const uint64_t kRegionCount = 8; // A..F plus slack static const uint64_t kDefaultSize = 65536; // region indices inside the shared allocation enum { REG_A = 0, // first writer's payload (CPU, allocating side) REG_B = 1, // importer write through the plain host mapping (mmap / AHB lock / vk map) REG_C = 2, // importer write through the imported Vulkan mapping REG_D = 3, // importer write through the imported GL mapping (cross-process) REG_E = 4, // GPU write (vkCmdFillBuffer) REG_F = 5, // in-process GL-import write }; static void fillPattern(void* p, uint64_t bytes, uint32_t seed) { uint8_t* b = (uint8_t*)p; for (uint64_t i = 0; i < bytes; ++i) { b[i] = (uint8_t)((seed * 2654435761u + (uint32_t)i * 31u + (uint32_t)(i >> 8) * 7u) & 0xFF); } } // returns -1 on match, else the index of the first mismatching byte static int64_t checkPattern(const void* p, uint64_t bytes, uint32_t seed) { const uint8_t* b = (const uint8_t*)p; for (uint64_t i = 0; i < bytes; ++i) { uint8_t want = (uint8_t)((seed * 2654435761u + (uint32_t)i * 31u + (uint32_t)(i >> 8) * 7u) & 0xFF); if (b[i] != want) return (int64_t)i; } return -1; } // Byte offset of region 0 inside the shared allocation. 0 by default; --regions-at-end // moves the whole A..F window to the last kRegionCount*kRegion bytes, so a large --size // proves the far end of the allocation is mapped and GPU-reachable, not only its first // pages. The parent hands the value to every child with --region-base=. static uint64_t gRegionBase = 0; // --hold-ms=N (P11 B2 memory calibration): the T0S client keeps its AHB locked this long after // the last round, while the parent still holds its Vulkan and GL imports, so an outside sampler // can read what a held, imported AHB of --size bytes costs in each accounting. static uint32_t gHoldMs = 0; static uint64_t regOff(int region) { return gRegionBase + (uint64_t)region * kRegion; } static void writeRegion(void* base, int region, uint32_t seed) { fillPattern((uint8_t*)base + regOff(region), kRegion, seed); } static int64_t checkRegion(const void* base, int region, uint32_t seed) { return checkPattern((const uint8_t*)base + regOff(region), kRegion, seed); } // vkCmdFillBuffer writes a repeating 32-bit word; -1 on match, else the first // mismatching word index * 4 static int64_t checkFillWord(const void* base, int region, uint32_t word) { const uint32_t* w = (const uint32_t*)((const uint8_t*)base + regOff(region)); for (uint64_t i = 0; i < kRegion / 4; ++i) if (w[i] != word) return (int64_t)(i * 4); return -1; } // --------------------------------------------------------------------------- // verdict: decisive legs must round-trip bytes, in both directions // --------------------------------------------------------------------------- struct Leg { std::string name; bool decisive = false; // counted by the verdict; informational legs are not bool attempted = false; bool readOk = false; // the allocating side's bytes were visible to the other side bool writeOk = false; // the other side's bytes came back std::string fail; // failing step + driver error code }; static const char* legVerdict(const std::vector& legs, std::string* why) { int decisive = 0, round = 0; std::string bad; for (const Leg& l : legs) { if (!l.decisive) continue; ++decisive; if (l.attempted && l.readOk && l.writeOk) { ++round; } else { if (!bad.empty()) bad += "; "; bad += l.name + "=" + (l.fail.empty() ? std::string("no round trip") : l.fail); } } if (why) *why = bad; if (decisive == 0) return "SKIP"; if (round == decisive) return "OK"; if (round > 0) return "PARTIAL"; return "FAIL"; } // compact per-leg trace that stays in the summary line static std::string legTrace(const std::vector& legs) { std::string s; for (const Leg& l : legs) { if (!s.empty()) s += " "; const char* v = !l.attempted ? "notrun" : (l.readOk && l.writeOk) ? "rt" : l.readOk ? "read-only" : l.writeOk ? "write-only" : "no"; s += l.name + "[" + (l.decisive ? "D" : "i") + "]=" + v; } return s; } // --------------------------------------------------------------------------- // socket message plumbing // --------------------------------------------------------------------------- enum MsgTag : uint32_t { MSG_T1_OFFER = 1, MSG_T1_RESULT = 2, MSG_T0_REQUEST = 3, MSG_T0_ALLOC = 4, MSG_T0_VERIFY = 5, MSG_T0_RESULT = 6, MSG_T3_OFFER = 7, MSG_T3_RESULT = 8, MSG_T1GL_OFFER = 9, MSG_T1GL_RESULT = 10, MSG_T3C_REQUEST = 11, MSG_T3C_READY = 12, MSG_T3C_VERIFY = 13, MSG_T3C_RESULT = 14, MSG_T0S_REQUEST = 15, MSG_T0S_ALLOC = 16, MSG_T0S_ROUND = 17, MSG_T0S_GPUDONE = 18, MSG_T0S_CHECK = 19, MSG_T0S_DONE = 20, MSG_BYE = 99, }; struct MsgHeader { uint32_t tag; uint32_t len; }; static bool writeAll(int fd, const void* p, size_t n) { const uint8_t* b = (const uint8_t*)p; while (n) { ssize_t w = write(fd, b, n); if (w <= 0) { if (w < 0 && errno == EINTR) continue; return false; } b += w; n -= (size_t)w; } return true; } static bool readAll(int fd, void* p, size_t n) { uint8_t* b = (uint8_t*)p; while (n) { ssize_t r = read(fd, b, n); if (r <= 0) { if (r < 0 && errno == EINTR) continue; return false; } b += r; n -= (size_t)r; } return true; } // header + payload go out in one sendmsg so SCM_RIGHTS lands with the header byte static bool sendMsg(int sock, uint32_t tag, const void* payload, size_t len, int fdToPass) { MsgHeader h{tag, (uint32_t)len}; struct iovec iov[2]; iov[0].iov_base = &h; iov[0].iov_len = sizeof(h); iov[1].iov_base = (void*)payload; iov[1].iov_len = len; char cbuf[CMSG_SPACE(sizeof(int))]; memset(cbuf, 0, sizeof(cbuf)); struct msghdr msg; memset(&msg, 0, sizeof(msg)); msg.msg_iov = iov; msg.msg_iovlen = len ? 2 : 1; if (fdToPass >= 0) { msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); struct cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm->cmsg_level = SOL_SOCKET; cm->cmsg_type = SCM_RIGHTS; cm->cmsg_len = CMSG_LEN(sizeof(int)); memcpy(CMSG_DATA(cm), &fdToPass, sizeof(int)); } ssize_t s; do { s = sendmsg(sock, &msg, 0); } while (s < 0 && errno == EINTR); if (s < 0) return false; size_t total = sizeof(h) + len; if ((size_t)s == total) return true; // partial: finish the tail with plain writes (control data already delivered) size_t done = (size_t)s; if (done < sizeof(h)) return false; // should not happen for such small headers return writeAll(sock, (const uint8_t*)payload + (done - sizeof(h)), total - done); } static bool recvMsg(int sock, uint32_t* tag, void* payload, size_t maxLen, size_t* outLen, int* fdOut) { if (fdOut) *fdOut = -1; MsgHeader h{}; struct iovec iov; iov.iov_base = &h; iov.iov_len = sizeof(h); char cbuf[CMSG_SPACE(sizeof(int))]; memset(cbuf, 0, sizeof(cbuf)); struct msghdr msg; memset(&msg, 0, sizeof(msg)); msg.msg_iov = &iov; msg.msg_iovlen = 1; msg.msg_control = cbuf; msg.msg_controllen = sizeof(cbuf); ssize_t r; do { r = recvmsg(sock, &msg, MSG_WAITALL); } while (r < 0 && errno == EINTR); if (r != (ssize_t)sizeof(h)) return false; for (struct cmsghdr* cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SCM_RIGHTS) { int got = -1; memcpy(&got, CMSG_DATA(cm), sizeof(int)); if (fdOut) { *fdOut = got; } else if (got >= 0) { close(got); } } } *tag = h.tag; if (outLen) *outLen = h.len; if (h.len > maxLen) return false; if (h.len && !readAll(sock, payload, h.len)) return false; return true; } static void setRecvTimeout(int sock, int seconds) { struct timeval tv; tv.tv_sec = seconds; tv.tv_usec = 0; setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); } static std::string describeFd(int fd) { if (fd < 0) return "no-fd"; char path[64]; snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); char link[512]; ssize_t n = readlink(path, link, sizeof(link) - 1); std::string desc; if (n > 0) { link[n] = 0; desc = link; } else { desc = ""; } off_t sz = lseek(fd, 0, SEEK_END); if (sz >= 0) { desc += fmt(" size=%lld", (long long)sz); lseek(fd, 0, SEEK_SET); } else { desc += fmt(" lseek-errno=%d(%s)", errno, strerror(errno)); } struct stat st; if (fstat(fd, &st) == 0) { const char* kind = S_ISREG(st.st_mode) ? "reg" : S_ISCHR(st.st_mode) ? "chr" : S_ISFIFO(st.st_mode) ? "fifo" : S_ISSOCK(st.st_mode) ? "sock" : "other"; desc += fmt(" kind=%s stsize=%lld", kind, (long long)st.st_size); } return desc; } // --------------------------------------------------------------------------- // Vulkan context // --------------------------------------------------------------------------- struct VkCtx { VkInstance instance = VK_NULL_HANDLE; VkPhysicalDevice phys = VK_NULL_HANDLE; VkDevice device = VK_NULL_HANDLE; uint32_t queueFamily = 0; VkQueue queue = VK_NULL_HANDLE; VkCommandPool cmdPool = VK_NULL_HANDLE; VkPhysicalDeviceMemoryProperties memProps{}; VkPhysicalDeviceProperties props{}; uint8_t deviceUUID[VK_UUID_SIZE]{}; std::vector deviceExts; bool hasExtMemFd = false; bool hasDmaBuf = false; bool hasExtMemHost = false; bool hasAhb = false; bool hasQueueFamilyForeign = false; PFN_vkGetMemoryFdKHR pGetMemoryFdKHR = nullptr; PFN_vkGetMemoryFdPropertiesKHR pGetMemoryFdPropertiesKHR = nullptr; #if PROBE_HAVE_AHB PFN_vkGetAndroidHardwareBufferPropertiesANDROID pGetAhbProps = nullptr; #endif PFN_vkGetMemoryHostPointerPropertiesEXT pGetHostPtrProps = nullptr; VkDeviceSize minImportedHostPointerAlignment = 0; bool hasExt(const char* name) const { for (const std::string& s : deviceExts) if (s == name) return true; return false; } }; static bool vkCtxInit(VkCtx& c, bool verbose) { VkApplicationInfo app{}; app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; app.pApplicationName = "extmem_probe"; app.apiVersion = VK_API_VERSION_1_1; uint32_t instExtCount = 0; vkEnumerateInstanceExtensionProperties(nullptr, &instExtCount, nullptr); std::vector instExts(instExtCount); if (instExtCount) vkEnumerateInstanceExtensionProperties(nullptr, &instExtCount, instExts.data()); std::vector wanted; auto haveInst = [&](const char* n) { for (auto& e : instExts) if (!strcmp(e.extensionName, n)) return true; return false; }; if (haveInst(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME)) wanted.push_back(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME); if (haveInst(VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME)) wanted.push_back(VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME); VkInstanceCreateInfo ici{}; ici.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; ici.pApplicationInfo = &app; ici.enabledExtensionCount = (uint32_t)wanted.size(); ici.ppEnabledExtensionNames = wanted.empty() ? nullptr : wanted.data(); VkResult r = vkCreateInstance(&ici, nullptr, &c.instance); if (r != VK_SUCCESS) { pr("vkCreateInstance failed: %s", vkStr(r).c_str()); return false; } gVk11ShimInstance = c.instance; uint32_t n = 0; vkEnumeratePhysicalDevices(c.instance, &n, nullptr); if (!n) { pr("no physical devices"); return false; } std::vector devs(n); vkEnumeratePhysicalDevices(c.instance, &n, devs.data()); c.phys = devs[0]; VkPhysicalDeviceIDProperties idp{}; idp.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES; VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProps{}; hostProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT; idp.pNext = &hostProps; VkPhysicalDeviceProperties2 p2{}; p2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2; p2.pNext = &idp; vkGetPhysicalDeviceProperties2(c.phys, &p2); c.props = p2.properties; memcpy(c.deviceUUID, idp.deviceUUID, VK_UUID_SIZE); c.minImportedHostPointerAlignment = hostProps.minImportedHostPointerAlignment; vkGetPhysicalDeviceMemoryProperties(c.phys, &c.memProps); uint32_t extCount = 0; vkEnumerateDeviceExtensionProperties(c.phys, nullptr, &extCount, nullptr); std::vector exts(extCount); if (extCount) vkEnumerateDeviceExtensionProperties(c.phys, nullptr, &extCount, exts.data()); for (auto& e : exts) c.deviceExts.push_back(e.extensionName); c.hasExtMemFd = c.hasExt(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME); c.hasDmaBuf = c.hasExt(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME); c.hasExtMemHost = c.hasExt(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); c.hasAhb = c.hasExt("VK_ANDROID_external_memory_android_hardware_buffer"); c.hasQueueFamilyForeign = c.hasExt(VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME); uint32_t qf = 0; vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qf, nullptr); std::vector qfp(qf); vkGetPhysicalDeviceQueueFamilyProperties(c.phys, &qf, qfp.data()); c.queueFamily = 0; for (uint32_t i = 0; i < qf; ++i) { if (qfp[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { c.queueFamily = i; break; } } std::vector devExts; if (c.hasExt(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME)) devExts.push_back(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME); if (c.hasExtMemFd) devExts.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME); if (c.hasDmaBuf) devExts.push_back(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME); if (c.hasExtMemHost) devExts.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); if (c.hasAhb) { devExts.push_back("VK_ANDROID_external_memory_android_hardware_buffer"); if (c.hasQueueFamilyForeign) devExts.push_back(VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME); if (c.hasExt(VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME)) devExts.push_back(VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME); if (c.hasExt(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME)) devExts.push_back(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME); if (c.hasExt(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME)) devExts.push_back(VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME); if (c.hasExt(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME)) devExts.push_back(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME); if (c.hasExt(VK_KHR_MAINTENANCE_1_EXTENSION_NAME)) devExts.push_back(VK_KHR_MAINTENANCE_1_EXTENSION_NAME); } float prio = 1.0f; VkDeviceQueueCreateInfo q{}; q.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; q.queueFamilyIndex = c.queueFamily; q.queueCount = 1; q.pQueuePriorities = &prio; VkDeviceCreateInfo dci{}; dci.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; dci.queueCreateInfoCount = 1; dci.pQueueCreateInfos = &q; dci.enabledExtensionCount = (uint32_t)devExts.size(); dci.ppEnabledExtensionNames = devExts.empty() ? nullptr : devExts.data(); r = vkCreateDevice(c.phys, &dci, nullptr, &c.device); if (r != VK_SUCCESS) { pr("vkCreateDevice failed: %s", vkStr(r).c_str()); return false; } vkGetDeviceQueue(c.device, c.queueFamily, 0, &c.queue); VkCommandPoolCreateInfo cpi{}; cpi.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; cpi.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; cpi.queueFamilyIndex = c.queueFamily; VkResult pr_ = vkCreateCommandPool(c.device, &cpi, nullptr, &c.cmdPool); if (pr_ != VK_SUCCESS) { c.cmdPool = VK_NULL_HANDLE; pr("vkCreateCommandPool failed: %s (GPU touch will be skipped)", vkStr(pr_).c_str()); } c.pGetMemoryFdKHR = (PFN_vkGetMemoryFdKHR)vkGetDeviceProcAddr(c.device, "vkGetMemoryFdKHR"); c.pGetMemoryFdPropertiesKHR = (PFN_vkGetMemoryFdPropertiesKHR)vkGetDeviceProcAddr(c.device, "vkGetMemoryFdPropertiesKHR"); #if PROBE_HAVE_AHB c.pGetAhbProps = (PFN_vkGetAndroidHardwareBufferPropertiesANDROID)vkGetDeviceProcAddr( c.device, "vkGetAndroidHardwareBufferPropertiesANDROID"); #endif c.pGetHostPtrProps = (PFN_vkGetMemoryHostPointerPropertiesEXT)vkGetDeviceProcAddr( c.device, "vkGetMemoryHostPointerPropertiesEXT"); if (verbose) { pr("vulkan device: %s api=%u.%u.%u driverVersion=0x%08x vendor=0x%04x", c.props.deviceName, VK_VERSION_MAJOR(c.props.apiVersion), VK_VERSION_MINOR(c.props.apiVersion), VK_VERSION_PATCH(c.props.apiVersion), c.props.driverVersion, c.props.vendorID); char uuid[64] = {0}; for (uint32_t i = 0; i < VK_UUID_SIZE; ++i) snprintf(uuid + i * 2, 3, "%02x", c.deviceUUID[i]); pr("deviceUUID=%s minImportedHostPointerAlignment=%llu queueFamily=%u", uuid, (unsigned long long)c.minImportedHostPointerAlignment, c.queueFamily); } return true; } static void vkCtxDestroy(VkCtx& c) { if (c.cmdPool) vkDestroyCommandPool(c.device, c.cmdPool, nullptr); if (c.device) vkDestroyDevice(c.device, nullptr); if (c.instance) vkDestroyInstance(c.instance, nullptr); c.cmdPool = VK_NULL_HANDLE; c.device = VK_NULL_HANDLE; c.instance = VK_NULL_HANDLE; } // index of a memory type in `bits` that has all of `want`, or -1 static int pickMemType(const VkPhysicalDeviceMemoryProperties& mp, uint32_t bits, VkMemoryPropertyFlags want) { for (uint32_t i = 0; i < mp.memoryTypeCount; ++i) { if (!(bits & (1u << i))) continue; if ((mp.memoryTypes[i].propertyFlags & want) == want) return (int)i; } return -1; } static const VkBufferUsageFlags kProbeBufferUsage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; // --------------------------------------------------------------------------- // GPU touch: prove the shared allocation survives a real GPU access // // The GPU copies `readRegion` into a private staging buffer (so a mismatch means // the GPU could not read what the host/peer wrote) and fills `fillRegion` with a // known word (so the caller can check, through whichever mapping it is testing, // that a GPU write lands in the shared pages). Without this an OK verdict would // only prove that a map call returned a pointer. // --------------------------------------------------------------------------- struct GpuTouch { bool ran = false; VkResult submitResult = VK_NOT_READY; int64_t readMismatch = -3; // -1 match, -3 never ran std::string fail; }; static GpuTouch gpuTouch(VkCtx& c, VkBuffer buf, int readRegion, uint32_t readSeed, int fillRegion, uint32_t fillWord) { GpuTouch g; if (buf == VK_NULL_HANDLE || c.cmdPool == VK_NULL_HANDLE || c.queue == VK_NULL_HANDLE) { g.fail = "no buffer/queue/command pool for the GPU touch"; return g; } // private host-visible staging target for the read-back VkBufferCreateInfo sbi{}; sbi.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; sbi.size = kRegion; sbi.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT; VkBuffer staging = VK_NULL_HANDLE; VkResult r = vkCreateBuffer(c.device, &sbi, nullptr, &staging); if (r != VK_SUCCESS) { g.fail = "staging vkCreateBuffer=" + vkStr(r); return g; } VkMemoryRequirements sreq{}; vkGetBufferMemoryRequirements(c.device, staging, &sreq); int sType = pickMemType(c.memProps, sreq.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (sType < 0) { vkDestroyBuffer(c.device, staging, nullptr); g.fail = fmt("no HOST_VISIBLE|HOST_COHERENT staging type in bits=0x%x", sreq.memoryTypeBits); return g; } VkMemoryAllocateInfo smai{}; smai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; smai.allocationSize = sreq.size; smai.memoryTypeIndex = (uint32_t)sType; VkDeviceMemory smem = VK_NULL_HANDLE; r = vkAllocateMemory(c.device, &smai, nullptr, &smem); if (r != VK_SUCCESS) { vkDestroyBuffer(c.device, staging, nullptr); g.fail = "staging vkAllocateMemory=" + vkStr(r); return g; } vkBindBufferMemory(c.device, staging, smem, 0); VkCommandBufferAllocateInfo cai{}; cai.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; cai.commandPool = c.cmdPool; cai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cai.commandBufferCount = 1; VkCommandBuffer cmd = VK_NULL_HANDLE; r = vkAllocateCommandBuffers(c.device, &cai, &cmd); if (r != VK_SUCCESS) { vkFreeMemory(c.device, smem, nullptr); vkDestroyBuffer(c.device, staging, nullptr); g.fail = "vkAllocateCommandBuffers=" + vkStr(r); return g; } VkCommandBufferBeginInfo bi{}; bi.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmd, &bi); // host writes are made visible to the device by the queue submit itself for // HOST_COHERENT memory, but the shared allocation may be imported and // non-coherent, so ask for it explicitly VkMemoryBarrier pre{}; pre.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; pre.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT; pre.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &pre, 0, nullptr, 0, nullptr); VkBufferCopy copy{}; copy.srcOffset = (VkDeviceSize)regOff(readRegion); copy.dstOffset = 0; copy.size = kRegion; vkCmdCopyBuffer(cmd, buf, staging, 1, ©); vkCmdFillBuffer(cmd, buf, (VkDeviceSize)regOff(fillRegion), (VkDeviceSize)kRegion, fillWord); // device writes must be made visible to the host explicitly VkMemoryBarrier post{}; post.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER; post.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; post.dstAccessMask = VK_ACCESS_HOST_READ_BIT; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &post, 0, nullptr, 0, nullptr); vkEndCommandBuffer(cmd); VkSubmitInfo si{}; si.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; si.commandBufferCount = 1; si.pCommandBuffers = &cmd; g.submitResult = vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE); if (g.submitResult == VK_SUCCESS) { VkResult wr = vkQueueWaitIdle(c.queue); if (wr != VK_SUCCESS) { g.fail = "vkQueueWaitIdle=" + vkStr(wr); } else { void* sp = nullptr; VkResult mr = vkMapMemory(c.device, smem, 0, VK_WHOLE_SIZE, 0, &sp); if (mr == VK_SUCCESS && sp) { g.readMismatch = checkPattern(sp, kRegion, readSeed); vkUnmapMemory(c.device, smem); g.ran = true; if (g.readMismatch != -1) g.fail = fmt("GPU copy out of the shared allocation mismatched at byte %lld", (long long)g.readMismatch); } else { g.fail = "staging vkMapMemory=" + vkStr(mr); } } } else { g.fail = "vkQueueSubmit=" + vkStr(g.submitResult); } vkFreeCommandBuffers(c.device, c.cmdPool, 1, &cmd); vkFreeMemory(c.device, smem, nullptr); vkDestroyBuffer(c.device, staging, nullptr); return g; } // --------------------------------------------------------------------------- // exportable HOST_VISIBLE|HOST_COHERENT allocation + fd // --------------------------------------------------------------------------- struct ExportAlloc { VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void* host = nullptr; uint64_t allocationSize = 0; uint64_t bufferSize = 0; uint32_t memoryTypeIndex = 0; uint32_t memoryTypeBits = 0; bool dedicated = false; int fd = -1; bool advertisedExportable = false; bool advertisedImportable = false; VkResult bindResult = VK_SUCCESS; std::string fail; // empty on success }; // A failure on a handle type the driver advertised as EXPORTABLE is a driver // bug (FAIL); the same failure on one it never advertised is simply the route // not being there (UNSUPPORTED). One rule, used at every export failure site. static const char* exportFailStatus(const ExportAlloc& a) { return a.advertisedExportable ? "FAIL" : "UNSUPPORTED"; } static bool exportHostVisible(VkCtx& c, VkExternalMemoryHandleTypeFlagBits ht, uint64_t size, const char* tag, ExportAlloc& a) { a.bufferSize = size; VkPhysicalDeviceExternalBufferInfo ebi{}; ebi.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_BUFFER_INFO; ebi.usage = kProbeBufferUsage; ebi.handleType = ht; VkExternalBufferProperties ebp{}; ebp.sType = VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES; vkGetPhysicalDeviceExternalBufferProperties(c.phys, &ebi, &ebp); a.advertisedExportable = (ebp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) != 0; a.advertisedImportable = (ebp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0; a.dedicated = (ebp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0; pr("%s advertisedExportable=%d importable=%d dedicatedOnly=%d", tag, (int)a.advertisedExportable, (int)a.advertisedImportable, (int)a.dedicated); VkExternalMemoryBufferCreateInfo ext{}; ext.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; ext.handleTypes = ht; VkBufferCreateInfo bci{}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.pNext = &ext; bci.size = size; bci.usage = kProbeBufferUsage; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; VkResult r = vkCreateBuffer(c.device, &bci, nullptr, &a.buf); if (r != VK_SUCCESS) { a.buf = VK_NULL_HANDLE; a.fail = "vkCreateBuffer(external)=" + vkStr(r); return false; } VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(c.device, a.buf, &req); a.allocationSize = req.size; a.memoryTypeBits = req.memoryTypeBits; int typeIdx = pickMemType(c.memProps, req.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (typeIdx < 0) { a.fail = fmt("no HOST_VISIBLE|HOST_COHERENT memory type in bits=0x%x", req.memoryTypeBits); return false; } a.memoryTypeIndex = (uint32_t)typeIdx; pr("%s memReq size=%llu align=%llu typeBits=0x%x -> type %d", tag, (unsigned long long)req.size, (unsigned long long)req.alignment, req.memoryTypeBits, typeIdx); VkExportMemoryAllocateInfo exportInfo{}; exportInfo.sType = VK_STRUCTURE_TYPE_EXPORT_MEMORY_ALLOCATE_INFO; exportInfo.handleTypes = ht; VkMemoryDedicatedAllocateInfo ded{}; ded.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; ded.buffer = a.buf; if (a.dedicated) exportInfo.pNext = &ded; VkMemoryAllocateInfo mai{}; mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.pNext = &exportInfo; mai.allocationSize = req.size; mai.memoryTypeIndex = a.memoryTypeIndex; r = vkAllocateMemory(c.device, &mai, nullptr, &a.mem); if (r != VK_SUCCESS) { a.mem = VK_NULL_HANDLE; a.fail = fmt("vkAllocateMemory(export)=%s (advertisedExportable=%d)", vkStr(r).c_str(), (int)a.advertisedExportable); return false; } a.bindResult = vkBindBufferMemory(c.device, a.buf, a.mem, 0); if (a.bindResult != VK_SUCCESS) pr("%s vkBindBufferMemory=%s (continuing)", tag, vkStr(a.bindResult).c_str()); r = vkMapMemory(c.device, a.mem, 0, VK_WHOLE_SIZE, 0, &a.host); if (r != VK_SUCCESS || !a.host) { a.host = nullptr; a.fail = "exporter-side vkMapMemory=" + vkStr(r); return false; } VkMemoryGetFdInfoKHR gfi{}; gfi.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; gfi.memory = a.mem; gfi.handleType = ht; r = c.pGetMemoryFdKHR(c.device, &gfi, &a.fd); if (r != VK_SUCCESS || a.fd < 0) { a.fd = -1; a.fail = fmt("vkGetMemoryFdKHR=%s (advertisedExportable=%d)", vkStr(r).c_str(), (int)a.advertisedExportable); return false; } pr("%s exported fd=%d -> %s", tag, a.fd, describeFd(a.fd).c_str()); return true; } static void freeExportAlloc(VkCtx& c, ExportAlloc& a) { if (a.fd >= 0) close(a.fd); if (a.host) vkUnmapMemory(c.device, a.mem); if (a.mem) vkFreeMemory(c.device, a.mem, nullptr); if (a.buf) vkDestroyBuffer(c.device, a.buf, nullptr); a.fd = -1; a.host = nullptr; a.mem = VK_NULL_HANDLE; a.buf = VK_NULL_HANDLE; } // --------------------------------------------------------------------------- // GLES / EGL context // --------------------------------------------------------------------------- struct GlCtx { EGLDisplay dpy = EGL_NO_DISPLAY; EGLContext ctx = EGL_NO_CONTEXT; EGLSurface surf = EGL_NO_SURFACE; std::vector glExts; std::vector eglExts; std::string vendor, renderer, version; PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC pGetNativeClientBuffer = nullptr; PFNGLBUFFERSTORAGEEXTERNALEXTPROC pBufferStorageExternal = nullptr; // GL_EXT_memory_object / GL_EXT_memory_object_fd PFNGLCREATEMEMORYOBJECTSEXTPROC pCreateMemoryObjects = nullptr; PFNGLDELETEMEMORYOBJECTSEXTPROC pDeleteMemoryObjects = nullptr; PFNGLMEMORYOBJECTPARAMETERIVEXTPROC pMemoryObjectParameteriv = nullptr; PFNGLBUFFERSTORAGEMEMEXTPROC pBufferStorageMem = nullptr; PFNGLIMPORTMEMORYFDEXTPROC pImportMemoryFd = nullptr; PFNGLGETUNSIGNEDBYTEI_VEXTPROC pGetUnsignedBytei_v = nullptr; void (GL_APIENTRYP pMemoryBarrier)(GLbitfield) = nullptr; bool hasGl(const char* n) const { for (auto& s : glExts) if (s == n) return true; return false; } bool hasEgl(const char* n) const { for (auto& s : eglExts) if (s == n) return true; return false; } // everything the T1 GLES leg needs bool canImportFd() const { return hasGl("GL_EXT_memory_object") && hasGl("GL_EXT_memory_object_fd") && pCreateMemoryObjects && pImportMemoryFd && pBufferStorageMem; } std::string missingForImportFd() const { return fmt("GL_EXT_memory_object=%d GL_EXT_memory_object_fd=%d GL_EXT_buffer_storage=%d " "glCreateMemoryObjectsEXT=%d glImportMemoryFdEXT=%d glBufferStorageMemEXT=%d", (int)hasGl("GL_EXT_memory_object"), (int)hasGl("GL_EXT_memory_object_fd"), (int)hasGl("GL_EXT_buffer_storage"), (int)(pCreateMemoryObjects != nullptr), (int)(pImportMemoryFd != nullptr), (int)(pBufferStorageMem != nullptr)); } }; static void splitExts(const char* s, std::vector& out) { if (!s) return; std::string cur; for (const char* p = s; *p; ++p) { if (*p == ' ') { if (!cur.empty()) out.push_back(cur); cur.clear(); } else { cur.push_back(*p); } } if (!cur.empty()) out.push_back(cur); } static bool glCtxInit(GlCtx& g) { g.dpy = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (g.dpy == EGL_NO_DISPLAY) { pr("eglGetDisplay failed"); return false; } EGLint major = 0, minor = 0; if (!eglInitialize(g.dpy, &major, &minor)) { pr("eglInitialize failed 0x%04x", eglGetError()); return false; } pr("EGL %d.%d vendor=%s", major, minor, eglQueryString(g.dpy, EGL_VENDOR)); splitExts(eglQueryString(g.dpy, EGL_EXTENSIONS), g.eglExts); const char* clientExts = eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS); splitExts(clientExts, g.eglExts); const EGLint cfgAttr[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE}; EGLConfig cfg = nullptr; EGLint numCfg = 0; if (!eglChooseConfig(g.dpy, cfgAttr, &cfg, 1, &numCfg) || numCfg == 0) { pr("eglChooseConfig failed 0x%04x", eglGetError()); return false; } const EGLint pbAttr[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE}; g.surf = eglCreatePbufferSurface(g.dpy, cfg, pbAttr); if (g.surf == EGL_NO_SURFACE) { pr("eglCreatePbufferSurface failed 0x%04x", eglGetError()); return false; } eglBindAPI(EGL_OPENGL_ES_API); const EGLint versions[][2] = {{3, 2}, {3, 1}, {3, 0}}; for (auto& v : versions) { const EGLint ctxAttr[] = {EGL_CONTEXT_MAJOR_VERSION, v[0], EGL_CONTEXT_MINOR_VERSION, v[1], EGL_NONE}; g.ctx = eglCreateContext(g.dpy, cfg, EGL_NO_CONTEXT, ctxAttr); if (g.ctx != EGL_NO_CONTEXT) break; } if (g.ctx == EGL_NO_CONTEXT) { pr("eglCreateContext failed 0x%04x", eglGetError()); return false; } if (!eglMakeCurrent(g.dpy, g.surf, g.surf, g.ctx)) { pr("eglMakeCurrent failed 0x%04x", eglGetError()); return false; } const char* vd = (const char*)glGetString(GL_VENDOR); const char* rd = (const char*)glGetString(GL_RENDERER); const char* vr = (const char*)glGetString(GL_VERSION); g.vendor = vd ? vd : ""; g.renderer = rd ? rd : ""; g.version = vr ? vr : ""; GLint numExt = 0; glGetIntegerv(GL_NUM_EXTENSIONS, &numExt); for (GLint i = 0; i < numExt; ++i) { const char* e = (const char*)glGetStringi(GL_EXTENSIONS, (GLuint)i); if (e) g.glExts.push_back(e); } if (g.glExts.empty()) splitExts((const char*)glGetString(GL_EXTENSIONS), g.glExts); g.pGetNativeClientBuffer = (PFNEGLGETNATIVECLIENTBUFFERANDROIDPROC)eglGetProcAddress("eglGetNativeClientBufferANDROID"); g.pBufferStorageExternal = (PFNGLBUFFERSTORAGEEXTERNALEXTPROC)eglGetProcAddress("glBufferStorageExternalEXT"); g.pCreateMemoryObjects = (PFNGLCREATEMEMORYOBJECTSEXTPROC)eglGetProcAddress("glCreateMemoryObjectsEXT"); g.pDeleteMemoryObjects = (PFNGLDELETEMEMORYOBJECTSEXTPROC)eglGetProcAddress("glDeleteMemoryObjectsEXT"); g.pMemoryObjectParameteriv = (PFNGLMEMORYOBJECTPARAMETERIVEXTPROC)eglGetProcAddress("glMemoryObjectParameterivEXT"); g.pBufferStorageMem = (PFNGLBUFFERSTORAGEMEMEXTPROC)eglGetProcAddress("glBufferStorageMemEXT"); g.pImportMemoryFd = (PFNGLIMPORTMEMORYFDEXTPROC)eglGetProcAddress("glImportMemoryFdEXT"); g.pGetUnsignedBytei_v = (PFNGLGETUNSIGNEDBYTEI_VEXTPROC)eglGetProcAddress("glGetUnsignedBytei_vEXT"); // ES 3.1 core, but loaded dynamically so an ES 3.0 context still links g.pMemoryBarrier = (void(GL_APIENTRYP)(GLbitfield))eglGetProcAddress("glMemoryBarrier"); return true; } static void glCtxDestroy(GlCtx& g) { if (g.dpy != EGL_NO_DISPLAY) { eglMakeCurrent(g.dpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (g.ctx != EGL_NO_CONTEXT) eglDestroyContext(g.dpy, g.ctx); if (g.surf != EGL_NO_SURFACE) eglDestroySurface(g.dpy, g.surf); eglTerminate(g.dpy); } g.dpy = EGL_NO_DISPLAY; } // GL_DEVICE_UUID_EXT must equal the Vulkan deviceUUID for an fd import to be // legal; a mismatch is the usual reason glImportMemoryFdEXT declines. static std::string glDeviceUuidReport(GlCtx& g, const uint8_t* vkUuid, bool* matched) { if (matched) *matched = false; if (!g.hasGl("GL_EXT_memory_object") || !g.pGetUnsignedBytei_v) return "unavailable"; GLint n = 0; glDrain(); glGetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, &n); if (glDrain() != GL_NO_ERROR || n <= 0) return "GL_NUM_DEVICE_UUIDS_EXT unreadable"; std::string out; for (GLint i = 0; i < n; ++i) { GLubyte uuid[GL_UUID_SIZE_EXT] = {0}; g.pGetUnsignedBytei_v(GL_DEVICE_UUID_EXT, (GLuint)i, uuid); char hex[2 * GL_UUID_SIZE_EXT + 1] = {0}; for (int k = 0; k < GL_UUID_SIZE_EXT; ++k) snprintf(hex + k * 2, 3, "%02x", uuid[k]); if (!out.empty()) out += ","; out += hex; if (!memcmp(uuid, vkUuid, GL_UUID_SIZE_EXT) && matched) *matched = true; } return out; } // --------------------------------------------------------------------------- // GL side of T1: import an exported fd as GL buffer storage and map it // --------------------------------------------------------------------------- struct GlImport { bool memObjOk = false; bool storageOk = false; bool mapOk = false; bool persistentCoherent = false; // the PERSISTENT|COHERENT map is what AcquirePersistentMap needs GLenum errImport = GL_NO_ERROR; GLenum errStorage = GL_NO_ERROR; GLenum errMap = GL_NO_ERROR; // error of the PERSISTENT|COHERENT attempt GLenum errMap2 = GL_NO_ERROR; // error of the plain MAP_READ|MAP_WRITE fallback GLuint memObj = 0; GLuint buf = 0; void* ptr = nullptr; uint64_t mappedSize = 0; std::string variant; // which phrasing the driver accepted std::string ladder; // every phrasing tried, with its error std::string fail; }; // Borrows `fd` (dups it per attempt; the caller keeps ownership). // // Drivers disagree about how this call has to be phrased -- whether the memory // object must be flagged dedicated, and whether the buffer may be smaller than // the imported allocation -- and a probe that tried only one phrasing would // report a driver preference as a missing capability. So walk the ladder and // report which rung the driver accepted. static void glImportFdBuffer(GlCtx& g, int fd, uint64_t allocationSize, uint64_t bufferSize, bool dedicatedHint, GlImport& o) { struct Attempt { bool dedicated; uint64_t importSize; uint64_t storageSize; }; std::vector attempts; // some drivers validate the imported size against the fd's own size rather // than against the exporter's VkMemoryRequirements::size off_t fdSize = lseek(fd, 0, SEEK_END); if (fdSize > 0) lseek(fd, 0, SEEK_SET); std::vector importSizes{allocationSize}; if (fdSize > 0 && (uint64_t)fdSize != allocationSize) importSizes.push_back((uint64_t)fdSize); for (uint64_t imp : importSizes) { for (bool ded : {dedicatedHint, !dedicatedHint}) { attempts.push_back(Attempt{ded, imp, bufferSize}); if (imp != bufferSize) attempts.push_back(Attempt{ded, imp, imp}); } } glGenBuffers(1, &o.buf); for (const Attempt& at : attempts) { std::string tag = fmt("[ded=%d imp=%llu store=%llu]", (int)at.dedicated, (unsigned long long)at.importSize, (unsigned long long)at.storageSize); glDrain(); GLuint mo = 0; g.pCreateMemoryObjects(1, &mo); if (at.dedicated && g.pMemoryObjectParameteriv) { GLint yes = GL_TRUE; g.pMemoryObjectParameteriv(mo, GL_DEDICATED_MEMORY_OBJECT_EXT, &yes); glDrain(); } int dupFd = dup(fd); g.pImportMemoryFd(mo, (GLuint64)at.importSize, GL_HANDLE_TYPE_OPAQUE_FD_EXT, (GLint)dupFd); GLenum eImport = glDrain(); if (eImport != GL_NO_ERROR) { // Deliberately NOT closed: EXT_memory_object_fd transfers ownership of // the fd to the implementation and does not say whether that still // happens when the import fails, and Mesa closes it either way. A // double close would land on whatever fd the allocator handed out // next -- the socket, in this program. At most a handful of rungs // run, so leaking the dup is the cheap, safe side of that trade. (void)dupFd; if (g.pDeleteMemoryObjects) g.pDeleteMemoryObjects(1, &mo); glDrain(); if (!o.memObjOk) o.errImport = eImport; o.ladder += tag + "import=" + glErrStr(eImport) + " "; continue; } o.memObjOk = true; o.errImport = GL_NO_ERROR; glBindBuffer(GL_ARRAY_BUFFER, o.buf); glDrain(); g.pBufferStorageMem(GL_ARRAY_BUFFER, (GLsizeiptr)at.storageSize, mo, 0); GLenum eStorage = glDrain(); o.ladder += tag + "storage=" + glErrStr(eStorage) + " "; if (eStorage != GL_NO_ERROR) { o.errStorage = eStorage; if (g.pDeleteMemoryObjects) g.pDeleteMemoryObjects(1, &mo); glDrain(); // storage is immutable once it takes, so a failed attempt needs a // fresh buffer name before the next rung glBindBuffer(GL_ARRAY_BUFFER, 0); glDeleteBuffers(1, &o.buf); glGenBuffers(1, &o.buf); continue; } o.memObj = mo; o.errStorage = GL_NO_ERROR; o.storageOk = true; o.mappedSize = at.storageSize; o.variant = tag; break; } if (!o.memObjOk) { o.fail = "glImportMemoryFdEXT -> " + glErrStr(o.errImport) + " (ladder: " + o.ladder + ")"; return; } if (!o.storageOk) { o.fail = "glBufferStorageMemEXT -> " + glErrStr(o.errStorage) + " (ladder: " + o.ladder + ")"; return; } bufferSize = o.mappedSize; o.ptr = glMapBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)bufferSize, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT); o.errMap = glDrain(); if (o.ptr) { o.mapOk = true; o.persistentCoherent = true; return; } // A driver may back the storage but refuse the persistent/coherent flags -- // that is exactly the T1/T2 distinction for DirectGLES, so it is reported // separately rather than folded into one failure. o.ptr = glMapBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)bufferSize, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); o.errMap2 = glDrain(); if (o.ptr) { o.mapOk = true; o.fail = "PERSISTENT|COHERENT map refused (" + glErrStr(o.errMap) + "), only a scoped map works"; } else { o.fail = "glMapBufferRange persistent -> " + glErrStr(o.errMap) + ", plain -> " + glErrStr(o.errMap2); } } static void glImportPublish(GlCtx& g, GlImport& o) { if (!o.mapOk) return; if (o.persistentCoherent) { if (g.pMemoryBarrier) g.pMemoryBarrier(GL_ALL_BARRIER_BITS); } else { glUnmapBuffer(GL_ARRAY_BUFFER); o.ptr = nullptr; } glFinish(); } static void glImportRelease(GlCtx& g, GlImport& o) { if (o.buf) { glBindBuffer(GL_ARRAY_BUFFER, o.buf); if (o.ptr) glUnmapBuffer(GL_ARRAY_BUFFER); glBindBuffer(GL_ARRAY_BUFFER, 0); glDeleteBuffers(1, &o.buf); } if (o.memObj && g.pDeleteMemoryObjects) g.pDeleteMemoryObjects(1, &o.memObj); glDrain(); o.buf = 0; o.memObj = 0; o.ptr = nullptr; } // --------------------------------------------------------------------------- // child spawn // --------------------------------------------------------------------------- // Spawns /proc/self/exe --child=; the child gets `sock` on fd 3. static pid_t spawnChild(const char* route, int* parentSock) { int sv[2]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) { pr("socketpair failed errno=%d", errno); return -1; } pid_t pid = fork(); if (pid < 0) { pr("fork failed errno=%d", errno); close(sv[0]); close(sv[1]); return -1; } if (pid == 0) { close(sv[0]); if (sv[1] != 3) { dup2(sv[1], 3); close(sv[1]); } char arg[64]; snprintf(arg, sizeof(arg), "--child=%s", route); char base[64]; snprintf(base, sizeof(base), "--region-base=%llu", (unsigned long long)gRegionBase); char self[512]; ssize_t n = readlink("/proc/self/exe", self, sizeof(self) - 1); if (n <= 0) _exit(90); self[n] = 0; char hold[64]; snprintf(hold, sizeof(hold), "--hold-ms=%u", gHoldMs); char* argv[] = {self, arg, base, hold, nullptr}; execv(self, argv); _exit(91); } close(sv[1]); *parentSock = sv[0]; setRecvTimeout(sv[0], 60); return pid; } // Bounded: a child wedged inside a driver call must not hold the probe (and the // device) forever. Poll for a few seconds, then kill it and report that. static std::string reapChild(pid_t pid) { int status = 0; bool killed = false; for (int i = 0; i < 100; ++i) { pid_t w = waitpid(pid, &status, WNOHANG); if (w == pid) { if (WIFEXITED(status)) return fmt("child exit=%d%s", WEXITSTATUS(status), killed ? " (killed)" : ""); if (WIFSIGNALED(status)) return fmt("child signal=%d%s", WTERMSIG(status), killed ? " (killed)" : ""); return "child ?"; } if (w < 0) return fmt("waitpid errno=%d", errno); if (i == 60 && !killed) { kill(pid, SIGKILL); killed = true; } usleep(50000); } kill(pid, SIGKILL); if (waitpid(pid, &status, 0) < 0) return fmt("child unreaped, waitpid errno=%d", errno); return fmt("child killed after hang (signal=%d)", WIFSIGNALED(status) ? WTERMSIG(status) : 0); } // --------------------------------------------------------------------------- // wire payloads // --------------------------------------------------------------------------- struct T1Offer { uint64_t allocationSize; uint64_t bufferSize; uint32_t handleType; // VkExternalMemoryHandleTypeFlagBits used to export uint32_t seedA; // pattern the parent wrote in REG_A uint32_t seedB; // pattern the child must write in REG_B (via mmap) uint32_t seedC; // pattern the child must write in REG_C (via imported vkMapMemory) uint32_t memoryTypeIndex; uint32_t memoryTypeBits; uint32_t gpuWord; // word the GPU filled REG_E with uint32_t gpuRan; // 0 -> REG_E carries nothing, do not check it }; struct T1Result { int32_t gotFd; int32_t mmapOk; int32_t mmapErrno; int64_t mmapMismatch; // -1 == data matched int64_t mmapPatternOffset; // where the exporter's payload really starts in the mapping, -1 = not found int64_t mmapGpuMismatch; // REG_E through the plain mapping (-2 = not checked) int32_t vkInitOk; int32_t fdPropsResult; // VkResult of vkGetMemoryFdPropertiesKHR uint32_t fdMemoryTypeBits; int32_t importResult; // VkResult of vkAllocateMemory with the import struct int32_t bindResult; int32_t mapResult; int64_t vkMismatch; // -1 == data matched int64_t vkGpuMismatch; // REG_E through the imported mapping (-2 = not checked) int32_t wroteB; int32_t wroteC; char note[384]; }; struct T1GlOffer { uint64_t allocationSize; uint64_t bufferSize; uint32_t seedA; uint32_t seedD; // the child writes REG_D through the imported GL mapping uint32_t gpuWord; uint32_t gpuRan; uint32_t dedicated; }; struct T1GlResult { int32_t glInitOk; int32_t haveExts; int32_t memObjOk; int32_t storageOk; int32_t mapOk; int32_t persistentCoherent; uint32_t errImport, errStorage, errMap, errMap2; int64_t mismatchA; int64_t mismatchGpu; int32_t wroteD; char note[640]; }; struct T0Request { uint64_t size; uint32_t seedA; // pattern the child writes through AHardwareBuffer_lock }; struct T0Alloc { int32_t allocOk; int32_t allocErr; uint64_t size; uint32_t stride; char note[192]; }; struct T0Verify { uint32_t seedB; // parent wrote REG_B through the imported vkMapMemory uint32_t seedC; // parent wrote REG_C through the imported GL mapping uint32_t seedD; // parent wrote REG_D through AHardwareBuffer_lock uint32_t gpuWord; // the GPU filled REG_E with this uint32_t writtenMask; // bit0=B bit1=C bit2=D bit3=E(gpu) }; struct T0Result { int32_t lockOk; int32_t lockErr; int64_t mismatchB; int64_t mismatchC; int64_t mismatchD; int64_t mismatchE; char note[192]; }; // T0S (sustained lock): the client locks the AHB once and never unlocks it while the // server's GPU reads what the client's CPU wrote and writes back, round after round. struct T0SRequest { uint64_t size; uint32_t rounds; uint32_t seedBase; }; struct T0SRound { uint32_t round; uint32_t seedA; // the client CPU wrote REG_A with this pattern through its held lock }; struct T0SGpuDone { uint32_t round; uint32_t vkWord; // the server's Vulkan GPU filled REG_E with this word uint32_t glWord; // the server's GL compute dispatch filled REG_F with this word uint32_t mask; // bit0 = vk fill ran, bit1 = gl fill ran }; struct T0SCheck { uint32_t round; int32_t pad; int64_t mismatchE; // through the client's still-held lock pointer, -2 = not checked int64_t mismatchF; }; struct T0SDone { int32_t lockRc; int32_t unlockRc; int32_t relockRc; int32_t pad; int64_t relockMismatchE; // after unlock + a fresh lock: the last GPU writes, -2 = not checked int64_t relockMismatchF; char note[160]; }; struct T3Offer { uint64_t size; uint32_t seedA; uint32_t seedB; uint32_t gpuWord; uint32_t gpuRan; }; struct T3Result { int32_t mmapOk; int32_t mmapErrno; int64_t mismatch; int64_t gpuMismatch; char note[192]; }; // T3 in the direction that makes it a tier: the CLIENT allocates, the SERVER // imports the client's host pointer. struct T3cRequest { uint64_t size; // must be a multiple of minImportedHostPointerAlignment uint32_t seedA; // the child writes REG_A }; struct T3cReady { int32_t ok; int32_t err; uint64_t size; char note[160]; }; struct T3cVerify { uint32_t seedB; // the parent wrote REG_B through the imported VkDeviceMemory uint32_t gpuWord; // the parent's GPU filled REG_E uint32_t mask; // bit0=B bit1=E }; struct T3cResult { int64_t mismatchB; int64_t mismatchE; char note[160]; }; // --------------------------------------------------------------------------- // Phase A: enumeration // --------------------------------------------------------------------------- static std::string memFlagStr(VkMemoryPropertyFlags f) { std::string s; if (f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) s += "DEVICE_LOCAL "; if (f & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) s += "HOST_VISIBLE "; if (f & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) s += "HOST_COHERENT "; if (f & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) s += "HOST_CACHED "; if (f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) s += "LAZY "; if (f & VK_MEMORY_PROPERTY_PROTECTED_BIT) s += "PROTECTED "; return s; } // The whole point of this probe is what the driver does in the process MobileGL // runs in. `adb shell` is not that process: it is the `shell` SELinux domain, // which has access to device nodes and ashmem/dmabuf rules that `untrusted_app` // does not necessarily share. Print the domain we actually got so a later app // run can be compared against it. static const char* kDomainCaveat = "run context is `adb shell` (SELinux domain u:r:shell:s0), NOT the untrusted_app " "domain MobileGL runs in; per-domain SELinux rules can reject a route that works here"; static void printRunContext() { std::string sec = readSmallFile("/proc/self/attr/current"); pr("=== run context ==="); pr("uid=%d gid=%d pid=%d selinux=%s", (int)getuid(), (int)getgid(), (int)getpid(), sec.c_str()); pr("CAVEAT: %s", kDomainCaveat); pr(" to answer the question for the real domain, run this binary from the app " "process (spike A's trace-app hook) rather than from adb shell -- see README.md"); } static void reportExternalBufferCaps(VkCtx& c, VkExternalMemoryHandleTypeFlagBits ht, const char* name) { VkPhysicalDeviceExternalBufferInfo info{}; info.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_BUFFER_INFO; info.usage = kProbeBufferUsage; info.handleType = ht; VkExternalBufferProperties out{}; out.sType = VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES; vkGetPhysicalDeviceExternalBufferProperties(c.phys, &info, &out); const VkExternalMemoryProperties& p = out.externalMemoryProperties; std::string feat; if (p.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) feat += "DEDICATED_ONLY "; if (p.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) feat += "EXPORTABLE "; if (p.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) feat += "IMPORTABLE "; if (feat.empty()) feat = ""; pr(" externalBuffer[%s]: features=%s exportFrom=0x%x compatible=0x%x", name, feat.c_str(), p.exportFromImportedHandleTypes, p.compatibleHandleTypes); } static void phaseEnumerate(VkCtx& c, GlCtx& g, bool glOk) { pr("=== phase A: capability enumeration ==="); char model[PROP_VALUE_MAX] = {0}, dev[PROP_VALUE_MAX] = {0}, rel[PROP_VALUE_MAX] = {0}; getProp("ro.product.model", model, sizeof(model)); getProp("ro.product.device", dev, sizeof(dev)); getProp("ro.build.version.release", rel, sizeof(rel)); pr("device: model=%s device=%s android=%s", model, dev, rel); pr("vulkan: %s (api %u.%u.%u, driver 0x%08x, vendor 0x%04x)", c.props.deviceName, VK_VERSION_MAJOR(c.props.apiVersion), VK_VERSION_MINOR(c.props.apiVersion), VK_VERSION_PATCH(c.props.apiVersion), c.props.driverVersion, c.props.vendorID); struct { const char* name; bool present; } probe[] = { {VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME, c.hasExt(VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME)}, {VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME, c.hasExtMemFd}, {VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME, c.hasDmaBuf}, {VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME, c.hasExtMemHost}, {"VK_ANDROID_external_memory_android_hardware_buffer", c.hasAhb}, {VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, c.hasQueueFamilyForeign}, {VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, c.hasExt(VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME)}, }; for (auto& e : probe) pr(" VK ext %-58s %s", e.name, e.present ? "YES" : "no"); pr("memory types (%u):", c.memProps.memoryTypeCount); for (uint32_t i = 0; i < c.memProps.memoryTypeCount; ++i) { const VkMemoryType& mt = c.memProps.memoryTypes[i]; pr(" [%u] heap=%u size=%lluMiB flags=%s", i, mt.heapIndex, (unsigned long long)(c.memProps.memoryHeaps[mt.heapIndex].size >> 20), memFlagStr(mt.propertyFlags).c_str()); } // Only query handle types the driver actually claims: a handle type whose // extension is absent is not required to be understood by this entry point. reportExternalBufferCaps(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT, "OPAQUE_FD"); if (c.hasDmaBuf) reportExternalBufferCaps(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, "DMA_BUF"); if (c.hasExtMemHost) reportExternalBufferCaps(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, "HOST_ALLOCATION"); if (c.hasAhb) reportExternalBufferCaps(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID, "AHARDWAREBUFFER"); if (!glOk) { pr("gles: context unavailable, GL extension probe skipped"); record("A-gles-context", "FAIL", "no headless EGL context; every GLES leg is unanswered"); return; } pr("gles: vendor=%s renderer=%s version=%s", g.vendor.c_str(), g.renderer.c_str(), g.version.c_str()); const char* glWanted[] = { "GL_EXT_memory_object", "GL_EXT_memory_object_fd", "GL_EXT_external_buffer", "GL_EXT_buffer_storage", "GL_OES_EGL_image", "GL_OES_EGL_image_external", "GL_OES_EGL_image_external_essl3", "GL_EXT_memory_object_win32", }; for (const char* n : glWanted) pr(" GL ext %-40s %s", n, g.hasGl(n) ? "YES" : "no"); const char* eglWanted[] = { "EGL_ANDROID_get_native_client_buffer", "EGL_KHR_image_base", "EGL_ANDROID_image_native_buffer", "EGL_EXT_image_dma_buf_import", "EGL_KHR_gl_texture_2D_image", }; for (const char* n : eglWanted) pr(" EGL ext %-40s %s", n, g.hasEgl(n) ? "YES" : "no"); pr(" eglGetNativeClientBufferANDROID=%p glBufferStorageExternalEXT=%p", (void*)g.pGetNativeClientBuffer, (void*)g.pBufferStorageExternal); pr(" glCreateMemoryObjectsEXT=%p glImportMemoryFdEXT=%p glBufferStorageMemEXT=%p glMemoryObjectParameterivEXT=%p", (void*)g.pCreateMemoryObjects, (void*)g.pImportMemoryFd, (void*)g.pBufferStorageMem, (void*)g.pMemoryObjectParameteriv); bool uuidMatch = false; std::string glUuid = glDeviceUuidReport(g, c.deviceUUID, &uuidMatch); char vkUuid[2 * VK_UUID_SIZE + 1] = {0}; for (uint32_t i = 0; i < VK_UUID_SIZE; ++i) snprintf(vkUuid + i * 2, 3, "%02x", c.deviceUUID[i]); pr(" GL_DEVICE_UUID_EXT=%s vkDeviceUUID=%s match=%d", glUuid.c_str(), vkUuid, (int)uuidMatch); } // --------------------------------------------------------------------------- // T1 parent: server exports its own allocation // --------------------------------------------------------------------------- static void runT1Parent(VkCtx& c, VkExternalMemoryHandleTypeFlagBits handleType, const char* routeName, uint64_t size) { if (!c.hasExtMemFd || !c.pGetMemoryFdKHR) { record(routeName, "UNSUPPORTED", "VK_KHR_external_memory_fd absent"); return; } if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT && !c.hasDmaBuf) { record(routeName, "UNSUPPORTED", "VK_EXT_external_memory_dma_buf absent"); return; } ExportAlloc a; std::string tag = fmt("T1[%s]", routeName); if (!exportHostVisible(c, handleType, size, tag.c_str(), a)) { // one rule for every export-path failure, advertised or not record(routeName, exportFailStatus(a), a.fail); freeExportAlloc(c, a); return; } const uint32_t seedA = 0xA5A50001u, seedB = 0xB0B00002u, seedC = 0xC0C00003u; const uint32_t gpuWord = 0x5EED1234u; memset(a.host, 0, (size_t)size); writeRegion(a.host, REG_A, seedA); // real GPU access on the shared allocation, before the handover: the GPU // reads REG_A (host-written) and writes REG_E, which the importer then has // to see through its own mapping. GpuTouch gt = gpuTouch(c, a.buf, REG_A, seedA, REG_E, gpuWord); int64_t gpuFillSeenHere = gt.ran ? checkFillWord(a.host, REG_E, gpuWord) : -3; pr("%s gpuTouch ran=%d submit=%s readMismatch=%lld fillSeenByExporter=%lld %s", tag.c_str(), (int)gt.ran, vkStr(gt.submitResult).c_str(), (long long)gt.readMismatch, (long long)gpuFillSeenHere, gt.fail.c_str()); int sock = -1; pid_t pid = spawnChild("t1", &sock); if (pid < 0) { record(routeName, "FAIL", "spawnChild failed"); freeExportAlloc(c, a); return; } T1Offer offer{}; offer.allocationSize = a.allocationSize; offer.bufferSize = size; offer.handleType = (uint32_t)handleType; offer.seedA = seedA; offer.seedB = seedB; offer.seedC = seedC; offer.memoryTypeIndex = a.memoryTypeIndex; offer.memoryTypeBits = a.memoryTypeBits; offer.gpuWord = gpuWord; offer.gpuRan = (gt.ran && gpuFillSeenHere == -1) ? 1u : 0u; // For OPAQUE_FD the raw mmap leg is informational only: the Vulkan spec // explicitly forbids interpreting an opaque fd payload outside the driver, // so a driver that refuses it is conformant and MobileGL would never take // that route. For DMA_BUF a CPU mapping is the point of the handle type, // so there it is decisive. const bool mmapDecisive = (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT); std::string detail; const char* status = "FAIL"; if (!sendMsg(sock, MSG_T1_OFFER, &offer, sizeof(offer), a.fd)) { detail = fmt("sendMsg(offer) errno=%d", errno); record(routeName, "FAIL", detail); sendMsg(sock, MSG_BYE, nullptr, 0, -1); reapChild(pid); close(sock); freeExportAlloc(c, a); return; } close(a.fd); a.fd = -1; uint32_t tag2 = 0; T1Result res{}; size_t got = 0; if (!recvMsg(sock, &tag2, &res, sizeof(res), &got, nullptr) || tag2 != MSG_T1_RESULT || got != sizeof(res)) { detail = fmt("no T1 result from child (errno=%d, %s)", errno, reapChild(pid).c_str()); record(routeName, "FAIL", detail); close(sock); freeExportAlloc(c, a); return; } // The child wrote REG_B (mmap) and REG_C (imported vkMapMemory); check that // the writes are visible through the *server's* own mapping. int64_t backB = res.wroteB ? checkRegion(a.host, REG_B, seedB) : -2; int64_t backC = res.wroteC ? checkRegion(a.host, REG_C, seedC) : -2; std::vector legs; { Leg l; l.name = "rawmmap"; l.decisive = mmapDecisive; l.attempted = res.mmapOk != 0; l.readOk = res.mmapOk && res.mmapMismatch == -1 && (!offer.gpuRan || res.mmapGpuMismatch == -1); l.writeOk = res.wroteB && backB == -1; if (!l.attempted) l.fail = fmt("mmap failed errno=%d(%s)", res.mmapErrno, strerror(res.mmapErrno)); else if (!l.readOk) l.fail = fmt("exporter payload not at offset 0 (cmp=%lld payloadAt=%lld gpuCmp=%lld)", (long long)res.mmapMismatch, (long long)res.mmapPatternOffset, (long long)res.mmapGpuMismatch); else if (!l.writeOk) l.fail = fmt("importer write not visible to exporter (back=%lld)", (long long)backB); legs.push_back(l); } { Leg l; l.name = "vkimport"; l.decisive = true; l.attempted = res.importResult == VK_SUCCESS; l.readOk = res.importResult == VK_SUCCESS && res.mapResult == VK_SUCCESS && res.vkMismatch == -1 && (!offer.gpuRan || res.vkGpuMismatch == -1); l.writeOk = res.wroteC && backC == -1; if (!res.vkInitOk) l.fail = "child Vulkan init failed"; else if (res.importResult != VK_SUCCESS) l.fail = "vkAllocateMemory(import)=" + vkStr((VkResult)res.importResult); else if (res.mapResult != VK_SUCCESS) l.fail = "importer vkMapMemory=" + vkStr((VkResult)res.mapResult); else if (!l.readOk) l.fail = fmt("payload mismatch cmp=%lld gpuCmp=%lld", (long long)res.vkMismatch, (long long)res.vkGpuMismatch); else if (!l.writeOk) l.fail = fmt("importer write not visible to exporter (back=%lld)", (long long)backC); legs.push_back(l); } { Leg l; l.name = "gpu"; l.decisive = true; l.attempted = gt.ran; l.readOk = gt.readMismatch == -1; l.writeOk = gpuFillSeenHere == -1; if (!gt.ran) l.fail = "GPU touch did not run: " + gt.fail; else if (!l.readOk) l.fail = fmt("GPU read of the shared allocation mismatched at %lld", (long long)gt.readMismatch); else if (!l.writeOk) l.fail = fmt("GPU write not visible through the exporter's map (at %lld)", (long long)gpuFillSeenHere); legs.push_back(l); } std::string why; status = legVerdict(legs, &why); detail = fmt( "%s | mmap=%s(errno=%d,cmp=%lld,payloadAt=%lld,gpu=%lld,back=%lld) vkimport=%s(fdProps=%s bits=0x%x " "bind=%s map=%s cmp=%lld gpu=%lld back=%lld) gpuTouch(submit=%s read=%lld fill=%lld) %s%s%s", legTrace(legs).c_str(), res.mmapOk ? (res.mmapOk == 2 ? "ok-buffersize" : "ok") : "fail", res.mmapErrno, (long long)res.mmapMismatch, (long long)res.mmapPatternOffset, (long long)res.mmapGpuMismatch, (long long)backB, res.importResult == VK_SUCCESS ? "ok" : vkStr((VkResult)res.importResult).c_str(), vkStr((VkResult)res.fdPropsResult).c_str(), res.fdMemoryTypeBits, vkStr((VkResult)res.bindResult).c_str(), vkStr((VkResult)res.mapResult).c_str(), (long long)res.vkMismatch, (long long)res.vkGpuMismatch, (long long)backC, vkStr(gt.submitResult).c_str(), (long long)gt.readMismatch, (long long)gpuFillSeenHere, res.note, why.empty() ? "" : " | why: ", why.c_str()); if (!mmapDecisive) detail += " | rawmmap informational: an opaque fd is not required to be mmap-able"; sendMsg(sock, MSG_BYE, nullptr, 0, -1); detail += " "; detail += reapChild(pid); close(sock); freeExportAlloc(c, a); record(routeName, status, detail); } // --------------------------------------------------------------------------- // T1 child // --------------------------------------------------------------------------- static int childT1(int sock) { setRecvTimeout(sock, 30); T1Offer offer{}; uint32_t tag = 0; size_t got = 0; int fd = -1; if (!recvMsg(sock, &tag, &offer, sizeof(offer), &got, &fd) || tag != MSG_T1_OFFER) { pr("child: bad offer (errno=%d)", errno); return 2; } T1Result res{}; res.mmapMismatch = -3; res.vkMismatch = -3; res.mmapGpuMismatch = -2; res.vkGpuMismatch = -2; res.gotFd = fd; if (fd < 0) { snprintf(res.note, sizeof(res.note), "no fd received over SCM_RIGHTS"); sendMsg(sock, MSG_T1_RESULT, &res, sizeof(res), -1); return 3; } pr("child: got fd=%d -> %s", fd, describeFd(fd).c_str()); std::string note = describeFd(fd); // (1) plain mmap of the exported fd size_t mappedLen = (size_t)offer.allocationSize; int firstErrno = 0; void* p = mmap(nullptr, mappedLen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p == MAP_FAILED) { firstErrno = errno; res.mmapOk = 0; res.mmapErrno = firstErrno; pr("child: mmap(MAP_SHARED, allocationSize) failed errno=%d (%s)", firstErrno, strerror(firstErrno)); // second chance: some allocators only allow the buffer size, not the padded size mappedLen = (size_t)offer.bufferSize; p = mmap(nullptr, mappedLen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p != MAP_FAILED) { res.mmapOk = 2; res.mmapErrno = 0; // the mapping succeeded; the first errno is history, not the verdict note += fmt(" [mmap needed bufferSize not allocationSize; allocationSize errno=%d]", firstErrno); } else { res.mmapErrno = errno; } } else { res.mmapOk = 1; } // Read through the plain mapping, but do NOT write through it yet: if this // mapping is offset-shifted relative to the driver's view of the same // allocation, an early write here lands on top of the exporter's payload and // poisons the Vulkan-import read below. Reads first, writes afterwards. res.mmapPatternOffset = -1; if (p != MAP_FAILED) { res.mmapMismatch = checkRegion(p, REG_A, offer.seedA); if (offer.gpuRan) res.mmapGpuMismatch = checkFillWord(p, REG_E, offer.gpuWord); if (res.mmapMismatch != -1) { // Locate the exporter's payload: an fd that maps at a fixed offset from // the driver's base is still usable, but only if that offset is // discoverable, which opaque-fd does not promise. Report it either way. uint8_t want[64]; fillPattern(want, sizeof(want), offer.seedA); const uint8_t* hay = (const uint8_t*)p; for (uint64_t off = 0; off + sizeof(want) <= mappedLen; ++off) { if (!memcmp(hay + off, want, sizeof(want))) { res.mmapPatternOffset = (int64_t)off; break; } } } } // writes through the plain mapping, deferred until every read is done auto writeThroughMmap = [&]() { if (p == MAP_FAILED) return; writeRegion(p, REG_B, offer.seedB); res.wroteB = 1; msync(p, mappedLen, MS_SYNC); }; // (2) import the same fd into a child-side VkDeviceMemory and map it VkCtx c; if (!vkCtxInit(c, false)) { writeThroughMmap(); snprintf(res.note, sizeof(res.note), "%s | child vulkan init failed", note.c_str()); sendMsg(sock, MSG_T1_RESULT, &res, sizeof(res), -1); return 4; } res.vkInitOk = 1; VkExternalMemoryHandleTypeFlagBits ht = (VkExternalMemoryHandleTypeFlagBits)offer.handleType; uint32_t fdTypeBits = 0xFFFFFFFFu; if (c.pGetMemoryFdPropertiesKHR) { VkMemoryFdPropertiesKHR fdProps{}; fdProps.sType = VK_STRUCTURE_TYPE_MEMORY_FD_PROPERTIES_KHR; VkResult fr = c.pGetMemoryFdPropertiesKHR(c.device, ht, fd, &fdProps); res.fdPropsResult = (int32_t)fr; res.fdMemoryTypeBits = fdProps.memoryTypeBits; // OPAQUE_FD does not permit vkGetMemoryFdPropertiesKHR; only DMA_BUF does. if (fr == VK_SUCCESS && ht == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) fdTypeBits = fdProps.memoryTypeBits; } else { res.fdPropsResult = (int32_t)VK_ERROR_EXTENSION_NOT_PRESENT; } VkExternalMemoryBufferCreateInfo ext{}; ext.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; ext.handleTypes = ht; VkBufferCreateInfo bci{}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.pNext = &ext; bci.size = offer.bufferSize; bci.usage = kProbeBufferUsage; VkBuffer buf = VK_NULL_HANDLE; VkResult r = vkCreateBuffer(c.device, &bci, nullptr, &buf); if (r != VK_SUCCESS) { res.importResult = (int32_t)r; writeThroughMmap(); snprintf(res.note, sizeof(res.note), "%s | child vkCreateBuffer=%s", note.c_str(), vkStr(r).c_str()); sendMsg(sock, MSG_T1_RESULT, &res, sizeof(res), -1); vkCtxDestroy(c); return 5; } VkMemoryRequirements req{}; vkGetBufferMemoryRequirements(c.device, buf, &req); uint32_t bits = req.memoryTypeBits & fdTypeBits; // For OPAQUE_FD the spec requires the importer to name the *same* memory type // index the exporter allocated from; only DMA_BUF lets the importer choose // from vkGetMemoryFdPropertiesKHR. int typeIdx; if (ht == VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT) { typeIdx = (int)offer.memoryTypeIndex; } else { typeIdx = pickMemType(c.memProps, bits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (typeIdx < 0) typeIdx = (int)offer.memoryTypeIndex; // fall back to the exporter's choice } // the import consumes the fd on success, so hand over a duplicate int importFd = dup(fd); VkImportMemoryFdInfoKHR imp{}; imp.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_FD_INFO_KHR; imp.handleType = ht; imp.fd = importFd; VkMemoryDedicatedAllocateInfo dedicated{}; dedicated.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; dedicated.buffer = buf; imp.pNext = &dedicated; VkMemoryAllocateInfo mai{}; mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.pNext = &imp; mai.allocationSize = offer.allocationSize; mai.memoryTypeIndex = (uint32_t)typeIdx; VkDeviceMemory mem = VK_NULL_HANDLE; r = vkAllocateMemory(c.device, &mai, nullptr, &mem); res.importResult = (int32_t)r; if (r != VK_SUCCESS) { // retry without the dedicated-allocation chain -- some drivers reject it close(importFd); importFd = dup(fd); imp.fd = importFd; imp.pNext = nullptr; r = vkAllocateMemory(c.device, &mai, nullptr, &mem); if (r == VK_SUCCESS) { note += " [import needed no dedicated info]"; res.importResult = (int32_t)r; } else { close(importFd); writeThroughMmap(); snprintf(res.note, sizeof(res.note), "%s | import=%s type=%d bits=0x%x", note.c_str(), vkStr(r).c_str(), typeIdx, bits); vkDestroyBuffer(c.device, buf, nullptr); sendMsg(sock, MSG_T1_RESULT, &res, sizeof(res), -1); vkCtxDestroy(c); return 0; } } res.bindResult = (int32_t)vkBindBufferMemory(c.device, buf, mem, 0); void* host = nullptr; r = vkMapMemory(c.device, mem, 0, VK_WHOLE_SIZE, 0, &host); res.mapResult = (int32_t)r; if (r == VK_SUCCESS && host) { res.vkMismatch = checkRegion(host, REG_A, offer.seedA); if (offer.gpuRan) res.vkGpuMismatch = checkFillWord(host, REG_E, offer.gpuWord); writeRegion(host, REG_C, offer.seedC); res.wroteC = 1; vkUnmapMemory(c.device, mem); } // now that both mappings have been read, write through the plain one too writeThroughMmap(); snprintf(res.note, sizeof(res.note), "%s | childType=%d bits=0x%x", note.c_str(), typeIdx, bits); vkFreeMemory(c.device, mem, nullptr); vkDestroyBuffer(c.device, buf, nullptr); sendMsg(sock, MSG_T1_RESULT, &res, sizeof(res), -1); vkCtxDestroy(c); if (p != MAP_FAILED) munmap(p, mappedLen); close(fd); return 0; } // --------------------------------------------------------------------------- // T1 for DirectGLES ("Espryt"): the exported fd imported as GL buffer storage // // AcquirePersistentMap on the GLES backend is glBufferStorageEXT + // glMapBufferRange(PERSISTENT|COHERENT), not a VkDeviceMemory map, so the // Vulkan T1 answer above does not decide the tier for that backend. The GL // route to the same question is GL_EXT_memory_object{,_fd}: import the fd as a // memory object, back a buffer with it, and map that buffer persistently. // Tried in-process first (isolates "GL can import this fd at all" from // "the fd survives a process boundary"), then cross-process. // --------------------------------------------------------------------------- static const char* kT1GlSameProc = "T1-gles-memobj-fd-same-proc"; static const char* kT1GlCrossProc = "T1-gles-memobj-fd-cross-proc"; static int childT1Gl(int sock) { setRecvTimeout(sock, 30); T1GlOffer offer{}; uint32_t tag = 0; size_t got = 0; int fd = -1; if (!recvMsg(sock, &tag, &offer, sizeof(offer), &got, &fd) || tag != MSG_T1GL_OFFER) { pr("child: bad T1GL offer errno=%d", errno); return 2; } T1GlResult res{}; res.mismatchA = -3; res.mismatchGpu = -2; if (fd < 0) { snprintf(res.note, sizeof(res.note), "no fd over SCM_RIGHTS"); sendMsg(sock, MSG_T1GL_RESULT, &res, sizeof(res), -1); return 3; } std::string note = describeFd(fd); GlCtx g; if (!glCtxInit(g)) { snprintf(res.note, sizeof(res.note), "%s | child EGL/GLES init failed", note.c_str()); sendMsg(sock, MSG_T1GL_RESULT, &res, sizeof(res), -1); close(fd); return 4; } res.glInitOk = 1; if (!g.canImportFd()) { snprintf(res.note, sizeof(res.note), "%s | %s", note.c_str(), g.missingForImportFd().c_str()); sendMsg(sock, MSG_T1GL_RESULT, &res, sizeof(res), -1); glCtxDestroy(g); close(fd); return 0; } res.haveExts = 1; GlImport imp; glImportFdBuffer(g, fd, offer.allocationSize, offer.bufferSize, offer.dedicated != 0, imp); res.memObjOk = imp.memObjOk; res.storageOk = imp.storageOk; res.mapOk = imp.mapOk; res.persistentCoherent = imp.persistentCoherent; res.errImport = imp.errImport; res.errStorage = imp.errStorage; res.errMap = imp.errMap; res.errMap2 = imp.errMap2; if (imp.mapOk && imp.ptr) { res.mismatchA = checkRegion(imp.ptr, REG_A, offer.seedA); if (offer.gpuRan) res.mismatchGpu = checkFillWord(imp.ptr, REG_E, offer.gpuWord); writeRegion(imp.ptr, REG_D, offer.seedD); res.wroteD = 1; glImportPublish(g, imp); } snprintf(res.note, sizeof(res.note), "%s | accepted=%s | ladder: %s| %s", note.c_str(), imp.variant.empty() ? "none" : imp.variant.c_str(), imp.ladder.c_str(), imp.fail.c_str()); glImportRelease(g, imp); sendMsg(sock, MSG_T1GL_RESULT, &res, sizeof(res), -1); glCtxDestroy(g); close(fd); return 0; } static void runT1GlesParent(VkCtx& c, GlCtx& g, bool glOk, uint64_t size) { if (!glOk) { record(kT1GlSameProc, "SKIP", "no headless GLES context"); record(kT1GlCrossProc, "SKIP", "no headless GLES context"); return; } bool uuidMatch = false; std::string glUuid = glDeviceUuidReport(g, c.deviceUUID, &uuidMatch); std::string uuidNote = fmt("glDeviceUUID=%s vkMatch=%d", glUuid.c_str(), (int)uuidMatch); if (!g.canImportFd()) { std::string d = g.missingForImportFd() + " | " + uuidNote; record(kT1GlSameProc, "UNSUPPORTED", d); record(kT1GlCrossProc, "UNSUPPORTED", d); return; } if (!c.hasExtMemFd || !c.pGetMemoryFdKHR) { record(kT1GlSameProc, "UNSUPPORTED", "VK_KHR_external_memory_fd absent, nothing to import"); record(kT1GlCrossProc, "UNSUPPORTED", "VK_KHR_external_memory_fd absent, nothing to import"); return; } ExportAlloc a; if (!exportHostVisible(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT, size, "T1-gles", a)) { std::string d = a.fail + " | " + uuidNote; record(kT1GlSameProc, exportFailStatus(a), d); record(kT1GlCrossProc, exportFailStatus(a), d); freeExportAlloc(c, a); return; } const uint32_t seedA = 0x61510001u, seedD = 0x61510004u, seedF = 0x61510006u; const uint32_t gpuWord = 0x6C651234u; memset(a.host, 0, (size_t)size); writeRegion(a.host, REG_A, seedA); GpuTouch gt = gpuTouch(c, a.buf, REG_A, seedA, REG_E, gpuWord); int64_t gpuFillSeenHere = gt.ran ? checkFillWord(a.host, REG_E, gpuWord) : -3; const bool gpuUsable = gt.ran && gt.readMismatch == -1 && gpuFillSeenHere == -1; pr("T1-gles gpuTouch ran=%d submit=%s read=%lld fill=%lld %s", (int)gt.ran, vkStr(gt.submitResult).c_str(), (long long)gt.readMismatch, (long long)gpuFillSeenHere, gt.fail.c_str()); // ---- (a) same process ---------------------------------------------------- { GlImport imp; glImportFdBuffer(g, a.fd, a.allocationSize, size, a.dedicated, imp); int64_t cmpA = -3, cmpGpu = -2, backF = -2; if (imp.mapOk && imp.ptr) { cmpA = checkRegion(imp.ptr, REG_A, seedA); if (gpuUsable) cmpGpu = checkFillWord(imp.ptr, REG_E, gpuWord); writeRegion(imp.ptr, REG_F, seedF); glImportPublish(g, imp); backF = checkRegion(a.host, REG_F, seedF); } std::vector legs; Leg l; l.name = "gl-import"; l.decisive = true; l.attempted = imp.storageOk; l.readOk = imp.mapOk && cmpA == -1 && (!gpuUsable || cmpGpu == -1); l.writeOk = imp.mapOk && backF == -1; if (!imp.memObjOk) l.fail = "glImportMemoryFdEXT -> " + glErrStr(imp.errImport); else if (!imp.storageOk) l.fail = "glBufferStorageMemEXT -> " + glErrStr(imp.errStorage); else if (!imp.mapOk) l.fail = "glMapBufferRange persistent -> " + glErrStr(imp.errMap) + ", plain -> " + glErrStr(imp.errMap2); else if (!l.readOk) l.fail = fmt("Vulkan-written payload not visible through the GL map (cmp=%lld gpuCmp=%lld)", (long long)cmpA, (long long)cmpGpu); else if (!l.writeOk) l.fail = fmt("GL-map write not visible through the Vulkan map (back=%lld)", (long long)backF); legs.push_back(l); // The tier needs a *persistent coherent* mapping, not a scoped one: a // driver that only grants the scoped map cannot host AcquirePersistentMap. Leg pc; pc.name = "persistent-coherent"; pc.decisive = true; pc.attempted = imp.mapOk; pc.readOk = imp.persistentCoherent; pc.writeOk = imp.persistentCoherent; if (!imp.mapOk) pc.fail = "no mapping at all"; else if (!imp.persistentCoherent) pc.fail = "PERSISTENT|COHERENT refused (" + glErrStr(imp.errMap) + "), only a scoped map works"; legs.push_back(pc); std::string why; const char* status = legVerdict(legs, &why); record(kT1GlSameProc, status, fmt("%s | memObj=%d(%s) storage=%d(%s) accepted=%s map=%d persistentCoherent=%d(%s/%s) cmpA=%lld " "cmpGpu=%lld backF=%lld | ladder: %s| %s | %s", legTrace(legs).c_str(), (int)imp.memObjOk, glErrStr(imp.errImport).c_str(), (int)imp.storageOk, glErrStr(imp.errStorage).c_str(), imp.variant.empty() ? "none" : imp.variant.c_str(), (int)imp.mapOk, (int)imp.persistentCoherent, glErrStr(imp.errMap).c_str(), glErrStr(imp.errMap2).c_str(), (long long)cmpA, (long long)cmpGpu, (long long)backF, imp.ladder.c_str(), uuidNote.c_str(), why.c_str())); glImportRelease(g, imp); } // ---- (b) cross process --------------------------------------------------- { int sock = -1; pid_t pid = spawnChild("t1gl", &sock); if (pid < 0) { record(kT1GlCrossProc, "FAIL", "spawnChild failed"); freeExportAlloc(c, a); return; } T1GlOffer offer{}; offer.allocationSize = a.allocationSize; offer.bufferSize = size; offer.seedA = seedA; offer.seedD = seedD; offer.gpuWord = gpuWord; offer.gpuRan = gpuUsable ? 1u : 0u; offer.dedicated = a.dedicated ? 1u : 0u; if (!sendMsg(sock, MSG_T1GL_OFFER, &offer, sizeof(offer), a.fd)) { record(kT1GlCrossProc, "FAIL", fmt("sendMsg(offer) errno=%d", errno)); sendMsg(sock, MSG_BYE, nullptr, 0, -1); reapChild(pid); close(sock); freeExportAlloc(c, a); return; } T1GlResult res{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(sock, &tag, &res, sizeof(res), &got, nullptr) || tag != MSG_T1GL_RESULT || got != sizeof(res)) { record(kT1GlCrossProc, "FAIL", fmt("no reply errno=%d %s", errno, reapChild(pid).c_str())); close(sock); freeExportAlloc(c, a); return; } int64_t backD = res.wroteD ? checkRegion(a.host, REG_D, seedD) : -2; std::vector legs; Leg l; l.name = "gl-import"; l.decisive = true; l.attempted = res.storageOk != 0; l.readOk = res.mapOk && res.mismatchA == -1 && (!offer.gpuRan || res.mismatchGpu == -1); l.writeOk = res.wroteD && backD == -1; if (!res.glInitOk) l.fail = "child EGL/GLES init failed"; else if (!res.haveExts) l.fail = "child lacks GL_EXT_memory_object{,_fd}"; else if (!res.memObjOk) l.fail = "glImportMemoryFdEXT -> " + glErrStr(res.errImport); else if (!res.storageOk) l.fail = "glBufferStorageMemEXT -> " + glErrStr(res.errStorage); else if (!res.mapOk) l.fail = "glMapBufferRange persistent -> " + glErrStr(res.errMap) + ", plain -> " + glErrStr(res.errMap2); else if (!l.readOk) l.fail = fmt("exporter payload not visible through the child's GL map (cmp=%lld gpuCmp=%lld)", (long long)res.mismatchA, (long long)res.mismatchGpu); else if (!l.writeOk) l.fail = fmt("child GL-map write not visible to the exporter (back=%lld)", (long long)backD); legs.push_back(l); Leg pc; pc.name = "persistent-coherent"; pc.decisive = true; pc.attempted = res.mapOk != 0; pc.readOk = res.persistentCoherent != 0; pc.writeOk = res.persistentCoherent != 0; if (!res.mapOk) pc.fail = "no mapping at all"; else if (!res.persistentCoherent) pc.fail = "PERSISTENT|COHERENT refused (" + glErrStr(res.errMap) + "), only a scoped map works"; legs.push_back(pc); std::string why; const char* status = legVerdict(legs, &why); std::string detail = fmt("%s | glInit=%d exts=%d memObj=%d(%s) storage=%d(%s) map=%d persistentCoherent=%d(%s/%s) " "cmpA=%lld cmpGpu=%lld backD=%lld | %s | %s [%s] ", legTrace(legs).c_str(), res.glInitOk, res.haveExts, res.memObjOk, glErrStr(res.errImport).c_str(), res.storageOk, glErrStr(res.errStorage).c_str(), res.mapOk, res.persistentCoherent, glErrStr(res.errMap).c_str(), glErrStr(res.errMap2).c_str(), (long long)res.mismatchA, (long long)res.mismatchGpu, (long long)backD, uuidNote.c_str(), why.c_str(), res.note); sendMsg(sock, MSG_BYE, nullptr, 0, -1); detail += reapChild(pid); close(sock); record(kT1GlCrossProc, status, detail); } freeExportAlloc(c, a); } // --------------------------------------------------------------------------- // T0: child allocates an AHardwareBuffer BLOB, parent imports it // --------------------------------------------------------------------------- #if PROBE_HAVE_AHB static int childT0(int sock) { setRecvTimeout(sock, 30); T0Request rq{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(sock, &tag, &rq, sizeof(rq), &got, nullptr) || tag != MSG_T0_REQUEST) { pr("child: bad T0 request errno=%d", errno); return 2; } AHardwareBuffer_Desc desc{}; desc.width = (uint32_t)rq.size; desc.height = 1; desc.layers = 1; desc.format = AHARDWAREBUFFER_FORMAT_BLOB; desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER; AHardwareBuffer* ahb = nullptr; int rc = AHardwareBuffer_allocate(&desc, &ahb); T0Alloc alloc{}; alloc.allocOk = (rc == 0 && ahb) ? 1 : 0; alloc.allocErr = rc; alloc.size = rq.size; if (!alloc.allocOk) { snprintf(alloc.note, sizeof(alloc.note), "AHardwareBuffer_allocate rc=%d errno=%d", rc, errno); sendMsg(sock, MSG_T0_ALLOC, &alloc, sizeof(alloc), -1); return 3; } AHardwareBuffer_Desc back{}; AHardwareBuffer_describe(ahb, &back); alloc.stride = back.stride; snprintf(alloc.note, sizeof(alloc.note), "desc w=%u h=%u layers=%u fmt=0x%x usage=0x%llx stride=%u", back.width, back.height, back.layers, back.format, (unsigned long long)back.usage, back.stride); void* p = nullptr; rc = AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, nullptr, &p); if (rc != 0 || !p) { alloc.allocOk = 2; snprintf(alloc.note + strlen(alloc.note), sizeof(alloc.note) - strlen(alloc.note), " | lock rc=%d", rc); sendMsg(sock, MSG_T0_ALLOC, &alloc, sizeof(alloc), -1); return 4; } memset(p, 0, (size_t)rq.size); writeRegion(p, REG_A, rq.seedA); AHardwareBuffer_unlock(ahb, nullptr); if (!sendMsg(sock, MSG_T0_ALLOC, &alloc, sizeof(alloc), -1)) return 5; int sendRc = AHardwareBuffer_sendHandleToUnixSocket(ahb, sock); pr("child: AHardwareBuffer_sendHandleToUnixSocket rc=%d", sendRc); if (sendRc != 0) return 6; T0Verify ver{}; if (!recvMsg(sock, &tag, &ver, sizeof(ver), &got, nullptr) || tag != MSG_T0_VERIFY) { pr("child: no T0 verify errno=%d", errno); AHardwareBuffer_release(ahb); return 7; } T0Result res{}; res.mismatchB = res.mismatchC = res.mismatchD = res.mismatchE = -2; void* q = nullptr; rc = AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &q); res.lockOk = (rc == 0 && q) ? 1 : 0; res.lockErr = rc; if (res.lockOk) { if (ver.writtenMask & 1) res.mismatchB = checkRegion(q, REG_B, ver.seedB); if (ver.writtenMask & 2) res.mismatchC = checkRegion(q, REG_C, ver.seedC); if (ver.writtenMask & 4) res.mismatchD = checkRegion(q, REG_D, ver.seedD); if (ver.writtenMask & 8) res.mismatchE = checkFillWord(q, REG_E, ver.gpuWord); AHardwareBuffer_unlock(ahb, nullptr); } snprintf(res.note, sizeof(res.note), "mask=0x%x", ver.writtenMask); sendMsg(sock, MSG_T0_RESULT, &res, sizeof(res), -1); AHardwareBuffer_release(ahb); return 0; } static void runT0Parent(VkCtx& c, GlCtx& g, bool glOk, uint64_t size) { const uint32_t seedA = 0x0A0A0011u, seedB = 0x0B0B0022u, seedC = 0x0C0C0033u, seedD = 0x0D0D0044u; const uint32_t gpuWord = 0x70701234u; // The composite row is recorded at the very end from the full // import + map + compare + write-back chain; the handoff alone is only a // diagnostic and gets its own informational row. auto failAll = [&](const std::string& why) { record("T0-ahb-handoff", "FAIL", why); record("T0-ahb-blob-transfer", "FAIL", "handoff failed, nothing to import: " + why); }; int sock = -1; pid_t pid = spawnChild("t0", &sock); if (pid < 0) { failAll("spawnChild failed"); return; } T0Request rq{}; rq.size = size; rq.seedA = seedA; if (!sendMsg(sock, MSG_T0_REQUEST, &rq, sizeof(rq), -1)) { failAll(fmt("sendMsg errno=%d", errno)); close(sock); reapChild(pid); return; } T0Alloc alloc{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(sock, &tag, &alloc, sizeof(alloc), &got, nullptr) || tag != MSG_T0_ALLOC) { failAll(fmt("no alloc reply errno=%d %s", errno, reapChild(pid).c_str())); close(sock); return; } if (alloc.allocOk != 1) { failAll(fmt("child alloc failed rc=%d %s", alloc.allocErr, alloc.note)); close(sock); reapChild(pid); return; } pr("T0 child allocated: %s", alloc.note); AHardwareBuffer* ahb = nullptr; int rc = AHardwareBuffer_recvHandleFromUnixSocket(sock, &ahb); if (rc != 0 || !ahb) { failAll(fmt("recvHandleFromUnixSocket rc=%d errno=%d", rc, errno)); close(sock); reapChild(pid); return; } AHardwareBuffer_Desc desc{}; AHardwareBuffer_describe(ahb, &desc); pr("T0 parent received AHB: w=%u h=%u fmt=0x%x usage=0x%llx stride=%u", desc.width, desc.height, desc.format, (unsigned long long)desc.usage, desc.stride); record("T0-ahb-handoff", "OK", fmt("socket handoff of a %llu-byte BLOB works (%s) -- handoff only, see T0-ahb-blob-transfer for the tier", (unsigned long long)size, alloc.note)); uint32_t writtenMask = 0; int64_t cpuCmp = -3, vkCmp = -3, glCmp = -3, vkGpuCmp = -2; bool vkMapped = false, glMapped = false, glPersistent = false; GpuTouch gt; std::string vkFail, glFail, cpuFail, gpuFail; // (a) CPU path: AHardwareBuffer_lock on the receiving side { void* p = nullptr; rc = AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, nullptr, &p); if (rc != 0 || !p) { cpuFail = fmt("AHardwareBuffer_lock rc=%d errno=%d", rc, errno); record("T0-ahb-cpu-lock", "FAIL", cpuFail); } else { cpuCmp = checkRegion(p, REG_A, seedA); writeRegion(p, REG_D, seedD); writtenMask |= 4; AHardwareBuffer_unlock(ahb, nullptr); if (cpuCmp != -1) cpuFail = fmt("payload mismatch at %lld", (long long)cpuCmp); record("T0-ahb-cpu-lock", cpuCmp == -1 ? "OK" : "FAIL", fmt("cross-process CPU read of the client's payload, mismatch=%lld", (long long)cpuCmp)); } } // (b) Vulkan import (+ a real GPU access on the imported memory) if (!c.hasAhb || !c.pGetAhbProps) { vkFail = "VK_ANDROID_external_memory_android_hardware_buffer absent"; record("T0-ahb-vulkan-import", "UNSUPPORTED", vkFail); } else { VkAndroidHardwareBufferPropertiesANDROID props{}; props.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID; VkResult r = c.pGetAhbProps(c.device, ahb, &props); if (r != VK_SUCCESS) { vkFail = "vkGetAndroidHardwareBufferPropertiesANDROID=" + vkStr(r); record("T0-ahb-vulkan-import", "FAIL", vkFail); } else { pr("T0 AHB props: allocationSize=%llu memoryTypeBits=0x%x", (unsigned long long)props.allocationSize, props.memoryTypeBits); VkExternalMemoryBufferCreateInfo ext{}; ext.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID; VkBufferCreateInfo bci{}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.pNext = &ext; bci.size = size; bci.usage = kProbeBufferUsage; VkBuffer buf = VK_NULL_HANDLE; r = vkCreateBuffer(c.device, &bci, nullptr, &buf); if (r != VK_SUCCESS) { vkFail = "vkCreateBuffer(AHB external)=" + vkStr(r); record("T0-ahb-vulkan-import", "FAIL", vkFail); } else { int typeIdx = pickMemType(c.memProps, props.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); bool hostVisible = typeIdx >= 0; if (typeIdx < 0) typeIdx = pickMemType(c.memProps, props.memoryTypeBits, 0); VkImportAndroidHardwareBufferInfoANDROID imp{}; imp.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID; imp.buffer = ahb; VkMemoryDedicatedAllocateInfo ded{}; ded.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; ded.buffer = buf; imp.pNext = &ded; VkMemoryAllocateInfo mai{}; mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.pNext = &imp; mai.allocationSize = props.allocationSize; mai.memoryTypeIndex = (uint32_t)(typeIdx < 0 ? 0 : typeIdx); VkDeviceMemory mem = VK_NULL_HANDLE; r = vkAllocateMemory(c.device, &mai, nullptr, &mem); if (r != VK_SUCCESS) { vkFail = fmt("vkAllocateMemory(import AHB)=%s typeIdx=%d bits=0x%x", vkStr(r).c_str(), typeIdx, props.memoryTypeBits); record("T0-ahb-vulkan-import", "FAIL", vkFail); } else { VkResult br = vkBindBufferMemory(c.device, buf, mem, 0); // GPU access on the client's allocation -- the part that makes // T0 a tier rather than a successful mmap if (br == VK_SUCCESS) { gt = gpuTouch(c, buf, REG_A, seedA, REG_E, gpuWord); if (gt.ran) writtenMask |= 8; gpuFail = gt.fail; } else { gpuFail = "vkBindBufferMemory=" + vkStr(br); } void* host = nullptr; VkResult mr = vkMapMemory(c.device, mem, 0, VK_WHOLE_SIZE, 0, &host); if (mr == VK_SUCCESS && host) { vkMapped = true; vkCmp = checkRegion(host, REG_A, seedA); if (gt.ran) vkGpuCmp = checkFillWord(host, REG_E, gpuWord); writeRegion(host, REG_B, seedB); writtenMask |= 1; vkUnmapMemory(c.device, mem); if (vkCmp != -1) vkFail = fmt("payload mismatch at %lld", (long long)vkCmp); record("T0-ahb-vulkan-import", vkCmp == -1 ? "OK" : "PARTIAL", fmt("imported+mapped (hostVisibleType=%d bind=%s) payload mismatch=%lld gpuFill=%lld", (int)hostVisible, vkStr(br).c_str(), (long long)vkCmp, (long long)vkGpuCmp)); } else { vkFail = fmt("vkMapMemory=%s (bind=%s hostVisibleType=%d bits=0x%x)", vkStr(mr).c_str(), vkStr(br).c_str(), (int)hostVisible, props.memoryTypeBits); record("T0-ahb-vulkan-import", "PARTIAL", "import ok, " + vkFail); } vkFreeMemory(c.device, mem, nullptr); } vkDestroyBuffer(c.device, buf, nullptr); } } } record("T0-ahb-gpu-access", gt.ran && gt.readMismatch == -1 ? "OK" : (gt.ran ? "FAIL" : "SKIP"), fmt("vkCmdCopyBuffer out of the client AHB + vkCmdFillBuffer into it: ran=%d submit=%s read=%lld " "fillSeenByServerMap=%lld %s", (int)gt.ran, vkStr(gt.submitResult).c_str(), (long long)gt.readMismatch, (long long)vkGpuCmp, gpuFail.c_str())); // (c) GL import through EGL_ANDROID_get_native_client_buffer + EXT_external_buffer // -- this is the DirectGLES ("Espryt") form of T0: the server backs a GL // buffer with the client's allocation and maps it persistent/coherent. if (!glOk) { glFail = "no GL context"; record("T0-ahb-gl-import", "SKIP", glFail); } else if (!g.hasGl("GL_EXT_external_buffer") || !g.pBufferStorageExternal || !g.pGetNativeClientBuffer) { glFail = fmt("GL_EXT_external_buffer=%d GL_EXT_buffer_storage=%d eglGetNativeClientBufferANDROID=%d " "glBufferStorageExternalEXT=%d", (int)g.hasGl("GL_EXT_external_buffer"), (int)g.hasGl("GL_EXT_buffer_storage"), (int)(g.pGetNativeClientBuffer != nullptr), (int)(g.pBufferStorageExternal != nullptr)); record("T0-ahb-gl-import", "UNSUPPORTED", glFail); } else { EGLClientBuffer cb = g.pGetNativeClientBuffer(ahb); if (!cb) { glFail = fmt("eglGetNativeClientBufferANDROID=NULL egl=0x%04x", eglGetError()); record("T0-ahb-gl-import", "FAIL", glFail); } else { GLuint b = 0; glGenBuffers(1, &b); glBindBuffer(GL_ARRAY_BUFFER, b); glDrain(); g.pBufferStorageExternal(GL_ARRAY_BUFFER, 0, (GLsizeiptr)size, cb, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT | GL_DYNAMIC_STORAGE_BIT_EXT); GLenum errStorage = glDrain(); if (errStorage != GL_NO_ERROR) { glFail = "glBufferStorageExternalEXT -> " + glErrStr(errStorage); record("T0-ahb-gl-import", "FAIL", glFail); } else { void* m = glMapBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT); GLenum errMap = glDrain(); GLenum errMap2 = GL_NO_ERROR; glPersistent = m != nullptr; if (!m) { m = glMapBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); errMap2 = glDrain(); } if (!m) { glFail = "glMapBufferRange persistent -> " + glErrStr(errMap) + ", plain -> " + glErrStr(errMap2); record("T0-ahb-gl-import", "FAIL", "storage ok, " + glFail); } else { glMapped = true; glCmp = checkRegion(m, REG_A, seedA); writeRegion(m, REG_C, seedC); writtenMask |= 2; glUnmapBuffer(GL_ARRAY_BUFFER); glFinish(); if (glCmp != -1) glFail = fmt("payload mismatch at %lld", (long long)glCmp); if (!glPersistent) glFail += " [PERSISTENT|COHERENT refused: " + glErrStr(errMap) + "]"; record("T0-ahb-gl-import", (glCmp == -1 && glPersistent) ? "OK" : "PARTIAL", fmt("GL map of the client AHB: persistentCoherent=%d (persistent err=%s, plain err=%s) " "payload mismatch=%lld", (int)glPersistent, glErrStr(errMap).c_str(), glErrStr(errMap2).c_str(), (long long)glCmp)); } } glBindBuffer(GL_ARRAY_BUFFER, 0); glDeleteBuffers(1, &b); } } // (d) ask the client to verify everything the server wrote T0Verify ver{}; ver.seedB = seedB; ver.seedC = seedC; ver.seedD = seedD; ver.gpuWord = gpuWord; ver.writtenMask = writtenMask; T0Result res{}; res.mismatchB = res.mismatchC = res.mismatchD = res.mismatchE = -3; bool gotVerify = false; std::string wbDetail; const char* wbStatus = "FAIL"; if (!sendMsg(sock, MSG_T0_VERIFY, &ver, sizeof(ver), -1)) { wbDetail = fmt("sendMsg(verify) errno=%d", errno); } else if (!recvMsg(sock, &tag, &res, sizeof(res), &got, nullptr) || tag != MSG_T0_RESULT) { wbDetail = fmt("no verify reply errno=%d", errno); } else { gotVerify = true; bool anyChecked = false, allOk = true; auto acc = [&](int64_t v) { if (v == -2 || v == -3) return; anyChecked = true; if (v != -1) allOk = false; }; acc(res.mismatchB); acc(res.mismatchC); acc(res.mismatchD); acc(res.mismatchE); wbStatus = !anyChecked ? "SKIP" : (allOk ? "OK" : "FAIL"); wbDetail = fmt("mask=0x%x vkWrite=%lld glWrite=%lld cpuWrite=%lld gpuFill=%lld (clientLock=%d rc=%d)", writtenMask, (long long)res.mismatchB, (long long)res.mismatchC, (long long)res.mismatchD, (long long)res.mismatchE, res.lockOk, res.lockErr); } record("T0-ahb-writeback-to-client", wbStatus, wbDetail); // composite tier verdict: handoff alone is not the tier std::vector legs; { Leg l; l.name = "vk-import"; l.decisive = true; l.attempted = vkMapped; l.readOk = vkMapped && vkCmp == -1; l.writeOk = gotVerify && (writtenMask & 1) && res.mismatchB == -1; if (!l.attempted) l.fail = vkFail.empty() ? "not attempted" : vkFail; else if (!l.readOk) l.fail = "server could not read the client payload: " + vkFail; else if (!l.writeOk) l.fail = fmt("server write not visible to the client (back=%lld)", (long long)res.mismatchB); legs.push_back(l); } { Leg l; l.name = "gl-import"; l.decisive = true; l.attempted = glMapped; l.readOk = glMapped && glCmp == -1 && glPersistent; l.writeOk = gotVerify && (writtenMask & 2) && res.mismatchC == -1; if (!l.attempted) l.fail = glFail.empty() ? "not attempted" : glFail; else if (!l.readOk) l.fail = "GL side: " + glFail; else if (!l.writeOk) l.fail = fmt("server GL write not visible to the client (back=%lld)", (long long)res.mismatchC); legs.push_back(l); } { Leg l; l.name = "gpu"; l.decisive = true; l.attempted = gt.ran; l.readOk = gt.readMismatch == -1; l.writeOk = gotVerify && (writtenMask & 8) && res.mismatchE == -1; if (!gt.ran) l.fail = "GPU touch did not run: " + gpuFail; else if (!l.readOk) l.fail = fmt("GPU read of the client allocation mismatched at %lld", (long long)gt.readMismatch); else if (!l.writeOk) l.fail = fmt("GPU write not visible to the client (back=%lld)", (long long)res.mismatchE); legs.push_back(l); } { Leg l; l.name = "cpu-lock"; l.decisive = false; // informational: proves the handle, not the tier l.attempted = cpuCmp != -3; l.readOk = cpuCmp == -1; l.writeOk = gotVerify && (writtenMask & 4) && res.mismatchD == -1; l.fail = cpuFail; legs.push_back(l); } std::string why; const char* status = legVerdict(legs, &why); record("T0-ahb-blob-transfer", status, fmt("%s | full chain handoff+import+map+compare+writeback | cpuCmp=%lld vkCmp=%lld glCmp=%lld " "glPersistentCoherent=%d gpuRan=%d | %s", legTrace(legs).c_str(), (long long)cpuCmp, (long long)vkCmp, (long long)glCmp, (int)glPersistent, (int)gt.ran, why.c_str())); sendMsg(sock, MSG_BYE, nullptr, 0, -1); std::string reap = reapChild(pid); pr("T0 %s", reap.c_str()); AHardwareBuffer_release(ahb); close(sock); } // --------------------------------------------------------------------------- // T0S: sustained lock. The T0 row above locks, writes, unlocks, and only then lets // the GPU in - the one ordering AHardwareBuffer_lock/unlock is specified for. A // persistent map is the opposite shape: the client keeps its pointer (= the lock) // for the life of the store and writes through it while the server's GPU reads and // writes the same pages. So here the client locks ONCE, never unlocks while the // rounds run, and per round: CPU-writes REG_A, the server's GPU reads REG_A (Vulkan // vkCmdCopyBuffer and a GL compute dispatch on the glBufferStorageExternalEXT // buffer) and fills REG_E (Vulkan) / REG_F (GL), and the client reads both fills // back through the still-held pointer. Only after the last round does it unlock and // lock again, which separates "the GPU writes never landed" from "the held CPU // mapping kept stale lines". // --------------------------------------------------------------------------- static int t0sClientRounds(int sock, AHardwareBuffer* ahb, void* p, int lockRc, uint32_t rounds, uint32_t seedBase); static int childT0Sustained(int sock) { setRecvTimeout(sock, 60); T0SRequest rq{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(sock, &tag, &rq, sizeof(rq), &got, nullptr) || tag != MSG_T0S_REQUEST) { pr("child: bad T0S request errno=%d", errno); return 2; } AHardwareBuffer_Desc desc{}; desc.width = (uint32_t)rq.size; desc.height = 1; desc.layers = 1; desc.format = AHARDWAREBUFFER_FORMAT_BLOB; desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER; AHardwareBuffer* ahb = nullptr; int rc = AHardwareBuffer_allocate(&desc, &ahb); T0Alloc alloc{}; alloc.allocOk = (rc == 0 && ahb) ? 1 : 0; alloc.allocErr = rc; alloc.size = rq.size; if (!alloc.allocOk) { snprintf(alloc.note, sizeof(alloc.note), "AHardwareBuffer_allocate rc=%d errno=%d", rc, errno); sendMsg(sock, MSG_T0S_ALLOC, &alloc, sizeof(alloc), -1); return 3; } AHardwareBuffer_Desc back{}; AHardwareBuffer_describe(ahb, &back); alloc.stride = back.stride; void* p = nullptr; const int lockRc = AHardwareBuffer_lock( ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, nullptr, &p); snprintf(alloc.note, sizeof(alloc.note), "desc w=%u usage=0x%llx held-lock rc=%d ptr=%p", back.width, (unsigned long long)back.usage, lockRc, p); if (lockRc != 0 || !p) { alloc.allocOk = 2; sendMsg(sock, MSG_T0S_ALLOC, &alloc, sizeof(alloc), -1); AHardwareBuffer_release(ahb); return 4; } memset(p, 0, (size_t)rq.size); if (!sendMsg(sock, MSG_T0S_ALLOC, &alloc, sizeof(alloc), -1)) return 5; int sendRc = AHardwareBuffer_sendHandleToUnixSocket(ahb, sock); pr("child: T0S held lock ptr=%p, sendHandle rc=%d", p, sendRc); if (sendRc != 0) return 6; const int rc2 = t0sClientRounds(sock, ahb, p, lockRc, rq.rounds, rq.seedBase); AHardwareBuffer_release(ahb); return rc2; } // The client half of the T0S rounds, after the handle has been sent: per round the CPU writes // REG_A through the held lock, the server's GPU reads it and fills REG_E / REG_F, and the client // checks both fills through the still-held pointer; then unlock + relock. The caller releases. static int t0sClientRounds(int sock, AHardwareBuffer* ahb, void* p, int lockRc, uint32_t rounds, uint32_t seedBase) { uint32_t tag = 0; size_t got = 0; uint32_t lastVk = 0, lastGl = 0, lastMask = 0; for (uint32_t i = 0; i < rounds; ++i) { T0SRound r{}; r.round = i; r.seedA = seedBase + i * 0x01010101u; writeRegion(p, REG_A, r.seedA); // through the held lock, no unlock if (!sendMsg(sock, MSG_T0S_ROUND, &r, sizeof(r), -1)) return 7; T0SGpuDone gd{}; if (!recvMsg(sock, &tag, &gd, sizeof(gd), &got, nullptr) || tag != MSG_T0S_GPUDONE) { pr("child: T0S no gpu-done in round %u errno=%d", i, errno); return 8; } T0SCheck ck{}; ck.round = i; ck.mismatchE = (gd.mask & 1) ? checkFillWord(p, REG_E, gd.vkWord) : -2; ck.mismatchF = (gd.mask & 2) ? checkFillWord(p, REG_F, gd.glWord) : -2; lastVk = gd.vkWord; lastGl = gd.glWord; lastMask = gd.mask; if (!sendMsg(sock, MSG_T0S_CHECK, &ck, sizeof(ck), -1)) return 9; } T0SDone dn{}; dn.lockRc = lockRc; if (gHoldMs) { pr("child: holding the locked AHB (%p) for %u ms", p, gHoldMs); usleep(static_cast(gHoldMs) * 1000u); } dn.unlockRc = AHardwareBuffer_unlock(ahb, nullptr); void* q = nullptr; dn.relockRc = AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &q); dn.relockMismatchE = dn.relockMismatchF = -2; if (dn.relockRc == 0 && q) { if (lastMask & 1) dn.relockMismatchE = checkFillWord(q, REG_E, lastVk); if (lastMask & 2) dn.relockMismatchF = checkFillWord(q, REG_F, lastGl); AHardwareBuffer_unlock(ahb, nullptr); } snprintf(dn.note, sizeof(dn.note), "held ptr=%p relock ptr=%p", p, q); sendMsg(sock, MSG_T0S_DONE, &dn, sizeof(dn), -1); recvMsg(sock, &tag, nullptr, 0, &got, nullptr); // BYE return 0; } // A GL compute pass over the imported buffer: copies `readWord`.. (1024 words) into // `out` and fills `fillWord`.. with `fill`. Word offsets are relative to the bound range. static const char* kT0SComputeSrc = "#version 310 es\n" "layout(local_size_x = 64) in;\n" "layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n" "layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n" "uniform uint uRead;\n" "uniform uint uFillAt;\n" "uniform uint uFill;\n" "void main() {\n" " uint i = gl_GlobalInvocationID.x;\n" " outb.w[i] = ext.w[uRead + i];\n" " ext.w[uFillAt + i] = uFill;\n" "}\n"; static GLuint buildT0SCompute(std::string* fail) { GLuint sh = glCreateShader(GL_COMPUTE_SHADER); glShaderSource(sh, 1, &kT0SComputeSrc, nullptr); glCompileShader(sh); GLint ok = 0; glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512] = {0}; glGetShaderInfoLog(sh, sizeof(log) - 1, nullptr, log); *fail = std::string("compute compile failed: ") + log; glDeleteShader(sh); return 0; } GLuint prog = glCreateProgram(); glAttachShader(prog, sh); glLinkProgram(prog); glDeleteShader(sh); glGetProgramiv(prog, GL_LINK_STATUS, &ok); if (!ok) { char log[512] = {0}; glGetProgramInfoLog(prog, sizeof(log) - 1, nullptr, log); *fail = std::string("compute link failed: ") + log; glDeleteProgram(prog); return 0; } return prog; } static void t0sServe(VkCtx& c, GlCtx& g, bool glOk, int sock, AHardwareBuffer* ahb, uint64_t size, uint32_t rounds, const char* rowPrefix, pid_t pid); static void printSummary(); static void runT0SustainedParent(VkCtx& c, GlCtx& g, bool glOk, uint64_t size, uint32_t rounds) { const uint32_t seedBase = 0x5A000001u; int sock = -1; pid_t pid = spawnChild("t0s", &sock); if (pid < 0) { record("T0S-sustained-lock", "FAIL", "spawnChild failed"); return; } T0SRequest rq{}; rq.size = size; rq.rounds = rounds; rq.seedBase = seedBase; T0Alloc alloc{}; uint32_t tag = 0; size_t got = 0; if (!sendMsg(sock, MSG_T0S_REQUEST, &rq, sizeof(rq), -1) || !recvMsg(sock, &tag, &alloc, sizeof(alloc), &got, nullptr) || tag != MSG_T0S_ALLOC || alloc.allocOk != 1) { record("T0S-sustained-lock", "FAIL", fmt("client alloc/held lock failed: ok=%d rc=%d %s errno=%d", alloc.allocOk, alloc.allocErr, alloc.note, errno)); close(sock); reapChild(pid); return; } pr("T0S child allocated and holds its lock: %s", alloc.note); AHardwareBuffer* ahb = nullptr; int rc = AHardwareBuffer_recvHandleFromUnixSocket(sock, &ahb); if (rc != 0 || !ahb) { record("T0S-sustained-lock", "FAIL", fmt("recvHandleFromUnixSocket rc=%d errno=%d", rc, errno)); close(sock); reapChild(pid); return; } t0sServe(c, g, glOk, sock, ahb, size, rounds, "T0S-sustained-lock", pid); AHardwareBuffer_release(ahb); close(sock); } // The server half of the T0S rounds on a received (still client-locked) AHB: import it into // Vulkan and GL, run `rounds` rounds against the client on `sock`, take its DONE, send BYE, and // record -vulkan / -gles / . `pid` >= 0 is a spawned child to reap. The // caller releases the AHB and closes `sock`. static void t0sServe(VkCtx& c, GlCtx& g, bool glOk, int sock, AHardwareBuffer* ahb, uint64_t size, uint32_t rounds, const char* rowPrefix, pid_t pid) { uint32_t tag = 0; size_t got = 0; // --- Vulkan import of the (still client-locked) AHB --- VkBuffer vbuf = VK_NULL_HANDLE; VkDeviceMemory vmem = VK_NULL_HANDLE; void* vhost = nullptr; std::string vkFail; if (!c.hasAhb || !c.pGetAhbProps) { vkFail = "VK_ANDROID_external_memory_android_hardware_buffer absent"; } else { VkAndroidHardwareBufferPropertiesANDROID props{}; props.sType = VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID; VkResult r = c.pGetAhbProps(c.device, ahb, &props); VkExternalMemoryBufferCreateInfo ext{}; ext.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID; VkBufferCreateInfo bci{}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.pNext = &ext; bci.size = size; bci.usage = kProbeBufferUsage; if (r != VK_SUCCESS) { vkFail = "vkGetAndroidHardwareBufferPropertiesANDROID=" + vkStr(r); } else if ((r = vkCreateBuffer(c.device, &bci, nullptr, &vbuf)) != VK_SUCCESS) { vkFail = "vkCreateBuffer(AHB external)=" + vkStr(r); vbuf = VK_NULL_HANDLE; } else { int typeIdx = pickMemType(c.memProps, props.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (typeIdx < 0) typeIdx = pickMemType(c.memProps, props.memoryTypeBits, 0); VkImportAndroidHardwareBufferInfoANDROID imp{}; imp.sType = VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID; imp.buffer = ahb; VkMemoryDedicatedAllocateInfo ded{}; ded.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; ded.buffer = vbuf; imp.pNext = &ded; VkMemoryAllocateInfo mai{}; mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.pNext = &imp; mai.allocationSize = props.allocationSize; mai.memoryTypeIndex = (uint32_t)(typeIdx < 0 ? 0 : typeIdx); if ((r = vkAllocateMemory(c.device, &mai, nullptr, &vmem)) != VK_SUCCESS) { vkFail = "vkAllocateMemory(import AHB)=" + vkStr(r); vmem = VK_NULL_HANDLE; } else if ((r = vkBindBufferMemory(c.device, vbuf, vmem, 0)) != VK_SUCCESS) { vkFail = "vkBindBufferMemory=" + vkStr(r); } else { // informational: the server's own host view of the same pages if (vkMapMemory(c.device, vmem, 0, VK_WHOLE_SIZE, 0, &vhost) != VK_SUCCESS) vhost = nullptr; } } } const bool vkReady = vkFail.empty() && vbuf != VK_NULL_HANDLE && vmem != VK_NULL_HANDLE; // --- GL import + compute program --- std::string glFail; GLuint gbuf = 0, gout = 0, prog = 0; GLint locRead = -1, locFillAt = -1, locFill = -1; if (!glOk) { glFail = "no GL context"; } else if (!g.hasGl("GL_EXT_external_buffer") || !g.pBufferStorageExternal || !g.pGetNativeClientBuffer) { glFail = "GL_EXT_external_buffer / eglGetNativeClientBufferANDROID absent"; } else { EGLClientBuffer cb = g.pGetNativeClientBuffer(ahb); if (!cb) { glFail = fmt("eglGetNativeClientBufferANDROID=NULL egl=0x%04x", eglGetError()); } else { glDrain(); glGenBuffers(1, &gbuf); glBindBuffer(GL_SHADER_STORAGE_BUFFER, gbuf); g.pBufferStorageExternal(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)size, cb, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT_EXT | GL_MAP_COHERENT_BIT_EXT | GL_DYNAMIC_STORAGE_BIT_EXT); GLenum e = glDrain(); if (e != GL_NO_ERROR) { glFail = "glBufferStorageExternalEXT -> " + glErrStr(e); } else { glGenBuffers(1, &gout); glBindBuffer(GL_SHADER_STORAGE_BUFFER, gout); glBufferData(GL_SHADER_STORAGE_BUFFER, (GLsizeiptr)kRegion, nullptr, GL_DYNAMIC_READ); prog = buildT0SCompute(&glFail); if (prog) { locRead = glGetUniformLocation(prog, "uRead"); locFillAt = glGetUniformLocation(prog, "uFillAt"); locFill = glGetUniformLocation(prog, "uFill"); } e = glDrain(); if (glFail.empty() && e != GL_NO_ERROR) glFail = "GL setup -> " + glErrStr(e); } glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); } } const bool glReady = glFail.empty() && prog != 0; uint32_t vkRead = 0, vkWrite = 0, glRead = 0, glWrite = 0, vkHostRead = 0, roundsRun = 0; std::string vkFirst, glFirst, hostFirst; uint32_t lastVk = 0, lastGl = 0; for (uint32_t i = 0; i < rounds; ++i) { T0SRound r{}; if (!recvMsg(sock, &tag, &r, sizeof(r), &got, nullptr) || tag != MSG_T0S_ROUND) { if (vkFirst.empty()) vkFirst = fmt("round %u: no client round message errno=%d", i, errno); break; } T0SGpuDone gd{}; gd.round = i; gd.vkWord = 0x7E000000u + i; gd.glWord = 0x6C000000u + i; lastVk = gd.vkWord; lastGl = gd.glWord; if (vhost) { int64_t h = checkRegion(vhost, REG_A, r.seedA); if (h == -1) ++vkHostRead; else if (hostFirst.empty()) hostFirst = fmt("round %u host mismatch at %lld", i, (long long)h); } if (vkReady) { GpuTouch gt = gpuTouch(c, vbuf, REG_A, r.seedA, REG_E, gd.vkWord); if (gt.ran) gd.mask |= 1; if (gt.ran && gt.readMismatch == -1) ++vkRead; else if (vkFirst.empty()) vkFirst = fmt("round %u GPU read: %s", i, gt.fail.c_str()); } if (glReady) { glUseProgram(prog); glUniform1ui(locRead, (GLuint)(REG_A * kRegion / 4)); glUniform1ui(locFillAt, (GLuint)(REG_F * kRegion / 4)); glUniform1ui(locFill, gd.glWord); glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, gbuf, (GLintptr)gRegionBase, (GLsizeiptr)(kRegionCount * kRegion)); glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, gout); glDispatchCompute((GLuint)(kRegion / 4 / 64), 1, 1); glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT); glFinish(); GLenum e = glDrain(); if (e == GL_NO_ERROR) gd.mask |= 2; glBindBuffer(GL_SHADER_STORAGE_BUFFER, gout); void* m = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, (GLsizeiptr)kRegion, GL_MAP_READ_BIT); int64_t mm = m ? checkPattern(m, kRegion, r.seedA) : -3; if (m) glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); if (e == GL_NO_ERROR && mm == -1) ++glRead; else if (glFirst.empty()) glFirst = fmt("round %u GL compute read: err=%s mismatch=%lld", i, glErrStr(e).c_str(), (long long)mm); } if (!sendMsg(sock, MSG_T0S_GPUDONE, &gd, sizeof(gd), -1)) break; T0SCheck ck{}; if (!recvMsg(sock, &tag, &ck, sizeof(ck), &got, nullptr) || tag != MSG_T0S_CHECK) break; ++roundsRun; if (ck.mismatchE == -1) ++vkWrite; else if ((gd.mask & 1) && vkFirst.empty()) vkFirst = fmt("round %u: GPU fill not seen through the client's held lock (mismatch=%lld)", i, (long long)ck.mismatchE); if (ck.mismatchF == -1) ++glWrite; else if ((gd.mask & 2) && glFirst.empty()) glFirst = fmt("round %u: GL compute fill not seen through the client's held lock (mismatch=%lld)", i, (long long)ck.mismatchF); } T0SDone dn{}; dn.relockMismatchE = dn.relockMismatchF = -3; bool gotDone = recvMsg(sock, &tag, &dn, sizeof(dn), &got, nullptr) && tag == MSG_T0S_DONE; sendMsg(sock, MSG_BYE, nullptr, 0, -1); std::string reap = pid >= 0 ? reapChild(pid) : std::string("no child"); const std::string rowVk = std::string(rowPrefix) + "-vulkan"; const std::string rowGl = std::string(rowPrefix) + "-gles"; auto rowStatus = [&](bool ready, uint32_t rd, uint32_t wr) -> const char* { if (!ready) return "FAIL"; if (rd == rounds && wr == rounds && roundsRun == rounds) return "OK"; if (rd > 0 || wr > 0) return "PARTIAL"; return "FAIL"; }; record(rowVk.c_str(), rowStatus(vkReady, vkRead, vkWrite), fmt("rounds=%u/%u gpuReadOfHeldCpuWrite=%u gpuFillSeenByHeldLock=%u serverHostMapRead=%u%s | after " "unlock+relock: fill=%lld | %s%s", roundsRun, rounds, vkRead, vkWrite, vkHostRead, vhost ? "" : "(no host map)", (long long)dn.relockMismatchE, vkReady ? vkFirst.c_str() : vkFail.c_str(), hostFirst.empty() ? "" : (" | " + hostFirst).c_str())); record(rowGl.c_str(), rowStatus(glReady, glRead, glWrite), fmt("rounds=%u/%u computeReadOfHeldCpuWrite=%u computeFillSeenByHeldLock=%u | after unlock+relock: " "fill=%lld | %s", roundsRun, rounds, glRead, glWrite, (long long)dn.relockMismatchF, glReady ? glFirst.c_str() : glFail.c_str())); std::vector legs; { Leg l; l.name = "vk-gpu"; l.decisive = true; l.attempted = vkReady && roundsRun > 0; l.readOk = vkReady && vkRead == rounds; l.writeOk = vkReady && vkWrite == rounds; l.fail = vkReady ? vkFirst : vkFail; legs.push_back(l); } { Leg l; l.name = "gl-gpu"; l.decisive = true; l.attempted = glReady && roundsRun > 0; l.readOk = glReady && glRead == rounds; l.writeOk = glReady && glWrite == rounds; l.fail = glReady ? glFirst : glFail; legs.push_back(l); } std::string why; const char* status = legVerdict(legs, &why); record(rowPrefix, status, fmt("%s | size=%llu regionBase=%llu rounds=%u client lock rc=%d, unlock rc=%d, relock rc=%d (%s) %s | %s", legTrace(legs).c_str(), (unsigned long long)size, (unsigned long long)gRegionBase, rounds, gotDone ? dn.lockRc : -99, gotDone ? dn.unlockRc : -99, gotDone ? dn.relockRc : -99, gotDone ? dn.note : "no done message", reap.c_str(), why.c_str())); if (prog) glDeleteProgram(prog); if (gout) glDeleteBuffers(1, &gout); if (gbuf) glDeleteBuffers(1, &gbuf); if (vhost) vkUnmapMemory(c.device, vmem); if (vmem) vkFreeMemory(c.device, vmem, nullptr); if (vbuf) vkDestroyBuffer(c.device, vbuf, nullptr); (void)lastVk; (void)lastGl; } // --------------------------------------------------------------------------- // P11 B2 step 1: does a client's AHardwareBuffer cross the REAL routes into the server app? // // server `libMobileGLServer.so --serve`: the probe packaged in the render server's // place (-Pmobilegl.extmemProbeAsServer=ON), so MobileGLServerService execs it where the // supervisor would run - in the server app's own untrusted_app domain - and B1's broker // connects to it, writes its PairBind pair and hands the connected pair to the helper. // Each accepted pair: skip the PairBind frame on both connections (told apart by the // frame length: control 64 bytes, aux 68 - PairBindFrames.java), then serve the client. // client `extmem_probe --route-client[=]`: exec'd by the B1 helper with // MOBILEGL_IPC_CONTROL=fd:, (routes a/b), or connecting to // itself (same app only - another domain is refused `connectto`, B0). // // Per variant the client allocates a BLOB AHB, locks it ONCE for the whole run, and hands it over // direct AHardwareBuffer_sendHandleToUnixSocket on the aux connection itself; // hop on a fresh socketpair whose other end travels over aux by SCM_RIGHTS (the server // then receives the handle from that socket). // Then the T0S rounds run on the control connection: the server imports the handle into Vulkan // and GL, its GPU reads what the client's CPU wrote through the held lock and writes fills the // client checks through the same held pointer. Rows: ROUTE--{,-vulkan,-gles}. // --------------------------------------------------------------------------- enum : uint32_t { MSG_RT_HELLO = 40, MSG_RT_HOP = 41, MSG_RT_SERVER = 42 }; struct RtHello { uint32_t variant; // 0 direct, 1 hop uint32_t rounds; uint64_t size; uint64_t regionBase; uint32_t seedBase; int32_t pad; char client[192]; char note[160]; }; struct RtServer { int32_t received; // 1 = the handle arrived and describes as a BLOB of `size` int32_t pad; char server[192]; char note[192]; }; static std::string selfContext() { std::string sec = readSmallFile("/proc/self/attr/current"); while (!sec.empty() && (sec.back() == '\n' || sec.back() == '\0')) sec.pop_back(); return fmt("%s uid=%d pid=%d", sec.c_str(), (int)getuid(), (int)getpid()); } static const char* kRtControlPrefixHex = "100000004d474c4308000e0007000800080000000000000f0c0000000000060008000400060000000400000010000000"; static const char* kRtAuxPrefixHex = "100000004d474c4308000c0007000800080000000000000f0c00000008000c000800070008000000000000010400000010000000"; static std::vector rtPairBindFrame(bool aux, const uint8_t nonce[16]) { const char* hex = aux ? kRtAuxPrefixHex : kRtControlPrefixHex; std::vector payload; for (size_t i = 0; hex[i] && hex[i + 1]; i += 2) { char b[3] = {hex[i], hex[i + 1], 0}; payload.push_back((uint8_t)strtoul(b, nullptr, 16)); } payload.insert(payload.end(), nonce, nonce + 16); std::vector out(8); const uint32_t magic = 0x464C474Du, len = (uint32_t)payload.size(); memcpy(out.data(), &magic, 4); memcpy(out.data() + 4, &len, 4); out.insert(out.end(), payload.begin(), payload.end()); return out; } // Reads one framed control message's header and payload; returns the payload length or -1. static int rtSkipFrame(int fd) { uint32_t hdr[2] = {0, 0}; if (!readAll(fd, hdr, sizeof(hdr))) return -1; if (hdr[0] != 0x464C474Du || hdr[1] > 4096) return -1; std::vector skip(hdr[1]); if (hdr[1] && !readAll(fd, skip.data(), hdr[1])) return -1; return (int)hdr[1]; } static int rtListen(const char* endpoint) { int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (s < 0) return -1; struct sockaddr_un a; memset(&a, 0, sizeof(a)); a.sun_family = AF_UNIX; socklen_t len; if (endpoint[0] == '@') { strncpy(a.sun_path + 1, endpoint + 1, sizeof(a.sun_path) - 2); len = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + strlen(endpoint + 1)); } else { unlink(endpoint); strncpy(a.sun_path, endpoint, sizeof(a.sun_path) - 1); len = (socklen_t)sizeof(a); } if (bind(s, (struct sockaddr*)&a, len) != 0 || listen(s, 16) != 0) { pr("route server: bind/listen %s failed errno=%d", endpoint, errno); close(s); return -1; } return s; } static int rtConnect(const char* endpoint) { int s = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if (s < 0) return -1; struct sockaddr_un a; memset(&a, 0, sizeof(a)); a.sun_family = AF_UNIX; socklen_t len; if (endpoint[0] == '@') { strncpy(a.sun_path + 1, endpoint + 1, sizeof(a.sun_path) - 2); len = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + strlen(endpoint + 1)); } else { strncpy(a.sun_path, endpoint, sizeof(a.sun_path) - 1); len = (socklen_t)sizeof(a); } if (connect(s, (struct sockaddr*)&a, len) != 0) { pr("route client: connect %s failed errno=%d", endpoint, errno); close(s); return -1; } return s; } static void rtServePair(VkCtx& c, GlCtx& g, bool glOk, int ctl, int aux) { setRecvTimeout(ctl, 60); setRecvTimeout(aux, 60); for (;;) { RtHello h{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(ctl, &tag, &h, sizeof(h), &got, nullptr) || tag == MSG_BYE) break; if (tag != MSG_RT_HELLO) { pr("route server: unexpected tag %u", tag); break; } h.client[sizeof(h.client) - 1] = 0; h.note[sizeof(h.note) - 1] = 0; gRegionBase = h.regionBase; const char* vname = h.variant ? "hop" : "direct"; pr("route server: client [%s] variant=%s size=%llu rounds=%u (%s)", h.client, vname, (unsigned long long)h.size, h.rounds, h.note); AHardwareBuffer* ahb = nullptr; int rc = -1; std::string how; if (h.variant == 0) { rc = AHardwareBuffer_recvHandleFromUnixSocket(aux, &ahb); how = fmt("recvHandleFromUnixSocket(aux) rc=%d errno=%d", rc, errno); } else { int hopFd = -1; uint32_t t2 = 0; if (!recvMsg(aux, &t2, nullptr, 0, &got, &hopFd) || t2 != MSG_RT_HOP || hopFd < 0) { how = fmt("hop socket did not arrive over aux (tag=%u fd=%d errno=%d)", t2, hopFd, errno); } else { rc = AHardwareBuffer_recvHandleFromUnixSocket(hopFd, &ahb); how = fmt("hop %s; recvHandleFromUnixSocket(hop) rc=%d errno=%d", describeFd(hopFd).c_str(), rc, errno); close(hopFd); } } RtServer sv{}; snprintf(sv.server, sizeof(sv.server), "%s", selfContext().c_str()); if (rc == 0 && ahb) { AHardwareBuffer_Desc d{}; AHardwareBuffer_describe(ahb, &d); sv.received = (d.format == AHARDWAREBUFFER_FORMAT_BLOB && d.width == h.size) ? 1 : 0; snprintf(sv.note, sizeof(sv.note), "%s | desc w=%u fmt=0x%x usage=0x%llx", how.c_str(), d.width, d.format, (unsigned long long)d.usage); } else { snprintf(sv.note, sizeof(sv.note), "%s", how.c_str()); } pr("route server: %s", sv.note); sendMsg(ctl, MSG_RT_SERVER, &sv, sizeof(sv), -1); const std::string row = fmt("ROUTE-%s-%llu", vname, (unsigned long long)h.size); if (!sv.received) { record(row.c_str(), "FAIL", fmt("client [%s] -> server [%s]: %s", h.client, sv.server, sv.note)); if (ahb) AHardwareBuffer_release(ahb); break; } pr("route server: client [%s] -> server [%s]", h.client, sv.server); t0sServe(c, g, glOk, ctl, ahb, h.size, h.rounds, row.c_str(), -1); AHardwareBuffer_release(ahb); } } static int routeServe(const char* endpoint) { setvbuf(stdout, nullptr, _IOLBF, 0); gRole = "route-server"; signal(SIGPIPE, SIG_IGN); pr("extmem route server: %s", selfContext().c_str()); VkCtx c; const bool vkOk = vkCtxInit(c, false); GlCtx g; const bool glOk = glCtxInit(g); pr("extmem route server: vulkan=%d gl=%d", (int)vkOk, (int)glOk); if (!vkOk) return 1; int ls = rtListen(endpoint); if (ls < 0) return 2; // The service's readiness line (srv.sh waits for it) - the real supervisor says the same. pr("extmem route server: listening on %s", endpoint); int pendingCtl = -1, pendingAux = -1; for (;;) { int s = accept4(ls, nullptr, nullptr, SOCK_CLOEXEC); if (s < 0) { if (errno == EINTR) continue; pr("route server: accept errno=%d", errno); return 3; } setRecvTimeout(s, 10); const int n = rtSkipFrame(s); if (n == 64) { if (pendingCtl >= 0) close(pendingCtl); pendingCtl = s; } else if (n == 68) { if (pendingAux >= 0) close(pendingAux); pendingAux = s; } else { pr("route server: connection without a PairBind (frame=%d) closed", n); close(s); continue; } if (pendingCtl >= 0 && pendingAux >= 0) { pr("route server: paired control=%d aux=%d", pendingCtl, pendingAux); rtServePair(c, g, glOk, pendingCtl, pendingAux); close(pendingCtl); close(pendingAux); pendingCtl = pendingAux = -1; gResults.clear(); } } } static int rtClientVariant(int ctl, int aux, uint32_t variant, uint64_t size, uint32_t rounds, bool atEnd) { AHardwareBuffer_Desc desc{}; desc.width = (uint32_t)size; desc.height = 1; desc.layers = 1; desc.format = AHARDWAREBUFFER_FORMAT_BLOB; desc.usage = AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_GPU_DATA_BUFFER; AHardwareBuffer* ahb = nullptr; const int arc = AHardwareBuffer_allocate(&desc, &ahb); const char* vname = variant ? "hop" : "direct"; const std::string row = fmt("ROUTE-%s-%llu-client", vname, (unsigned long long)size); if (arc != 0 || !ahb) { record(row.c_str(), "FAIL", fmt("AHardwareBuffer_allocate rc=%d", arc)); return 3; } void* p = nullptr; const int lockRc = AHardwareBuffer_lock( ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, -1, nullptr, &p); if (lockRc != 0 || !p) { record(row.c_str(), "FAIL", fmt("held lock rc=%d", lockRc)); AHardwareBuffer_release(ahb); return 4; } memset(p, 0, (size_t)size); gRegionBase = atEnd ? size - kRegionCount * kRegion : 0; RtHello h{}; h.variant = variant; h.rounds = rounds; h.size = size; h.regionBase = gRegionBase; h.seedBase = 0x3B000001u + variant * 0x100u; snprintf(h.client, sizeof(h.client), "%s", selfContext().c_str()); snprintf(h.note, sizeof(h.note), "held lock ptr=%p", p); int sendRc = -1; if (!sendMsg(ctl, MSG_RT_HELLO, &h, sizeof(h), -1)) { record(row.c_str(), "FAIL", fmt("hello send errno=%d", errno)); } else if (variant == 0) { sendRc = AHardwareBuffer_sendHandleToUnixSocket(ahb, aux); } else { int sp[2]; if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp) == 0) { sendRc = AHardwareBuffer_sendHandleToUnixSocket(ahb, sp[0]); close(sp[0]); if (sendRc == 0 && !sendMsg(aux, MSG_RT_HOP, nullptr, 0, sp[1])) sendRc = -errno; close(sp[1]); } } RtServer sv{}; uint32_t tag = 0; size_t got = 0; const bool haveServer = recvMsg(ctl, &tag, &sv, sizeof(sv), &got, nullptr) && tag == MSG_RT_SERVER; sv.server[sizeof(sv.server) - 1] = 0; sv.note[sizeof(sv.note) - 1] = 0; int rrc = 10; if (sendRc != 0 || !haveServer || !sv.received) { record(row.c_str(), "FAIL", fmt("sendRc=%d serverAnswered=%d received=%d server [%s]: %s", sendRc, (int)haveServer, (int)sv.received, sv.server, sv.note)); } else { rrc = t0sClientRounds(ctl, ahb, p, lockRc, rounds, h.seedBase); record(row.c_str(), rrc == 0 ? "OK" : "FAIL", fmt("client [%s] -> server [%s] | %s | rounds rc=%d (the server's rows carry the GPU verdict)", h.client, sv.server, sv.note, rrc)); } AHardwareBuffer_release(ahb); return rrc; } static int routeClient(const char* endpoint, uint64_t size, uint32_t rounds, bool atEnd) { gRole = "route-client"; signal(SIGPIPE, SIG_IGN); int ctl = -1, aux = -1; const char* env = getenv("MOBILEGL_IPC_CONTROL"); bool paired = false; if (endpoint && *endpoint) { ctl = rtConnect(endpoint); aux = rtConnect(endpoint); } else if (env && !strncmp(env, "fd:", 3) && sscanf(env + 3, "%d,%d", &ctl, &aux) == 2) { const char* pe = getenv("MOBILEGL_IPC_FD_PAIRED"); paired = pe && !strcmp(pe, "1"); } else { pr("route client: no endpoint and MOBILEGL_IPC_CONTROL is not fd:, (%s)", env ? env : "unset"); return 2; } if (ctl < 0 || aux < 0) return 2; pr("route client: %s control=%d aux=%d %s aux=%s", selfContext().c_str(), ctl, aux, paired ? "paired at hand-off" : "presenting its own PairBind pair", describeFd(aux).c_str()); if (!paired) { uint8_t nonce[16]; FILE* r = fopen("/dev/urandom", "rb"); if (!r || fread(nonce, 1, 16, r) != 16) memset(nonce, 0x5A, 16); if (r) fclose(r); const std::vector fc = rtPairBindFrame(false, nonce), fa = rtPairBindFrame(true, nonce); if (!writeAll(ctl, fc.data(), fc.size()) || !writeAll(aux, fa.data(), fa.size())) { pr("route client: PairBind write failed errno=%d", errno); return 3; } } setRecvTimeout(ctl, 60); int worst = 0; for (uint32_t v = 0; v < 2; ++v) { const int rc = rtClientVariant(ctl, aux, v, size, rounds, atEnd); if (rc != 0) worst = rc; } sendMsg(ctl, MSG_BYE, nullptr, 0, -1); printSummary(); close(ctl); close(aux); return worst; } #else // !PROBE_HAVE_AHB static int childT0(int) { pr("T0 is Android-only"); return 1; } static void runT0Parent(VkCtx&, GlCtx&, bool, uint64_t) { record("T0-ahb-blob-transfer", "SKIP", "AHardwareBuffer is Android-only; host build cannot run T0"); } static int childT0Sustained(int) { pr("T0S is Android-only"); return 1; } static void runT0SustainedParent(VkCtx&, GlCtx&, bool, uint64_t, uint32_t) { record("T0S-sustained-lock", "SKIP", "AHardwareBuffer is Android-only; host build cannot run T0S"); } static int routeServe(const char*) { pr("the AHB route server is Android-only"); return 1; } static int routeClient(const char*, uint64_t, uint32_t, bool) { pr("the AHB route client is Android-only"); return 1; } #endif // PROBE_HAVE_AHB // --------------------------------------------------------------------------- // T3: VK_EXT_external_memory_host over a memfd-backed mapping // --------------------------------------------------------------------------- // Reserves an alignment-corrected window and places `fd` inside it. Returns the // aligned pointer, or nullptr; `*reserveOut` must be munmap'ed with // `mapSize + align` bytes. static void* mapAlignedFd(int fd, uint64_t mapSize, uint64_t align, void** reserveOut, std::string* fail) { *reserveOut = mmap(nullptr, (size_t)(mapSize + align), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (*reserveOut == MAP_FAILED) { *reserveOut = nullptr; *fail = fmt("reserve mmap errno=%d(%s)", errno, strerror(errno)); return nullptr; } uintptr_t base = ((uintptr_t)*reserveOut + align - 1) & ~(uintptr_t)(align - 1); void* host = mmap((void*)base, (size_t)mapSize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, 0); if (host == MAP_FAILED) { *fail = fmt("mmap(fd, MAP_FIXED) errno=%d(%s)", errno, strerror(errno)); munmap(*reserveOut, (size_t)(mapSize + align)); *reserveOut = nullptr; return nullptr; } return host; } // Imports `host` as VkDeviceMemory and binds a buffer to it. struct HostImport { VkBuffer buf = VK_NULL_HANDLE; VkDeviceMemory mem = VK_NULL_HANDLE; void* mapped = nullptr; int typeIdx = -1; VkResult hostPtrProps = VK_NOT_READY; VkResult createResult = VK_NOT_READY; VkResult allocResult = VK_NOT_READY; VkResult bindResult = VK_NOT_READY; VkResult mapResult = VK_NOT_READY; uint32_t bits = 0; std::string fail; }; static bool importHostPointer(VkCtx& c, void* host, uint64_t mapSize, HostImport& o) { VkMemoryHostPointerPropertiesEXT hp{}; hp.sType = VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT; o.hostPtrProps = c.pGetHostPtrProps(c.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &hp); if (o.hostPtrProps != VK_SUCCESS) { o.fail = "vkGetMemoryHostPointerPropertiesEXT=" + vkStr(o.hostPtrProps); return false; } VkExternalMemoryBufferCreateInfo ext{}; ext.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; ext.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; VkBufferCreateInfo bci{}; bci.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bci.pNext = &ext; bci.size = mapSize; bci.usage = kProbeBufferUsage; o.createResult = vkCreateBuffer(c.device, &bci, nullptr, &o.buf); VkMemoryRequirements req{}; if (o.createResult == VK_SUCCESS) { vkGetBufferMemoryRequirements(c.device, o.buf, &req); } else { o.buf = VK_NULL_HANDLE; req.memoryTypeBits = 0xFFFFFFFFu; } o.bits = hp.memoryTypeBits & req.memoryTypeBits; o.typeIdx = pickMemType(c.memProps, o.bits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); if (o.typeIdx < 0) o.typeIdx = pickMemType(c.memProps, o.bits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); if (o.typeIdx < 0) { o.fail = fmt("no host-visible memory type in hostPtrBits=0x%x & reqBits=0x%x", hp.memoryTypeBits, req.memoryTypeBits); return false; } VkImportMemoryHostPointerInfoEXT imp{}; imp.sType = VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT; imp.handleType = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; imp.pHostPointer = host; VkMemoryAllocateInfo mai{}; mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.pNext = &imp; mai.allocationSize = mapSize; mai.memoryTypeIndex = (uint32_t)o.typeIdx; o.allocResult = vkAllocateMemory(c.device, &mai, nullptr, &o.mem); if (o.allocResult != VK_SUCCESS) { o.mem = VK_NULL_HANDLE; o.fail = fmt("vkAllocateMemory(import host ptr)=%s type=%d bits=0x%x", vkStr(o.allocResult).c_str(), o.typeIdx, o.bits); return false; } o.bindResult = (o.buf != VK_NULL_HANDLE) ? vkBindBufferMemory(c.device, o.buf, o.mem, 0) : VK_SUCCESS; o.mapResult = vkMapMemory(c.device, o.mem, 0, VK_WHOLE_SIZE, 0, &o.mapped); if (o.mapResult != VK_SUCCESS) { o.mapped = nullptr; o.fail = "vkMapMemory=" + vkStr(o.mapResult); return false; } return true; } static void releaseHostImport(VkCtx& c, HostImport& o) { if (o.mapped) vkUnmapMemory(c.device, o.mem); if (o.mem) vkFreeMemory(c.device, o.mem, nullptr); if (o.buf) vkDestroyBuffer(c.device, o.buf, nullptr); o.mapped = nullptr; o.mem = VK_NULL_HANDLE; o.buf = VK_NULL_HANDLE; } static int childT3(int sock) { setRecvTimeout(sock, 30); T3Offer offer{}; uint32_t tag = 0; size_t got = 0; int fd = -1; if (!recvMsg(sock, &tag, &offer, sizeof(offer), &got, &fd) || tag != MSG_T3_OFFER) return 2; T3Result res{}; res.mismatch = -3; res.gpuMismatch = -2; if (fd < 0) { snprintf(res.note, sizeof(res.note), "no fd"); sendMsg(sock, MSG_T3_RESULT, &res, sizeof(res), -1); return 3; } snprintf(res.note, sizeof(res.note), "%s", describeFd(fd).c_str()); void* p = mmap(nullptr, (size_t)offer.size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p == MAP_FAILED) { res.mmapOk = 0; res.mmapErrno = errno; } else { res.mmapOk = 1; res.mismatch = checkRegion(p, REG_A, offer.seedA); if (offer.gpuRan) res.gpuMismatch = checkFillWord(p, REG_E, offer.gpuWord); writeRegion(p, REG_B, offer.seedB); msync(p, (size_t)offer.size, MS_SYNC); munmap(p, (size_t)offer.size); } sendMsg(sock, MSG_T3_RESULT, &res, sizeof(res), -1); close(fd); return 0; } // The direction that makes T3 a tier: the CLIENT allocates the memory and the // SERVER imports the client's host pointer. The child is the client here. static int childT3Client(int sock) { setRecvTimeout(sock, 30); T3cRequest rq{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(sock, &tag, &rq, sizeof(rq), &got, nullptr) || tag != MSG_T3C_REQUEST) return 2; T3cReady ready{}; ready.size = rq.size; int memfd = memfd_create("extmem_probe_client", 0); if (memfd < 0) { ready.err = errno; snprintf(ready.note, sizeof(ready.note), "memfd_create errno=%d(%s)", errno, strerror(errno)); sendMsg(sock, MSG_T3C_READY, &ready, sizeof(ready), -1); return 3; } if (ftruncate(memfd, (off_t)rq.size) != 0) { ready.err = errno; snprintf(ready.note, sizeof(ready.note), "ftruncate errno=%d(%s)", errno, strerror(errno)); sendMsg(sock, MSG_T3C_READY, &ready, sizeof(ready), -1); close(memfd); return 4; } void* p = mmap(nullptr, (size_t)rq.size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0); if (p == MAP_FAILED) { ready.err = errno; snprintf(ready.note, sizeof(ready.note), "mmap errno=%d(%s)", errno, strerror(errno)); sendMsg(sock, MSG_T3C_READY, &ready, sizeof(ready), -1); close(memfd); return 5; } memset(p, 0, (size_t)rq.size); writeRegion(p, REG_A, rq.seedA); ready.ok = 1; snprintf(ready.note, sizeof(ready.note), "client memfd %s", describeFd(memfd).c_str()); if (!sendMsg(sock, MSG_T3C_READY, &ready, sizeof(ready), memfd)) { munmap(p, (size_t)rq.size); close(memfd); return 6; } T3cVerify ver{}; T3cResult res{}; res.mismatchB = res.mismatchE = -2; if (!recvMsg(sock, &tag, &ver, sizeof(ver), &got, nullptr) || tag != MSG_T3C_VERIFY) { munmap(p, (size_t)rq.size); close(memfd); return 7; } if (ver.mask & 1) res.mismatchB = checkRegion(p, REG_B, ver.seedB); if (ver.mask & 2) res.mismatchE = checkFillWord(p, REG_E, ver.gpuWord); snprintf(res.note, sizeof(res.note), "mask=0x%x", ver.mask); sendMsg(sock, MSG_T3C_RESULT, &res, sizeof(res), -1); munmap(p, (size_t)rq.size); close(memfd); return 0; } static void runT3ClientAllocParent(VkCtx& c, uint64_t size) { const char* route = "T3-client-memfd-server-import"; if (!c.hasExtMemHost || !c.pGetHostPtrProps) { record(route, "UNSUPPORTED", "VK_EXT_external_memory_host absent"); return; } uint64_t align = c.minImportedHostPointerAlignment ? c.minImportedHostPointerAlignment : 4096; uint64_t mapSize = (size + align - 1) & ~(align - 1); const uint32_t seedA = 0x3C3C0001u, seedB = 0x3C3C0002u, gpuWord = 0x3C3C1234u; int sock = -1; pid_t pid = spawnChild("t3c", &sock); if (pid < 0) { record(route, "FAIL", "spawnChild failed"); return; } T3cRequest rq{}; rq.size = mapSize; rq.seedA = seedA; if (!sendMsg(sock, MSG_T3C_REQUEST, &rq, sizeof(rq), -1)) { record(route, "FAIL", fmt("sendMsg(request) errno=%d", errno)); close(sock); reapChild(pid); return; } T3cReady ready{}; uint32_t tag = 0; size_t got = 0; int fd = -1; if (!recvMsg(sock, &tag, &ready, sizeof(ready), &got, &fd) || tag != MSG_T3C_READY) { record(route, "FAIL", fmt("no ready reply errno=%d %s", errno, reapChild(pid).c_str())); close(sock); return; } if (!ready.ok || fd < 0) { record(route, "FAIL", fmt("client could not allocate: %s (fd=%d)", ready.note, fd)); if (fd >= 0) close(fd); sendMsg(sock, MSG_BYE, nullptr, 0, -1); reapChild(pid); close(sock); return; } pr("T3c server received the client's memfd: %s", describeFd(fd).c_str()); void* reserve = nullptr; std::string mapFail; void* host = mapAlignedFd(fd, mapSize, align, &reserve, &mapFail); if (!host) { record(route, "FAIL", "server could not map the client's memfd: " + mapFail); close(fd); sendMsg(sock, MSG_BYE, nullptr, 0, -1); reapChild(pid); close(sock); return; } HostImport hi; bool imported = importHostPointer(c, host, mapSize, hi); int64_t cmpA = -3, gpuFillSeen = -3; GpuTouch gt; if (imported) { cmpA = checkRegion(hi.mapped, REG_A, seedA); // server reads what the client wrote writeRegion(hi.mapped, REG_B, seedB); // server writes back gt = gpuTouch(c, hi.buf, REG_A, seedA, REG_E, gpuWord); gpuFillSeen = gt.ran ? checkFillWord(hi.mapped, REG_E, gpuWord) : -3; } T3cVerify ver{}; ver.seedB = seedB; ver.gpuWord = gpuWord; ver.mask = (imported ? 1u : 0u) | ((gt.ran && gpuFillSeen == -1) ? 2u : 0u); T3cResult res{}; res.mismatchB = res.mismatchE = -3; bool gotVerify = false; if (sendMsg(sock, MSG_T3C_VERIFY, &ver, sizeof(ver), -1) && recvMsg(sock, &tag, &res, sizeof(res), &got, nullptr) && tag == MSG_T3C_RESULT) { gotVerify = true; } std::vector legs; { Leg l; l.name = "server-import"; l.decisive = true; l.attempted = imported; l.readOk = imported && cmpA == -1; l.writeOk = gotVerify && (ver.mask & 1) && res.mismatchB == -1; if (!imported) l.fail = hi.fail; else if (!l.readOk) l.fail = fmt("server could not read the client's payload (cmp=%lld)", (long long)cmpA); else if (!l.writeOk) l.fail = fmt("server write not visible to the client (back=%lld)", (long long)res.mismatchB); legs.push_back(l); } { Leg l; l.name = "gpu"; l.decisive = true; l.attempted = gt.ran; l.readOk = gt.readMismatch == -1; l.writeOk = gotVerify && (ver.mask & 2) && res.mismatchE == -1; if (!gt.ran) l.fail = "GPU touch did not run: " + gt.fail; else if (!l.readOk) l.fail = fmt("GPU read of the client's memory mismatched at %lld", (long long)gt.readMismatch); else if (!l.writeOk) l.fail = fmt("GPU write not visible to the client (serverMap=%lld clientMap=%lld)", (long long)gpuFillSeen, (long long)res.mismatchE); legs.push_back(l); } std::string why; const char* status = legVerdict(legs, &why); std::string detail = fmt("%s | client allocates, server imports: align=%llu size=%llu hostPtrProps=%s create=%s alloc=%s bind=%s " "map=%s type=%d bits=0x%x | serverReadOfClient=%lld clientReadOfServer=%lld gpuRead=%lld " "gpuFill(server=%lld,client=%lld) | %s [%s] ", legTrace(legs).c_str(), (unsigned long long)align, (unsigned long long)mapSize, vkStr(hi.hostPtrProps).c_str(), vkStr(hi.createResult).c_str(), vkStr(hi.allocResult).c_str(), vkStr(hi.bindResult).c_str(), vkStr(hi.mapResult).c_str(), hi.typeIdx, hi.bits, (long long)cmpA, (long long)res.mismatchB, (long long)gt.readMismatch, (long long)gpuFillSeen, (long long)res.mismatchE, why.c_str(), ready.note); releaseHostImport(c, hi); munmap(host, (size_t)mapSize); if (reserve) munmap(reserve, (size_t)(mapSize + align)); close(fd); sendMsg(sock, MSG_BYE, nullptr, 0, -1); detail += reapChild(pid); close(sock); record(route, status, detail); } static void runT3Parent(VkCtx& c, uint64_t size) { if (!c.hasExtMemHost || !c.pGetHostPtrProps) { record("T3-external-memory-host", "UNSUPPORTED", "VK_EXT_external_memory_host absent"); record("T3-memfd-cross-process", "UNSUPPORTED", "VK_EXT_external_memory_host absent"); return; } uint64_t align = c.minImportedHostPointerAlignment ? c.minImportedHostPointerAlignment : 4096; uint64_t mapSize = (size + align - 1) & ~(align - 1); int memfd = memfd_create("extmem_probe", 0); if (memfd < 0) { record("T3-external-memory-host", "FAIL", fmt("memfd_create errno=%d", errno)); return; } if (ftruncate(memfd, (off_t)mapSize) != 0) { record("T3-external-memory-host", "FAIL", fmt("ftruncate errno=%d", errno)); close(memfd); return; } void* reserve = nullptr; std::string mapFail; void* host = mapAlignedFd(memfd, mapSize, align, &reserve, &mapFail); if (!host) { record("T3-external-memory-host", "FAIL", mapFail); close(memfd); return; } const uint32_t seedA = 0x33330001u, seedB = 0x33330002u, gpuWord = 0x33331234u; memset(host, 0, (size_t)mapSize); writeRegion(host, REG_A, seedA); HostImport hi; bool imported = importHostPointer(c, host, mapSize, hi); int64_t cmpA = -3, gpuFillSeen = -3; GpuTouch gt; if (imported) { cmpA = checkRegion(hi.mapped, REG_A, seedA); gt = gpuTouch(c, hi.buf, REG_A, seedA, REG_E, gpuWord); gpuFillSeen = gt.ran ? checkFillWord(host, REG_E, gpuWord) : -3; } { std::vector legs; Leg l; l.name = "import-map"; l.decisive = true; l.attempted = imported; // one process on both ends here, so the "write back" direction is the // imported mapping seeing the original mmap's bytes l.readOk = imported && cmpA == -1; l.writeOk = imported && cmpA == -1; l.fail = imported ? (cmpA == -1 ? "" : fmt("payload mismatch at %lld", (long long)cmpA)) : hi.fail; legs.push_back(l); Leg gl; gl.name = "gpu"; gl.decisive = true; gl.attempted = gt.ran; gl.readOk = gt.readMismatch == -1; gl.writeOk = gpuFillSeen == -1; if (!gt.ran) gl.fail = "GPU touch did not run: " + gt.fail; else if (!gl.readOk) gl.fail = fmt("GPU read mismatched at %lld", (long long)gt.readMismatch); else if (!gl.writeOk) gl.fail = fmt("GPU write not visible through the host mapping (at %lld)", (long long)gpuFillSeen); legs.push_back(gl); std::string why; record("T3-external-memory-host", legVerdict(legs, &why), fmt("%s | align=%llu type=%d bits=0x%x hostPtrProps=%s alloc=%s bind=%s map=%s mismatch=%lld " "gpuRead=%lld gpuFill=%lld %s", legTrace(legs).c_str(), (unsigned long long)align, hi.typeIdx, hi.bits, vkStr(hi.hostPtrProps).c_str(), vkStr(hi.allocResult).c_str(), vkStr(hi.bindResult).c_str(), vkStr(hi.mapResult).c_str(), (long long)cmpA, (long long)gt.readMismatch, (long long)gpuFillSeen, why.c_str())); } // the same memfd handed to another process int sock = -1; pid_t pid = spawnChild("t3", &sock); if (pid < 0) { record("T3-memfd-cross-process", "FAIL", "spawnChild failed"); } else { T3Offer off{}; off.size = mapSize; off.seedA = seedA; off.seedB = seedB; off.gpuWord = gpuWord; off.gpuRan = (gt.ran && gpuFillSeen == -1) ? 1u : 0u; if (!sendMsg(sock, MSG_T3_OFFER, &off, sizeof(off), memfd)) { record("T3-memfd-cross-process", "FAIL", fmt("sendMsg errno=%d", errno)); } else { T3Result res{}; uint32_t tag = 0; size_t got = 0; if (!recvMsg(sock, &tag, &res, sizeof(res), &got, nullptr) || tag != MSG_T3_RESULT) { record("T3-memfd-cross-process", "FAIL", fmt("no reply errno=%d", errno)); } else { int64_t back = res.mmapOk ? checkRegion(host, REG_B, seedB) : -3; std::vector legs; Leg l; l.name = "peer-mmap"; l.decisive = true; l.attempted = res.mmapOk != 0; l.readOk = res.mmapOk && res.mismatch == -1 && (!off.gpuRan || res.gpuMismatch == -1); l.writeOk = res.mmapOk && back == -1; if (!l.attempted) l.fail = fmt("peer mmap failed errno=%d(%s)", res.mmapErrno, strerror(res.mmapErrno)); else if (!l.readOk) l.fail = fmt("peer could not read (cmp=%lld gpuCmp=%lld)", (long long)res.mismatch, (long long)res.gpuMismatch); else if (!l.writeOk) l.fail = fmt("peer write not visible here (back=%lld)", (long long)back); legs.push_back(l); std::string why; record("T3-memfd-cross-process", legVerdict(legs, &why), fmt("%s | child mmap=%d errno=%d cmp=%lld gpuCmp=%lld writeback=%lld %s [%s]", legTrace(legs).c_str(), res.mmapOk, res.mmapErrno, (long long)res.mismatch, (long long)res.gpuMismatch, (long long)back, why.c_str(), res.note)); } } sendMsg(sock, MSG_BYE, nullptr, 0, -1); reapChild(pid); close(sock); } releaseHostImport(c, hi); munmap(host, (size_t)mapSize); if (reserve) munmap(reserve, (size_t)(mapSize + align)); close(memfd); } // --------------------------------------------------------------------------- // T4: the image the peer allocated OUTSIDE Android // // The {Owner=Platform, Storage=OfferedImage} cell of an Anland-style Wayland host: a // glibc peer in a Droidspaces container creates the GBM buffer on the device the // container owns and offers it here as a dma-buf, and this side -- the one holding the // live EGL/GLES context -- has to be able to READ it and to DRAW into it. // // The topology is the reverse of every other leg on purpose. The peer is not a re-exec // of /proc/self/exe: nothing here needs a second driver instance (the only reason the // other legs exec is that driver threads and device state do not survive fork), and the // peer has to be glibc to create a GBM buffer at all. So this side LISTENS on an // abstract unix socket and the peer dials it, and because a Droidspaces container runs // with net_mode=host, one network namespace holds both ends. The peer is therefore not // this process's child: there is nothing to reapChild(), and the frozen protocol has no // MSG_BYE for T4, so the socket close is the whole teardown. // // Two failures must never be reported the same way: // capability (fourcc, modifier) is not in the driver's own eglQueryDmaBufFormatsEXT / // eglQueryDmaBufModifiersEXT answer. Refusing a pair the driver never // advertised is the driver being right, and no import attempt can change it. // driver bug the pair IS advertised and the import still fails. // The capability question is asked first and its answer travels in the row. // // Both paths are read at (0,0) AND at the far corner: a wrong stride or offset shifts the // corner texel, which a one-pixel read cannot see. // // T4 is two rows over that one connection, and they answer different halves: // T4-image-import the container's dma-buf, imported here. Gated on // EGL_EXT_image_dma_buf_import: a driver without it gets a capability // answer recorded before the ladder is entered, never an import // failure dressed up as a driver bug. // T4-ahb-image an AHardwareBuffer minted on THIS side and imported in the container, // which is the direction that still answers on such a driver. // // GL hands back channels (R,G,B,A) where the peer's seed is memory bytes, and the fourcc the // peer allocates -- GBM's ARGB8888 -- is B,G,R,A in memory. A working import therefore reads // back the seed's bytes in CHANNEL order rather than verbatim, so the row carries both // readings: the verbatim one the protocol specifies, and the channel-order one beside it, so // the byte-order convention can never be mistaken for a broken import (and vice versa). // // The host build compiles this section, but its runT4 is a one-line SKIP row: the only way // this side can be dialed is rtListen(), which lives inside the AHardwareBuffer guard with // the rest of the T0/route block, and a host build has neither a listener nor a container // peer. T4 itself needs no AHardwareBuffer -- PROBE_HAVE_AHB is simply the macro that // carries rtListen() -- so the guard is spelled out here with the stub beside it rather // than left to look like an Android-only leg. // --------------------------------------------------------------------------- #if PROBE_HAVE_AHB // The peer spells the same literal, so neither side depends on the other's byte order. static const uint32_t kT4Magic = 0x5434474DU; enum : uint32_t { MSG_T4_OFFER = 50, MSG_T4_READ = 51, MSG_T4_WROTE = 52, MSG_T4_ACK = 53, MSG_T4_RESULT = 54, // The Android-minted half of T4, appended so that no existing tag moves. The // AHardwareBuffer handle travels beside the offer exactly the way the T0 legs // send theirs -- AHardwareBuffer_sendHandleToUnixSocket into one end of a // socketpair, and that end's peer over SCM_RIGHTS with the message. MSG_T4_AHB_OFFER = 55, MSG_T4_AHB_ACK = 56, }; // Byte for byte the peer's struct in peer/t4_gbm_peer.c: every field is fixed width, so the // two compilers cannot disagree about padding even though only one of them is bionic. struct T4Layout { uint32_t magic, version, width, height, fourcc, stride, offset, reserved; uint64_t modifier; // UINT64_MAX = the peer has none, so rung 0 has nothing to name uint8_t seed[4]; // what the peer filled the image with, in the buffer's memory order char peer[128]; char note[128]; }; struct T4Read { int32_t texOk, rbOk, rung, pad; uint8_t texWord[4], texCorner[4], rbWord[4], rbCorner[4]; char importNote[160]; }; // path 0 = renderbuffer, 1 = texture. `color` is the byte sequence the peer should find in // the buffer, so it is in memory order and not in GL channel order. struct T4Wrote { int32_t path; uint8_t color[4]; }; struct T4Ack { int32_t ok; uint8_t observed[4]; char note[128]; }; // MSG_T4_AHB_OFFER: the buffer THIS side minted, described so the container can // import it without guessing any part of the layout. The fourcc is the DRM name // whose memory order is the order AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM names -- // DRM_FORMAT_ABGR8888, fourcc_code('A','B','2','4') = 0x34324241, whose // little-endian [31:0] A:B:G:R layout puts R,G,B,A in bytes 0..3, the same pairing // gralloc itself uses for HAL_PIXEL_FORMAT_RGBA_8888 -- so the bytes the peer // compares are the bytes glClearColor() produced, and no channel-renaming step // sits between the two sides where a convention could pass for a working image. // The stride is the stride AHardwareBuffer_describe() reported, never width*4. struct T4AhbOffer { uint32_t magic, version, width, height, stride, fourcc; uint8_t seed[4]; // what this side cleared into the buffer, in memory order char note[128]; }; // MSG_T4_AHB_ACK: what the container's GBM import made of the fd, what both pixels // of its mapping held BEFORE it wrote anything, and the pattern it then wrote over // the whole mapping. `matched` is the peer's own comparison -- bit0 for (0,0), // bit1 for the far corner -- and this side recomputes it from the two pixel fields // instead of trusting it, so a disagreement between the bits and the bytes is // visible in the row. struct T4AhbAck { int32_t imported; // the peer's gbm_bo_import returned a bo int32_t matched; // 1 = (0,0) held the announced seed, 2 = the far corner did uint32_t stride; // the stride the peer's own mapping reports uint8_t observed[4]; uint8_t observedCorner[4]; uint8_t pattern[4]; // zero when the peer could not write anything char note[128]; }; // The dma-buf import entry points stay local to T4 rather than joining GlCtx: T1-gles asks // the same question through GL_EXT_memory_object, and widening GlCtx would move that leg // for no reason. struct T4Egl { PFNEGLCREATEIMAGEKHRPROC pCreateImage = nullptr; PFNEGLDESTROYIMAGEKHRPROC pDestroyImage = nullptr; PFNEGLQUERYDMABUFFORMATSEXTPROC pFormats = nullptr; PFNEGLQUERYDMABUFMODIFIERSEXTPROC pModifiers = nullptr; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture = nullptr; PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC pTargetRb = nullptr; }; // The two-pixel read only means anything byte-exact: a wrong stride or offset shows up as // differing bytes, which a decimal rendering would hide. static std::string t4Hex4(const uint8_t* b) { return fmt("%02x%02x%02x%02x", b[0], b[1], b[2], b[3]); } // glClientWaitSync answers in its own vocabulary, which glErrStr does not cover. static std::string t4WaitStr(GLenum w) { switch (w) { case GL_ALREADY_SIGNALED: return "GL_ALREADY_SIGNALED"; case GL_CONDITION_SATISFIED: return "GL_CONDITION_SATISFIED"; case GL_TIMEOUT_EXPIRED: return "GL_TIMEOUT_EXPIRED"; case GL_WAIT_FAILED: return "GL_WAIT_FAILED"; default: return fmt("GL(0x%04x)", (unsigned)w); } } // The driver's own answer to "do you take this (fourcc, modifier) pair", asked before any // import so that a pair the driver never advertised can never be reported as an import // failure. `asked` says whether the answer means anything at all. struct T4Cap { bool asked = false; bool formatListed = false; bool listed = false; int formats = 0; int modifiers = 0; std::string note; }; static T4Cap t4Capability(EGLDisplay dpy, const T4Egl& e, uint32_t fourcc, uint64_t modifier) { T4Cap cap; if (!e.pFormats || !e.pModifiers) { cap.note = "EGL_EXT_image_dma_buf_import_modifiers absent, or its two entry points are: the driver " "publishes no (fourcc, modifier) list, so an import failure cannot be attributed either way"; return cap; } EGLint total = 0; if (!e.pFormats(dpy, 0, nullptr, &total) || total <= 0) { cap.note = fmt("eglQueryDmaBufFormatsEXT(list) -> 0x%04x num=%d", (unsigned)eglGetError(), (int)total); return cap; } std::vector formats((size_t)total); EGLint filled = 0; if (!e.pFormats(dpy, total, formats.data(), &filled)) { cap.note = fmt("eglQueryDmaBufFormatsEXT(%d) -> 0x%04x", (int)total, (unsigned)eglGetError()); return cap; } cap.asked = true; cap.formats = (int)filled; for (EGLint i = 0; i < filled; ++i) { if ((uint32_t)formats[(size_t)i] == fourcc) cap.formatListed = true; } if (!cap.formatListed) { cap.note = fmt("fourcc=0x%08x is not among the %d formats the driver lists: this is a capability answer, " "not an import failure", fourcc, (int)filled); return cap; } EGLint nmods = 0; if (!e.pModifiers(dpy, (EGLint)fourcc, 0, nullptr, nullptr, &nmods)) { cap.note = fmt("fourcc=0x%08x is listed, eglQueryDmaBufModifiersEXT(list) -> 0x%04x", fourcc, (unsigned)eglGetError()); return cap; } cap.modifiers = (int)nmods; if (modifier == UINT64_MAX) { // Nothing to look up: the implicit modifier is the only one rung 1 can name, and it // is legal for every format the driver lists. cap.listed = true; cap.note = fmt("fourcc=0x%08x is listed; the driver lists %d explicit modifier(s) but the peer offered " "none, so rung 1 is the only rung that can be spelled", fourcc, (int)nmods); return cap; } if (nmods <= 0) { cap.note = fmt("fourcc=0x%08x is listed with no explicit modifiers and the peer offered modifier=0x%016llx: " "the driver never advertised that pair", fourcc, (unsigned long long)modifier); return cap; } std::vector mods((size_t)nmods); EGLint modFilled = 0; if (!e.pModifiers(dpy, (EGLint)fourcc, nmods, mods.data(), nullptr, &modFilled)) { cap.note = fmt("eglQueryDmaBufModifiersEXT(fourcc=0x%08x, %d) -> 0x%04x", fourcc, (int)nmods, (unsigned)eglGetError()); return cap; } for (EGLint i = 0; i < modFilled; ++i) { if ((uint64_t)mods[(size_t)i] == modifier) cap.listed = true; } cap.note = cap.listed ? fmt("fourcc=0x%08x modifier=0x%016llx is in the driver's own list (%d formats, %d modifiers " "for this one), so any import failure below is the driver refusing its own pair", fourcc, (unsigned long long)modifier, (int)filled, (int)modFilled) : fmt("fourcc=0x%08x is listed but modifier=0x%016llx is not among its %d modifiers", fourcc, (unsigned long long)modifier, (int)modFilled); return cap; } // The ladder EGL_EXT_image_dma_buf_import{,_modifiers} defines. Rung 0 names the modifier // through EGL_DMA_BUF_PLANE0_MODIFIER_*_EXT, rung 1 is the original four-integer form. A // peer with no modifier has no rung-0 spelling at all, and the ladder records that as // "skipped" rather than as a rejection, so "rung 1 accepted" cannot be misread as the // driver having refused rung 0. struct T4Import { EGLImageKHR image = EGL_NO_IMAGE_KHR; int rung = -1; std::string accepted = "none"; std::string ladder; std::string fail; }; static T4Import t4ImportImage(EGLDisplay dpy, const T4Egl& e, int fd, const T4Layout& lay) { T4Import imp; for (int rung = 0; rung < 2; ++rung) { if (rung == 0 && lay.modifier == UINT64_MAX) { imp.ladder += "rung0=skipped(peer offered no modifier) "; continue; } EGLint attrs[24]; int n = 0; attrs[n++] = EGL_WIDTH; attrs[n++] = (EGLint)lay.width; attrs[n++] = EGL_HEIGHT; attrs[n++] = (EGLint)lay.height; attrs[n++] = EGL_LINUX_DRM_FOURCC_EXT; attrs[n++] = (EGLint)lay.fourcc; attrs[n++] = EGL_DMA_BUF_PLANE0_FD_EXT; attrs[n++] = fd; attrs[n++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; attrs[n++] = (EGLint)lay.offset; attrs[n++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; attrs[n++] = (EGLint)lay.stride; if (rung == 0) { attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; attrs[n++] = (EGLint)(lay.modifier & 0xFFFFFFFFu); attrs[n++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; attrs[n++] = (EGLint)(lay.modifier >> 32); } attrs[n++] = EGL_NONE; EGLImageKHR img = e.pCreateImage(dpy, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attrs); // read the error either way: it is this call's, and one left in the queue would be // blamed on the next one const EGLint err = eglGetError(); if (img != EGL_NO_IMAGE_KHR) { imp.image = img; imp.rung = rung; imp.accepted = rung == 0 ? "rung0-modifier" : "rung1-legacy"; imp.ladder += fmt("rung%d=accepted ", rung); return imp; } imp.ladder += fmt("rung%d=0x%04x ", rung, (unsigned)err); imp.fail = fmt("eglCreateImageKHR(rung%d) -> 0x%04x", rung, (unsigned)err); } return imp; } // --------------------------------------------------------------------------- // T4's second route to an EGLImage: wrap the peer's fd as an AHardwareBuffer // --------------------------------------------------------------------------- // // WHY THIS EXISTS. EGL_LINUX_DMA_BUF_EXT is the portable spelling and this driver does // not have it (measured), which left the cross-process image story depending on the HOST // allocating the buffer instead. But the device ships AHardwareBuffer_createFromHandle // in libnativewindow.so: the NDK does not declare it because it is a SystemApi, and plain // C has no opinion about that. If it accepts a container's bare dma-buf, the detour // disappears -- the compositor keeps minting its buffers exactly as it does today and the // render server wraps the fd it is already sent, with no host change at all. // // handleType is tried BOTH ways (1 for a handle from AHardwareBuffer_getNativeHandle, 2 // for a dma-buf) because that enum is not in the NDK here and guessing one would turn a // two-line answer into an unattributable FAIL. Nothing but the integer reaches the // platform. // // WHAT A SUCCESS HERE WOULD AND WOULD NOT SAY. Wrap plus EGLImage means the driver takes // the image. It does NOT say the driver will render into memory gralloc never described // -- that is what the two-pixel round trip at the end of this row measures, and it is the // only thing that settles it. struct T4NativeHandle { // native_handle_t, which the NDK does not declare int version; int numFds; int numInts; int data[1]; }; struct T4WrapApi { void* lib = nullptr; int (*createFromHandle)(const void* desc, const T4NativeHandle* handle, int handleType, void** out) = nullptr; }; static T4WrapApi t4WrapApi() { T4WrapApi a; // libnativewindow.so first: that is where the measured device exports it. const char* libs[] = {"libnativewindow.so", "libandroid.so", "libui.so"}; for (const char* name : libs) { a.lib = dlopen(name, RTLD_NOW | RTLD_LOCAL); if (!a.lib) continue; a.createFromHandle = (int (*)(const void*, const T4NativeHandle*, int, void**))dlsym( a.lib, "AHardwareBuffer_createFromHandle"); if (a.createFromHandle) return a; dlclose(a.lib); a.lib = nullptr; } return a; } // AHardwareBuffer_Format for the fourcc the peer named, or 0 for a pairing this row has // never seen. The memory order decides it: 'AB24' holds R,G,B,A and 'AR24' holds B,G,R,A, // and the two numbers below say exactly that in Android's vocabulary. They are written // out because they are the ABI of an enum the NDK does not carry. static uint32_t t4AhbFormatFor(uint32_t fourcc) { switch (fourcc) { case 0x34324241u: return 1u; // 'AB24' DRM_FORMAT_ABGR8888 -> R8G8B8A8_UNORM case 0x34325241u: return 5u; // 'AR24' DRM_FORMAT_ARGB8888 -> B8G8R8A8_UNORM default: return 0u; } } // The AHB the row wrapped. The EGLImage keeps using it until the row ends, and the row // ends at process exit, so the reference is deliberately never dropped: releasing a buffer // out from under a live image would be a use-after-free, which is a worse answer than a // leak in a program that is about to print its summary and exit. static void* gT4WrappedAhb = nullptr; static T4Import t4ImportViaWrap(GlCtx& g, const T4Egl& e, int fd, const T4Layout& lay) { T4Import imp; const T4WrapApi api = t4WrapApi(); if (!api.createFromHandle) { imp.ladder = "wrap-api=absent "; imp.fail = "no library here exports AHardwareBuffer_createFromHandle"; return imp; } // ahbFmt, not fmt: the file's fmt() is the printf helper, and a local named the same // would shadow it for the whole of this function. const uint32_t ahbFmt = t4AhbFormatFor(lay.fourcc); if (ahbFmt == 0) { imp.ladder = "wrap-api=present format=unknown "; imp.fail = fmt("fourcc=0x%08x has no AHardwareBuffer_Format on this row", lay.fourcc); return imp; } if (!g.pGetNativeClientBuffer) { imp.ladder = "wrap-api=present clientBuffer=absent "; imp.fail = "eglGetNativeClientBufferANDROID is not loadable, so an AHardwareBuffer cannot become an " "EGLClientBuffer"; return imp; } // AHardwareBuffer_Desc::stride is in PIXELS while the peer's layout is in bytes -- the // same trap that cost the Android-minted row a device run, hence the division. struct Desc { uint32_t width, height, layers, format; uint64_t usage; uint32_t stride, rfu0; uint64_t rfu1; } desc{}; desc.width = lay.width; desc.height = lay.height; desc.layers = 1; desc.format = ahbFmt; desc.usage = (1u << 8) | (1u << 9); // GPU_SAMPLED_IMAGE | GPU_COLOR_OUTPUT desc.stride = lay.stride / 4u; uint8_t hbuf[sizeof(T4NativeHandle) + 2 * sizeof(int)] = {0}; T4NativeHandle* h = (T4NativeHandle*)hbuf; h->version = (int)sizeof(hbuf); h->numFds = 1; h->numInts = 0; h->data[0] = fd; for (int type = 1; type <= 2; ++type) { void* ahb = nullptr; const int rc = api.createFromHandle(&desc, h, type, &ahb); imp.ladder += fmt("handleType%d=rc%d ", type, rc); if (rc != 0 || !ahb) continue; EGLClientBuffer cb = g.pGetNativeClientBuffer((const struct AHardwareBuffer*)ahb); const EGLint cbErr = eglGetError(); if (!cb) { imp.ladder += fmt("clientBuffer(type%d)=null(0x%04x) ", type, (unsigned)cbErr); continue; } const EGLint attrs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; EGLImageKHR img = e.pCreateImage(g.dpy, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, cb, attrs); const EGLint imgErr = eglGetError(); if (img == EGL_NO_IMAGE_KHR) { imp.ladder += fmt("image(type%d)=0x%04x ", type, (unsigned)imgErr); imp.fail = fmt("eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) on an fd wrapped as an AHardwareBuffer " "(handleType=%d) -> 0x%04x", type, (unsigned)imgErr); continue; } gT4WrappedAhb = ahb; imp.image = img; imp.rung = 100 + type; // off the two extension rungs, so the row cannot confuse them imp.accepted = fmt("wrap-handleType%d", type); imp.ladder += fmt("accepted(type%d) ", type); return imp; } if (imp.fail.empty()) imp.fail = "no handleType produced an EGLImage"; return imp; } // The memory order of the 32-bit layouts the peer can plausibly hand over. A DRM fourcc is // a name, not a layout rule -- 'AR24' is [31:0] A:R:G:B, so a little-endian buffer holds // B,G,R,A -- and a derived-then-wrong order would silently invert the peer's byte compare, // so the layouts are written out instead of computed. struct T4MemOrder { uint32_t fourcc; const char* mem; // channels in memory order, byte 0 first }; static const T4MemOrder kT4MemOrders[] = { {0x34325241u, "BGRA"}, // 'AR24' DRM_FORMAT_ARGB8888 {0x34324241u, "RGBA"}, // 'AB24' DRM_FORMAT_ABGR8888 {0x34324152u, "ABGR"}, // 'RA24' DRM_FORMAT_RGBA8888 {0x34324142u, "ARGB"}, // 'BA24' DRM_FORMAT_BGRA8888 {0x34325258u, "BGRX"}, // 'XR24' DRM_FORMAT_XRGB8888 {0x34324258u, "RGBX"}, // 'XB24' DRM_FORMAT_XBGR8888 {0x34325852u, "XBGR"}, // 'RX24' DRM_FORMAT_RGBX8888 {0x34325842u, "XRGB"}, // 'BX24' DRM_FORMAT_BGRX8888 }; struct T4ColorPlan { bool pinned = false; GLfloat clear[4] = {0.0f, 0.0f, 0.0f, 1.0f}; std::string note; }; static T4ColorPlan t4PlanColor(uint32_t fourcc, const uint8_t want[4]) { T4ColorPlan plan; const char* mem = nullptr; for (const T4MemOrder& m : kT4MemOrders) { if (m.fourcc == fourcc) mem = m.mem; } if (!mem) { // GL channel order is all this side can honestly claim here, and the row says the // compare the peer is about to make was not pinned by the format. for (int i = 0; i < 4; ++i) plan.clear[i] = want[i] / 255.0f; plan.note = fmt("fourcc=0x%08x is not one of the 32-bit layouts whose memory order this side knows, so " "the clear goes out in GL channel order and the peer's byte compare is not pinned by " "the format", fourcc); return plan; } plan.pinned = true; for (int i = 0; i < 4; ++i) { switch (mem[i]) { case 'R': plan.clear[0] = want[i] / 255.0f; break; case 'G': plan.clear[1] = want[i] / 255.0f; break; case 'B': plan.clear[2] = want[i] / 255.0f; break; case 'A': plan.clear[3] = want[i] / 255.0f; break; default: break; // 'X' is written as 1.0, which want[3] = 0xFF matches } } plan.note = fmt("fourcc=0x%08x holds %s in memory", fourcc, mem); return plan; } // GL hands back channels while the peer's seed is memory bytes; when the fourcc's memory // order is known the seed can be re-read in channel order, and the row reports that // comparison next to the verbatim one so a byte-order convention can never pass for a // broken import. Returns false, leaving `out` alone, when the order is not known. static bool t4SeedAsChannels(uint32_t fourcc, const uint8_t seed[4], uint8_t out[4]) { const char* mem = nullptr; for (const T4MemOrder& m : kT4MemOrders) { if (m.fourcc == fourcc) mem = m.mem; } if (!mem) return false; const char* channels = "RGBA"; for (int ch = 0; ch < 4; ++ch) { out[ch] = 0xFF; // an 'X' byte carries nothing; the driver writes 1.0 there for (int i = 0; i < 4; ++i) { if (mem[i] == channels[ch]) out[ch] = seed[i]; } } return true; } // The 1x1 sampling program the texture read goes through. The leg's own FBO holds the // image, so the draw has to land somewhere else, and the probe's surface is a 1x1 pbuffer -- // exactly the target this needs. static const char* kT4ReadVs = "#version 300 es\n" "void main() {\n" " vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n" " gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n" "}\n"; // highp on the texel centre: (w - 0.5) / w is not representable in mediump for a real image, // and a rounded coordinate would sample the neighbouring texel -- exactly the mistake the // corner read exists to catch. static const char* kT4ReadFs = "#version 300 es\n" "precision highp float;\n" "precision highp sampler2D;\n" "uniform sampler2D uTex;\n" "uniform vec2 uTexel;\n" "out vec4 oColor;\n" "void main() { oColor = texture(uTex, uTexel); }\n"; static GLuint t4BuildReadProgram(std::string* fail) { auto compile = [&](GLenum kind, const char* src) -> GLuint { GLuint sh = glCreateShader(kind); glShaderSource(sh, 1, &src, nullptr); glCompileShader(sh); GLint ok = 0; glGetShaderiv(sh, GL_COMPILE_STATUS, &ok); if (!ok) { char log[512] = {0}; glGetShaderInfoLog(sh, sizeof(log) - 1, nullptr, log); *fail = std::string("shader compile failed: ") + log; glDeleteShader(sh); return 0; } return sh; }; GLuint vs = compile(GL_VERTEX_SHADER, kT4ReadVs); if (!vs) return 0; GLuint fs = compile(GL_FRAGMENT_SHADER, kT4ReadFs); if (!fs) { glDeleteShader(vs); return 0; } GLuint prog = glCreateProgram(); glAttachShader(prog, vs); glAttachShader(prog, fs); glLinkProgram(prog); glDeleteShader(vs); glDeleteShader(fs); GLint ok = 0; glGetProgramiv(prog, GL_LINK_STATUS, &ok); if (!ok) { char log[512] = {0}; glGetProgramInfoLog(prog, sizeof(log) - 1, nullptr, log); *fail = std::string("link failed: ") + log; glDeleteProgram(prog); return 0; } return prog; } static bool t4ReadTexel(GLuint prog, GLint locTex, GLint locTexel, GLuint tex, uint32_t w, uint32_t h, uint32_t x, uint32_t y, uint8_t out[4], std::string* fail) { glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(0, 0, 1, 1); glUseProgram(prog); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, tex); glUniform1i(locTex, 0); glUniform2f(locTexel, ((float)x + 0.5f) / (float)w, ((float)y + 0.5f) / (float)h); glDrain(); glDrawArrays(GL_TRIANGLES, 0, 3); const GLenum drawErr = glDrain(); glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out); const GLenum readErr = glDrain(); if (drawErr != GL_NO_ERROR || readErr != GL_NO_ERROR) { *fail = fmt("1x1 sampling draw -> %s, glReadPixels -> %s", glErrStr(drawErr).c_str(), glErrStr(readErr).c_str()); return false; } return true; } static bool t4ReadRb(GLuint fbo, uint32_t x, uint32_t y, uint8_t out[4], std::string* fail) { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glDrain(); glReadPixels((GLint)x, (GLint)y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, out); const GLenum e = glDrain(); if (e != GL_NO_ERROR) { *fail = "glReadPixels off the image FBO -> " + glErrStr(e); return false; } return true; } // Clears one path to `color`, then glFinish *and* an explicit fence whose wait result is what // comes back: the peer is a different process on a different driver stack, so "the drawing // landed" needs evidence this side can show, and glFinish alone only says the commands were // accepted. static std::string t4WritePath(GLuint fbo, uint32_t w, uint32_t h, const GLfloat color[4]) { glBindFramebuffer(GL_FRAMEBUFFER, fbo); glViewport(0, 0, (GLsizei)w, (GLsizei)h); glClearColor(color[0], color[1], color[2], color[3]); glDrain(); glClear(GL_COLOR_BUFFER_BIT); const GLenum clearErr = glDrain(); glFinish(); const GLenum finishErr = glDrain(); const GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); const GLenum fenceErr = glDrain(); std::string wait = "no sync object"; if (sync) { // bounded: an unsignalled fence has to read as GL_TIMEOUT_EXPIRED, never as a hang wait = t4WaitStr(glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull)); glDeleteSync(sync); glDrain(); } return fmt("clear=%s finish=%s fence=%s wait=%s", glErrStr(clearErr).c_str(), glErrStr(finishErr).c_str(), glErrStr(fenceErr).c_str(), wait.c_str()); } // Per-path state: the import attempt, the two reads, and the peer's answer to the clear. struct T4PathRun { const char* name = "?"; bool targetOk = false; // glEGLImageTarget*OES took the image std::string targetFail; bool fboOk = false; // and the image is a complete COLOR_ATTACHMENT0 std::string fboFail; bool readDone = false; uint8_t word[4] = {0, 0, 0, 0}; uint8_t corner[4] = {0, 0, 0, 0}; std::string readFail; bool wrote = false; bool ackGot = false; int32_t ackOk = 0; uint8_t observed[4] = {0, 0, 0, 0}; std::string ackNote; std::string writeFail; }; struct T4GL { GLuint tex = 0; GLuint rb = 0; GLuint fboTex = 0; GLuint fboRb = 0; GLuint prog = 0; GLint locTex = -1; GLint locTexel = -1; }; // Waits for `want`, stepping over -- and naming -- any other frame. The frozen protocol // gives MSG_T4_RESULT no payload contract, so a frame carrying it is skipped rather than // parsed, which keeps the stream aligned for the ack that follows. Skipping assumes the // unexpected frame is no larger than the one being awaited, which is the only size the // protocol defines. static bool t4RecvTag(int sock, uint32_t want, void* payload, size_t maxLen, size_t* gotOut, int* fdOut, std::string* skipped, std::string* fail) { for (int i = 0; i < 8; ++i) { uint32_t tag = 0; size_t got = 0; int fd = -1; if (!recvMsg(sock, &tag, payload, maxLen, &got, &fd)) { *fail = fmt("recvMsg -> errno=%d(%s)", errno, strerror(errno)); if (fd >= 0) close(fd); return false; } if (tag == want) { if (gotOut) { *gotOut = got; } if (fdOut) { *fdOut = fd; } else if (fd >= 0) { close(fd); } return true; } if (fd >= 0) close(fd); if (skipped) { if (!skipped->empty()) *skipped += ","; *skipped += fmt("tag%u(len=%zu)", tag, got); } } *fail = "too many unexpected frames before the expected one"; return false; } // The Android-minted row, defined at the end of this section: runT4 records the // container-minted direction first and this one second, and the second is the row // the first cannot stand in for. static void runT4AhbRow(GlCtx& g, const T4Egl& e, int sock, const T4Layout& lay); static void runT4(GlCtx& g, bool glOk, const char* endpoint) { // Capability gates come before the listen: with no context, or without the extensions // and entry points the import is spelled in, there is nothing this side could import, // and saying so here beats leaving the peer to discover it through its own timeout. if (!glOk) { record(kT4Row, "SKIP", "no headless GLES context: nothing on this side could import the peer's dma-buf"); return; } T4Egl e; e.pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); e.pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); e.pFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT"); e.pModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT"); e.pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES"); e.pTargetRb = (PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC)eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"); if (!g.hasEgl("EGL_KHR_image_base") || !e.pCreateImage || !e.pDestroyImage) { record(kT4Row, "UNSUPPORTED", fmt("EGL_KHR_image_base=%d and of that extension eglCreateImageKHR=%d eglDestroyImageKHR=%d", (int)g.hasEgl("EGL_KHR_image_base"), (int)(e.pCreateImage != nullptr), (int)(e.pDestroyImage != nullptr))); return; } if (!g.hasGl("GL_OES_EGL_image") || (!e.pTargetTexture && !e.pTargetRb)) { record(kT4Row, "UNSUPPORTED", fmt("GL_OES_EGL_image=%d and of that extension glEGLImageTargetTexture2DOES=%d " "glEGLImageTargetRenderbufferStorageOES=%d", (int)g.hasGl("GL_OES_EGL_image"), (int)(e.pTargetTexture != nullptr), (int)(e.pTargetRb != nullptr))); return; } int ls = rtListen(endpoint); if (ls < 0) { record(kT4Row, "FAIL", fmt("could not listen on %s for the container peer: errno=%d(%s)", endpoint, errno, strerror(errno))); return; } setRecvTimeout(ls, 60); pr("T4: listening on %s for the Droidspaces peer (abstract socket; net_mode=host keeps both ends in one " "namespace)", endpoint); int sock = accept(ls, nullptr, nullptr); close(ls); if (sock < 0) { record(kT4Row, "FAIL", fmt("no peer dialed %s within the accept timeout: errno=%d(%s)", endpoint, errno, strerror(errno))); return; } setRecvTimeout(sock, 60); // ---- 1. the peer's offer ------------------------------------------------- T4Layout lay{}; uint32_t tag = 0; size_t got = 0; int fd = -1; if (!recvMsg(sock, &tag, &lay, sizeof(lay), &got, &fd) || tag != MSG_T4_OFFER || got != sizeof(lay)) { record(kT4Row, "FAIL", fmt("no T4 offer: tag=%u len=%zu errno=%d(%s)", tag, got, errno, strerror(errno))); if (fd >= 0) close(fd); close(sock); return; } lay.peer[sizeof(lay.peer) - 1] = 0; lay.note[sizeof(lay.note) - 1] = 0; const std::string fdDesc = describeFd(fd); const std::string modifierStr = lay.modifier == UINT64_MAX ? std::string("none") : fmt("0x%016llx", (unsigned long long)lay.modifier); pr("T4 peer [%s] %s", lay.peer, lay.note); pr("T4 offer: v%u %ux%u fourcc=0x%08x stride=%u offset=%u modifier=%s seed=%s fd[%s]", lay.version, lay.width, lay.height, lay.fourcc, lay.stride, lay.offset, modifierStr.c_str(), t4Hex4(lay.seed).c_str(), fdDesc.c_str()); if (lay.magic != kT4Magic) { record(kT4Row, "FAIL", fmt("offer magic=0x%08x, expected 0x%08x: the two sides do not speak the same " "protocol | peer [%s] %s", lay.magic, kT4Magic, lay.peer, lay.note)); if (fd >= 0) close(fd); close(sock); return; } if (fd < 0) { record(kT4Row, "FAIL", fmt("the offer carried no fd over SCM_RIGHTS, so the peer could not create the GBM " "buffer | %ux%u fourcc=0x%08x | peer [%s] %s", lay.width, lay.height, lay.fourcc, lay.peer, lay.note)); close(sock); return; } if (lay.width == 0 || lay.height == 0 || lay.stride == 0) { record(kT4Row, "FAIL", fmt("degenerate layout %ux%u stride=%u: nothing can be imported from it | fd[%s]", lay.width, lay.height, lay.stride, fdDesc.c_str())); close(fd); close(sock); return; } // ---- the capability gate: EGL_EXT_image_dma_buf_import ------------------- // Every rung of the EGL_LINUX_DMA_BUF_EXT ladder is defined by that extension, // so a driver that does not advertise it does not take a container's dma-buf at // all. Walking the ladder anyway would report a capability answer as an import // failure -- the two things this row exists to keep apart -- so the row is // recorded here, naming the extension, and the ladder is not entered. The peer // stays connected: the Android-minted row below is the half of T4 that can still // be measured on such a driver, and it needs this same connection. // ---- 2/3. into an EGLImage: the driver's own ladder, or the wrap that replaces it // A driver without EGL_EXT_image_dma_buf_import cannot be asked the ladder question at // all, so the row goes to the wrap route instead of reporting a capability answer it // has not finished establishing. Only when BOTH routes fail is the offer recorded as // one this driver cannot take up. T4Import imp; // The capability probe only means anything on the ladder route, and the row prints it // either way: a default-constructed T4Cap says "never asked" (asked=0), which is the // truth when the extension is not there to ask about. T4Cap cap; if (!g.hasEgl("EGL_EXT_image_dma_buf_import")) { pr("T4: EGL_EXT_image_dma_buf_import is not advertised: the EGL_LINUX_DMA_BUF_EXT ladder is not walked"); imp = t4ImportViaWrap(g, e, fd, lay); pr("T4 wrap: accepted=%s | ladder: %s| %s", imp.accepted.c_str(), imp.ladder.c_str(), imp.fail.empty() ? "-" : imp.fail.c_str()); if (imp.image == EGL_NO_IMAGE_KHR) { record(kT4Row, "UNSUPPORTED", fmt("neither route to an EGLImage exists here: EGL_EXT_image_dma_buf_import is not advertised " "(EGL_KHR_image_base=%d, GL_OES_EGL_image=%d) so EGL_LINUX_DMA_BUF_EXT has no spelling, and " "wrapping the fd as an AHardwareBuffer did not produce one either (%s): the container's %ux%u " "fourcc=0x%08x stride=%u offset=%u modifier=%s fd[%s] and peer [%s] %s are recorded as an " "offer this driver cannot take up", (int)g.hasEgl("EGL_KHR_image_base"), (int)g.hasGl("GL_OES_EGL_image"), imp.fail.empty() ? imp.ladder.c_str() : imp.fail.c_str(), lay.width, lay.height, lay.fourcc, lay.stride, lay.offset, modifierStr.c_str(), fdDesc.c_str(), lay.peer, lay.note)); runT4AhbRow(g, e, sock, lay); close(fd); close(sock); return; } } else { // ---- the driver's own question, before any import --------------------- cap = t4Capability(g.dpy, e, lay.fourcc, lay.modifier); pr("T4 capability: asked=%d formatListed=%d pairListed=%d formats=%d modifiers=%d | %s", (int)cap.asked, (int)cap.formatListed, (int)cap.listed, cap.formats, cap.modifiers, cap.note.c_str()); imp = t4ImportImage(g.dpy, e, fd, lay); pr("T4 import: accepted=%s | ladder: %s| %s", imp.accepted.c_str(), imp.ladder.c_str(), imp.fail.empty() ? "-" : imp.fail.c_str()); } // ---- 4. read both paths, at two pixels each ------------------------------ T4GL gl; T4PathRun tex, rb; tex.name = "tex"; rb.name = "rb"; uint8_t seedCh[4] = {0xFF, 0xFF, 0xFF, 0xFF}; const bool seedChKnown = t4SeedAsChannels(lay.fourcc, lay.seed, seedCh); if (imp.image != EGL_NO_IMAGE_KHR) { if (e.pTargetTexture) { glGenFramebuffers(1, &gl.fboTex); glGenTextures(1, &gl.tex); glBindTexture(GL_TEXTURE_2D, gl.tex); // NEAREST and no mip chain: the read has to land on the texel asked for and not // on an average of its neighbours glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glDrain(); e.pTargetTexture(GL_TEXTURE_2D, (GLeglImageOES)imp.image); const GLenum texErr = glDrain(); if (texErr != GL_NO_ERROR) { tex.targetFail = "glEGLImageTargetTexture2DOES -> " + glErrStr(texErr); } else { tex.targetOk = true; glBindFramebuffer(GL_FRAMEBUFFER, gl.fboTex); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gl.tex, 0); const GLenum st = glCheckFramebufferStatus(GL_FRAMEBUFFER); const GLenum attachErr = glDrain(); if (st != GL_FRAMEBUFFER_COMPLETE || attachErr != GL_NO_ERROR) { tex.fboFail = fmt("the image texture attached but the FBO is 0x%04x (%s)", (unsigned)st, glErrStr(attachErr).c_str()); } else { tex.fboOk = true; } } } else { tex.targetFail = "glEGLImageTargetTexture2DOES is absent, so the texture path was not exercised"; } if (e.pTargetRb) { glGenFramebuffers(1, &gl.fboRb); glGenRenderbuffers(1, &gl.rb); glBindRenderbuffer(GL_RENDERBUFFER, gl.rb); glDrain(); e.pTargetRb(GL_RENDERBUFFER, (GLeglImageOES)imp.image); const GLenum rbErr = glDrain(); if (rbErr != GL_NO_ERROR) { rb.targetFail = "glEGLImageTargetRenderbufferStorageOES -> " + glErrStr(rbErr); } else { rb.targetOk = true; glBindFramebuffer(GL_FRAMEBUFFER, gl.fboRb); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, gl.rb); const GLenum st = glCheckFramebufferStatus(GL_FRAMEBUFFER); const GLenum attachErr = glDrain(); if (st != GL_FRAMEBUFFER_COMPLETE || attachErr != GL_NO_ERROR) { rb.fboFail = fmt("the image renderbuffer attached but the FBO is 0x%04x (%s)", (unsigned)st, glErrStr(attachErr).c_str()); } else { rb.fboOk = true; } } } else { rb.targetFail = "glEGLImageTargetRenderbufferStorageOES is absent, so the renderbuffer path was not exercised"; } } else { tex.targetFail = fmt("no EGLImage to attach: %s (ladder: %s)", imp.fail.empty() ? "the import ladder was not entered" : imp.fail.c_str(), imp.ladder.c_str()); rb.targetFail = tex.targetFail; } const uint32_t farX = lay.width - 1, farY = lay.height - 1; if (tex.targetOk) { std::string progFail; gl.prog = t4BuildReadProgram(&progFail); if (!gl.prog) { tex.readFail = progFail; } else { gl.locTex = glGetUniformLocation(gl.prog, "uTex"); gl.locTexel = glGetUniformLocation(gl.prog, "uTexel"); std::string failA, failB; const bool okA = t4ReadTexel(gl.prog, gl.locTex, gl.locTexel, gl.tex, lay.width, lay.height, 0, 0, tex.word, &failA); const bool okB = t4ReadTexel(gl.prog, gl.locTex, gl.locTexel, gl.tex, lay.width, lay.height, farX, farY, tex.corner, &failB); tex.readDone = okA && okB; if (!okA) tex.readFail = "pixel(0,0): " + failA; if (!okB) tex.readFail += (tex.readFail.empty() ? "" : "; ") + std::string("pixel(w-1,h-1): ") + failB; } } else { tex.readFail = "not attempted: " + tex.targetFail; } if (rb.targetOk && rb.fboOk) { std::string failA, failB; const bool okA = t4ReadRb(gl.fboRb, 0, 0, rb.word, &failA); const bool okB = t4ReadRb(gl.fboRb, farX, farY, rb.corner, &failB); rb.readDone = okA && okB; if (!okA) rb.readFail = "pixel(0,0): " + failA; if (!okB) rb.readFail += (rb.readFail.empty() ? "" : "; ") + std::string("pixel(w-1,h-1): ") + failB; } else if (rb.targetOk) { rb.readFail = "not attempted: " + rb.fboFail; } else { rb.readFail = "not attempted: " + rb.targetFail; } // The protocol's read comparison is verbatim and the verdict keeps it that way. GL hands // back channels while the peer's seed is memory bytes, so when the fourcc's memory order // is known the same comparison is also made against the seed re-read in channel order: // the two disagreeing is a fact about byte order, not about the import, and the row shows // both rather than letting the verbatim test alone imply a broken driver. const bool texReadOk = tex.targetOk && tex.readDone && !memcmp(tex.word, lay.seed, 4) && !memcmp(tex.corner, lay.seed, 4); const bool rbReadOk = rb.targetOk && rb.readDone && !memcmp(rb.word, lay.seed, 4) && !memcmp(rb.corner, lay.seed, 4); const bool texOrderOk = seedChKnown && !memcmp(tex.word, seedCh, 4) && !memcmp(tex.corner, seedCh, 4); const bool rbOrderOk = seedChKnown && !memcmp(rb.word, seedCh, 4) && !memcmp(rb.corner, seedCh, 4); // ---- 5. what the two reads produced, back to the peer -------------------- T4Read rep{}; rep.texOk = texReadOk ? 1 : 0; rep.rbOk = rbReadOk ? 1 : 0; rep.rung = imp.rung; memcpy(rep.texWord, tex.word, 4); memcpy(rep.texCorner, tex.corner, 4); memcpy(rep.rbWord, rb.word, 4); memcpy(rep.rbCorner, rb.corner, 4); snprintf(rep.importNote, sizeof(rep.importNote), "%s | ladder: %s| %s", imp.accepted.c_str(), imp.ladder.c_str(), cap.note.c_str()); const bool readSent = sendMsg(sock, MSG_T4_READ, &rep, sizeof(rep), -1); if (!readSent) pr("T4: MSG_T4_READ did not go out: errno=%d(%s)", errno, strerror(errno)); // ---- 6/7. draw into each path, ack by ack -------------------------------- // Both palettes end in 0xFF so an ignored 'X' channel and a real 'A' agree, and the two // clears differ in every byte so a swapped path cannot pass unnoticed. const uint8_t rbWant[4] = {0x11, 0x22, 0x33, 0xFF}; const uint8_t texWant[4] = {0x44, 0x55, 0x66, 0xFF}; const T4ColorPlan rbPlan = t4PlanColor(lay.fourcc, rbWant); const T4ColorPlan texPlan = t4PlanColor(lay.fourcc, texWant); std::string rbFence = "not attempted", texFence = "not attempted"; std::string skipped; if (rb.fboOk) { rbFence = t4WritePath(gl.fboRb, lay.width, lay.height, rbPlan.clear); T4Wrote w{}; w.path = 0; memcpy(w.color, rbWant, 4); if (!sendMsg(sock, MSG_T4_WROTE, &w, sizeof(w), -1)) { rb.writeFail = fmt("sendMsg(MSG_T4_WROTE path=0) errno=%d(%s)", errno, strerror(errno)); } else { rb.wrote = true; T4Ack ack{}; std::string fail; if (!t4RecvTag(sock, MSG_T4_ACK, &ack, sizeof(ack), nullptr, nullptr, &skipped, &fail)) { rb.writeFail = "no ack for the renderbuffer write: " + fail; } else { ack.note[sizeof(ack.note) - 1] = 0; rb.ackGot = true; rb.ackOk = ack.ok; memcpy(rb.observed, ack.observed, 4); rb.ackNote = ack.note; if (!ack.ok) { rb.writeFail = fmt("peer ack ok=0 observed=%s want=%s note=[%s]", t4Hex4(ack.observed).c_str(), t4Hex4(rbWant).c_str(), ack.note); } } } } else { const std::string whyNot = rb.targetOk ? rb.fboFail : rb.targetFail; rb.writeFail = "no clear was attempted: " + whyNot; } if (tex.fboOk) { texFence = t4WritePath(gl.fboTex, lay.width, lay.height, texPlan.clear); T4Wrote w{}; w.path = 1; memcpy(w.color, texWant, 4); if (!sendMsg(sock, MSG_T4_WROTE, &w, sizeof(w), -1)) { tex.writeFail = fmt("sendMsg(MSG_T4_WROTE path=1) errno=%d(%s)", errno, strerror(errno)); } else { tex.wrote = true; T4Ack ack{}; std::string fail; if (!t4RecvTag(sock, MSG_T4_ACK, &ack, sizeof(ack), nullptr, nullptr, &skipped, &fail)) { tex.writeFail = "no ack for the texture write: " + fail; } else { ack.note[sizeof(ack.note) - 1] = 0; tex.ackGot = true; tex.ackOk = ack.ok; memcpy(tex.observed, ack.observed, 4); tex.ackNote = ack.note; if (!ack.ok) { tex.writeFail = fmt("peer ack ok=0 observed=%s want=%s note=[%s]", t4Hex4(ack.observed).c_str(), t4Hex4(texWant).c_str(), ack.note); } } } } else { const std::string whyNot = tex.targetOk ? tex.fboFail : tex.targetFail; tex.writeFail = "no clear was attempted: " + whyNot; } pr("T4 clear: renderbuffer %s ack=%d | texture %s ack=%d", rbFence.c_str(), rb.ackGot ? rb.ackOk : -1, texFence.c_str(), tex.ackGot ? tex.ackOk : -1); // ---- 8. the peer's last word --------------------------------------------- // MSG_T4_RESULT has no payload contract in the frozen protocol, so it is taken raw: the // length and the leading bytes go into the row and nothing is parsed out of it. uint8_t lastWord[256]; memset(lastWord, 0, sizeof(lastWord)); std::string resultNote; { setRecvTimeout(sock, 2); // bounded: a peer that says nothing must not hold the row up uint32_t rtag = 0; size_t rgot = 0; if (recvMsg(sock, &rtag, lastWord, sizeof(lastWord), &rgot, nullptr)) { if (rtag == MSG_T4_RESULT) { std::string hex; for (size_t i = 0; i < rgot && i < 16; ++i) hex += fmt("%02x", lastWord[i]); resultNote = fmt("MSG_T4_RESULT len=%zu bytes=%s", rgot, hex.c_str()); } else { resultNote = fmt("the peer's last frame was tag=%u len=%zu, not MSG_T4_RESULT", rtag, rgot); } } else { resultNote = fmt("no MSG_T4_RESULT frame (the peer closed, timed out, or sent more than %zu bytes): " "errno=%d(%s)", sizeof(lastWord), errno, strerror(errno)); } } // ---- the verdict --------------------------------------------------------- std::vector legs; { Leg l; l.name = tex.name; l.decisive = true; l.attempted = tex.targetOk; l.readOk = texReadOk; l.writeOk = tex.ackGot && tex.ackOk != 0; if (!tex.targetOk) l.fail = tex.targetFail; else if (!tex.readDone) l.fail = "texture read: " + tex.readFail; else if (!l.readOk) l.fail = fmt("texture read is not the peer's seed verbatim: word=%s corner=%s seed(memory)=%s " "seed(channels)=%s channelMatch=%d -- GL hands back channels while the peer fills " "memory bytes, so a fourcc whose memory order is not RGBA differs here on a working " "import", t4Hex4(tex.word).c_str(), t4Hex4(tex.corner).c_str(), t4Hex4(lay.seed).c_str(), seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", (int)texOrderOk); else if (!l.writeOk) l.fail = tex.writeFail; legs.push_back(l); } { Leg l; l.name = rb.name; l.decisive = true; l.attempted = rb.targetOk; l.readOk = rbReadOk; l.writeOk = rb.ackGot && rb.ackOk != 0; if (!rb.targetOk) l.fail = rb.targetFail; else if (!rb.readDone) l.fail = "renderbuffer read: " + rb.readFail; else if (!l.readOk) l.fail = fmt("renderbuffer read is not the peer's seed verbatim: word=%s corner=%s seed(memory)=%s " "seed(channels)=%s channelMatch=%d -- GL hands back channels while the peer fills " "memory bytes, so a fourcc whose memory order is not RGBA differs here on a working " "import", t4Hex4(rb.word).c_str(), t4Hex4(rb.corner).c_str(), t4Hex4(lay.seed).c_str(), seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", (int)rbOrderOk); else if (!l.writeOk) l.fail = rb.writeFail; legs.push_back(l); } std::string why; const char* status = legVerdict(legs, &why); // every failure string already names its step, so an empty one leaves no marker auto join = [](const std::string& a, const std::string& b) { if (a.empty()) return b; if (b.empty()) return a; return a + "; " + b; }; std::string detail = fmt( "%s | %ux%u fourcc=0x%08x stride=%u offset=%u modifier=%s seed=%s | capability[asked=%d format=%d pair=%d " "formats=%d modifiers=%d: %s] | import[accepted=%s ladder: %s| %s] | fd[%s]", legTrace(legs).c_str(), lay.width, lay.height, lay.fourcc, lay.stride, lay.offset, modifierStr.c_str(), t4Hex4(lay.seed).c_str(), (int)cap.asked, (int)cap.formatListed, (int)cap.listed, cap.formats, cap.modifiers, cap.note.c_str(), imp.accepted.c_str(), imp.ladder.c_str(), imp.fail.empty() ? "-" : imp.fail.c_str(), fdDesc.c_str()); detail += fmt(" | tex[target=%d fbo=%d word=%s corner=%s read=%d orderMapped=%d(%s) fail=[%s]]", (int)tex.targetOk, (int)tex.fboOk, t4Hex4(tex.word).c_str(), t4Hex4(tex.corner).c_str(), (int)tex.readDone, (int)texOrderOk, seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", join(tex.targetFail, join(tex.readFail, tex.writeFail)).c_str()); detail += fmt(" rb[target=%d fbo=%d word=%s corner=%s read=%d orderMapped=%d(%s) fail=[%s]]", (int)rb.targetOk, (int)rb.fboOk, t4Hex4(rb.word).c_str(), t4Hex4(rb.corner).c_str(), (int)rb.readDone, (int)rbOrderOk, seedChKnown ? t4Hex4(seedCh).c_str() : "fourcc order unknown", join(rb.targetFail, join(rb.readFail, rb.writeFail)).c_str()); detail += fmt(" | clear[rb fence=%s want=%s ack=%d observed=%s note=[%s]] [tex fence=%s want=%s ack=%d " "observed=%s note=[%s]] skipped=[%s]", rbFence.c_str(), t4Hex4(rbWant).c_str(), rb.ackGot ? rb.ackOk : -1, t4Hex4(rb.observed).c_str(), rb.ackNote.c_str(), texFence.c_str(), t4Hex4(texWant).c_str(), tex.ackGot ? tex.ackOk : -1, t4Hex4(tex.observed).c_str(), tex.ackNote.c_str(), skipped.c_str()); detail += fmt(" | colourOrder[rb pinned=%d %s] [tex pinned=%d %s] | readSent=%d | %s | peer [%s] %s | %s", (int)rbPlan.pinned, rbPlan.note.c_str(), (int)texPlan.pinned, texPlan.note.c_str(), (int)readSent, resultNote.c_str(), lay.peer, lay.note, why.c_str()); record(kT4Row, status, detail); // The other half of T4, over the connection that is still open: this side mints // the image and the container imports it. See runT4AhbRow for what it measures. runT4AhbRow(g, e, sock, lay); // The EGLImage outlives every GL object that wraps it, and the fd is the peer's only // handle on the buffer: tear down in that order. if (gl.prog) glDeleteProgram(gl.prog); if (gl.fboTex) glDeleteFramebuffers(1, &gl.fboTex); if (gl.fboRb) glDeleteFramebuffers(1, &gl.fboRb); if (gl.tex) glDeleteTextures(1, &gl.tex); if (gl.rb) glDeleteRenderbuffers(1, &gl.rb); if (imp.image != EGL_NO_IMAGE_KHR) e.pDestroyImage(g.dpy, imp.image); close(fd); close(sock); } // --------------------------------------------------------------------------- // T4-ahb-image: the same image, minted on the Android side // // T4-image-import above asks whether an image allocated in the container can be // imported HERE. This row asks the other half of the same question, and it is the // half that survives on a driver with no EGL_EXT_image_dma_buf_import. An app // cannot adopt the container's fd as an AHardwareBuffer (createFromHandle is a // SystemApi, not NDK), so the image is born on this side and travels the other way: // the buffer is allocated in this process, handed over as an fd over a socketpair -- // the mechanism T0 hands its BLOB across -- and imported in the container as a GBM bo. // // Both directions are measured, and neither is measured by this side alone: // read the peer must find, at (0,0) AND at the far corner of its own mapping, // the four bytes this side cleared into the image. // write the peer fills its mapping with a pattern of its own and this side reads // it back through the GPU -- glEGLImageTargetTexture2DOES plus the same 1x1 // sampling program the row above uses, not a second CPU mapping of a buffer // this process already owns, which would prove nothing about the peer. // A refusal anywhere is recorded where it happened with its EGL or GL error named, // and can never come out as OK. // --------------------------------------------------------------------------- static const char* kT4AhbRow = "T4-ahb-image"; // The DRM fourcc this row offers, and why it is that one: an AHardwareBuffer does // not carry a fourcc, so the two sides must agree on memory order in words. // AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM is R,G,B,A in memory, and the DRM name with // that same little-endian layout is DRM_FORMAT_ABGR8888, fourcc_code('A','B','2','4') // = 0x34324241, whose [31:0] A:B:G:R puts R in byte 0 -- the pairing gralloc itself // uses for HAL_PIXEL_FORMAT_RGBA_8888. Choosing it is what makes the comparison // below mean something: t4PlanColor() derives the clear colour from the same table // entry the peer's bytes are compared through, so a channel mistake cannot cancel // itself out between the two sides. static const uint32_t kT4AhbFourcc = 0x34324241u; // DRM_FORMAT_ABGR8888, memory order R,G,B,A // AHardwareBuffer_Desc::stride IS IN PIXELS -- the platform's own unit -- while every // consumer on the wire wants the row stride in BYTES: gbm_bo_import is told it, and the // peer's mapping and both of its pixel offsets follow from it. Announcing the pixel // count as if it were bytes cost a device run (the peer imported a 64-byte row for a // 256-byte-row image), so the conversion lives here, next to the fourcc that fixes the // bytes per pixel: DRM_FORMAT_ABGR8888 is four bytes per pixel, and the AHB format this // row allocates (AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM) is the same memory order. static const uint32_t kT4AhbBytesPerPixel = 4u; // The colour this side clears into the image before offering it: four bytes in that // memory order, all four distinct, so a permuted or repeated byte shows up as a // different sequence instead of hiding inside the colour itself. static const uint8_t kT4AhbSeed[4] = {0x2A, 0x5B, 0x8C, 0xFF}; static void runT4AhbRow(GlCtx& g, const T4Egl& e, int sock, const T4Layout& lay) { // The row is sized by the leg's only size knob: the peer dialled with --width/ // --height (64x64 by default) and the same two numbers size this allocation, so a // single option moves both sides and they cannot silently disagree about the // layout. The stride, by contrast, is never assumed to be width*4: it is what // AHardwareBuffer_describe() reports in PIXELS, converted to the BYTES every consumer // on the wire wants, and it is what the peer is told. const uint32_t w = lay.width, h = lay.height; const uint32_t farX = w - 1, farY = h - 1; const T4ColorPlan plan = t4PlanColor(kT4AhbFourcc, kT4AhbSeed); // ---- the entry points this row is spelled in ----------------------------- if (!g.hasEgl("EGL_ANDROID_get_native_client_buffer") || !g.pGetNativeClientBuffer) { record(kT4AhbRow, "UNSUPPORTED", fmt("EGL_ANDROID_get_native_client_buffer=%d and of that extension eglGetNativeClientBufferANDROID=%d: " "an AHardwareBuffer cannot become an EGLClientBuffer here, so there is no image to offer", (int)g.hasEgl("EGL_ANDROID_get_native_client_buffer"), (int)(g.pGetNativeClientBuffer != nullptr))); return; } if (!e.pTargetRb || !e.pTargetTexture) { record(kT4AhbRow, "UNSUPPORTED", fmt("GL_OES_EGL_image=%d but glEGLImageTargetRenderbufferStorageOES=%d glEGLImageTargetTexture2DOES=%d: " "this row clears through a renderbuffer and reads back through the texture path, and one of the " "two is absent", (int)g.hasGl("GL_OES_EGL_image"), (int)(e.pTargetRb != nullptr), (int)(e.pTargetTexture != nullptr))); return; } // ---- 1. the buffer, allocated here, described verbatim ------------------- AHardwareBuffer_Desc desc{}; desc.width = w; desc.height = h; desc.layers = 1; desc.format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN; AHardwareBuffer* ahb = nullptr; const int allocRc = AHardwareBuffer_allocate(&desc, &ahb); if (allocRc != 0 || !ahb) { record(kT4AhbRow, "UNSUPPORTED", fmt("AHardwareBuffer_allocate(width=%u height=%u layers=1 format=AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM " "usage=0x%llx) rc=%d errno=%d(%s): this side cannot mint the image at all, so the " "Android-minted direction has nothing to measure", w, h, (unsigned long long)desc.usage, allocRc, errno, strerror(errno))); return; } AHardwareBuffer_Desc back{}; AHardwareBuffer_describe(ahb, &back); pr("T4-ahb: AHardwareBuffer_allocate rc=%d, AHardwareBuffer_describe -> width=%u height=%u layers=%u " "format=0x%x usage=0x%llx stride=%u rfu0=%u rfu1=%llu", allocRc, back.width, back.height, back.layers, (unsigned)back.format, (unsigned long long)back.usage, back.stride, back.rfu0, (unsigned long long)back.rfu1); if (back.width == 0 || back.height == 0 || back.stride == 0) { record(kT4AhbRow, "FAIL", fmt("AHardwareBuffer_describe returned a degenerate layout: %ux%u layers=%u stride=%u", back.width, back.height, back.layers, back.stride)); AHardwareBuffer_release(ahb); return; } // ---- 2. import it HERE: client buffer -> EGLImage -> renderbuffer -------- EGLClientBuffer cb = g.pGetNativeClientBuffer(ahb); const EGLint cbErr = eglGetError(); pr("T4-ahb: eglGetNativeClientBufferANDROID(ahb=%p) -> %p, eglGetError=0x%04x", (void*)ahb, (void*)cb, (unsigned)cbErr); if (!cb) { record(kT4AhbRow, "FAIL", fmt("eglGetNativeClientBufferANDROID(ahb) returned NULL with eglGetError=0x%04x, for an " "AHardwareBuffer this same driver allocated one call earlier (%ux%u stride=%u)", (unsigned)cbErr, back.width, back.height, back.stride)); AHardwareBuffer_release(ahb); return; } const EGLint imgAttrs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; EGLImageKHR img = e.pCreateImage(g.dpy, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, cb, imgAttrs); const EGLint imgErr = eglGetError(); pr("T4-ahb: eglCreateImageKHR(EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, clientBuffer=%p) -> %p, " "eglGetError=0x%04x", (void*)cb, (void*)img, (unsigned)imgErr); if (img == EGL_NO_IMAGE_KHR) { record(kT4AhbRow, "FAIL", fmt("eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) -> EGL_NO_IMAGE_KHR with eglGetError=0x%04x: the " "driver took the client buffer (%p) and then refused the image", (unsigned)imgErr, (void*)cb)); AHardwareBuffer_release(ahb); return; } GLuint rb = 0, fboRb = 0, tex = 0, fboTex = 0, prog = 0; std::string stepFail; glGenRenderbuffers(1, &rb); glBindRenderbuffer(GL_RENDERBUFFER, rb); glDrain(); e.pTargetRb(GL_RENDERBUFFER, (GLeglImageOES)img); const GLenum rbErr = glDrain(); pr("T4-ahb: glEGLImageTargetRenderbufferStorageOES(GL_RENDERBUFFER, image) -> %s", glErrStr(rbErr).c_str()); if (rbErr != GL_NO_ERROR) stepFail = "glEGLImageTargetRenderbufferStorageOES -> " + glErrStr(rbErr); glGenFramebuffers(1, &fboRb); glBindFramebuffer(GL_FRAMEBUFFER, fboRb); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb); const GLenum fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); const GLenum attachErr = glDrain(); pr("T4-ahb: image renderbuffer FBO completeness=0x%04x (%s), glGetError=%s", (unsigned)fboStatus, fboStatus == GL_FRAMEBUFFER_COMPLETE ? "GL_FRAMEBUFFER_COMPLETE" : "not complete", glErrStr(attachErr).c_str()); if (stepFail.empty() && (fboStatus != GL_FRAMEBUFFER_COMPLETE || attachErr != GL_NO_ERROR)) { stepFail = fmt("the image renderbuffer attached but the FBO is 0x%04x (%s)", (unsigned)fboStatus, glErrStr(attachErr).c_str()); } if (!stepFail.empty()) { record(kT4AhbRow, "FAIL", fmt("%s | AHB[%ux%u layers=%u format=0x%x usage=0x%llx stride=%u] | fourcc=0x%08x seed=%s | image=%p " "egl=0x%04x clientBuffer=%p egl=0x%04x | nothing was drawn and nothing was offered", stepFail.c_str(), back.width, back.height, back.layers, (unsigned)back.format, (unsigned long long)back.usage, back.stride, kT4AhbFourcc, t4Hex4(kT4AhbSeed).c_str(), (void*)img, (unsigned)imgErr, (void*)cb, (unsigned)cbErr)); glDeleteFramebuffers(1, &fboRb); glDeleteRenderbuffers(1, &rb); e.pDestroyImage(g.dpy, img); AHardwareBuffer_release(ahb); return; } // The clear is what the peer's two pixels are compared against, so it goes // through the same clear + glFinish + fence path the row above uses, and that // fence's wait result is reported verbatim. const std::string clearFence = t4WritePath(fboRb, w, h, plan.clear); pr("T4-ahb: clear %s into the renderbuffer FBO: %s | %s", t4Hex4(kT4AhbSeed).c_str(), clearFence.c_str(), plan.note.c_str()); // ---- 3. hand it over: socketpair + SCM_RIGHTS, exactly as T0 does ------- T4AhbOffer off{}; off.magic = kT4Magic; off.version = 1; off.width = back.width; off.height = back.height; off.stride = back.stride * kT4AhbBytesPerPixel; // describe() reports pixels; the wire wants bytes off.fourcc = kT4AhbFourcc; memcpy(off.seed, kT4AhbSeed, 4); snprintf(off.note, sizeof(off.note), "clear=[%s] fence=[%s] modifier=DRM_FORMAT_MOD_LINEAR assumed", t4Hex4(kT4AhbSeed).c_str(), clearFence.c_str()); off.note[sizeof(off.note) - 1] = 0; int sp[2] = {-1, -1}; int sendRc = -1; std::string spDesc = "no socketpair"; std::string sendFail; if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sp) != 0) { sendRc = -errno; sendFail = fmt("socketpair -> errno=%d(%s)", errno, strerror(errno)); } else { spDesc = describeFd(sp[1]); sendRc = AHardwareBuffer_sendHandleToUnixSocket(ahb, sp[0]); if (sendRc != 0) { sendFail = fmt("AHardwareBuffer_sendHandleToUnixSocket(ahb, socketpair[0]) -> %d errno=%d(%s)", sendRc, errno, strerror(errno)); } close(sp[0]); if (sendRc == 0 && !sendMsg(sock, MSG_T4_AHB_OFFER, &off, sizeof(off), sp[1])) { sendRc = -errno; sendFail = fmt("sendMsg(MSG_T4_AHB_OFFER, fd=socketpair[1]) -> errno=%d(%s)", errno, strerror(errno)); } close(sp[1]); } const bool offered = sendRc == 0; pr("T4-ahb: MSG_T4_AHB_OFFER %ux%u stride=%u B (%u px x %u B/px) fourcc=0x%08x seed=%s note=[%s] " "handle-send=%d fd[%s]", off.width, off.height, off.stride, back.stride, kT4AhbBytesPerPixel, off.fourcc, t4Hex4(off.seed).c_str(), off.note, sendRc, spDesc.c_str()); // ---- 4. the peer's answer ------------------------------------------------ T4AhbAck ack{}; size_t ackGot = 0; std::string ackFail; std::string ackSkipped; bool ackRecv = false; if (offered) { // The dma-buf row's last step shortens the socket timeout to 2s to bound its // wait for a frame the peer never sends; this wait is a real exchange and // takes the leg's own budget back. setRecvTimeout(sock, 60); ackRecv = t4RecvTag(sock, MSG_T4_AHB_ACK, &ack, sizeof(ack), &ackGot, nullptr, &ackSkipped, &ackFail); } else { ackFail = sendFail; } if (ackRecv) { ack.note[sizeof(ack.note) - 1] = 0; pr("T4-ahb: MSG_T4_AHB_ACK imported=%d matched=0x%x stride=%u observed=%s observedCorner=%s pattern=%s " "note=[%s] skipped=[%s]", (int)ack.imported, (int)ack.matched, ack.stride, t4Hex4(ack.observed).c_str(), t4Hex4(ack.observedCorner).c_str(), t4Hex4(ack.pattern).c_str(), ack.note, ackSkipped.c_str()); } else if (offered) { pr("T4-ahb: no MSG_T4_AHB_ACK: %s", ackFail.c_str()); } // ---- 5. this side's own read: the texture path, at both pixels ----------- // Taken AFTER the peer's ack, so what comes back is its answer and not the // colour this side cleared a moment ago. std::string readFail; bool readDone = false; uint8_t texWord[4] = {0, 0, 0, 0}, texCorner[4] = {0, 0, 0, 0}; glGenTextures(1, &tex); glBindTexture(GL_TEXTURE_2D, tex); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glDrain(); e.pTargetTexture(GL_TEXTURE_2D, (GLeglImageOES)img); const GLenum texErr = glDrain(); pr("T4-ahb: glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image) -> %s", glErrStr(texErr).c_str()); if (texErr != GL_NO_ERROR) { readFail = "glEGLImageTargetTexture2DOES -> " + glErrStr(texErr); } else { glGenFramebuffers(1, &fboTex); glBindFramebuffer(GL_FRAMEBUFFER, fboTex); glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0); const GLenum texFboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); const GLenum texAttachErr = glDrain(); if (texFboStatus != GL_FRAMEBUFFER_COMPLETE || texAttachErr != GL_NO_ERROR) { readFail = fmt("the image texture attached but the FBO is 0x%04x (%s)", (unsigned)texFboStatus, glErrStr(texAttachErr).c_str()); } } if (readFail.empty()) { std::string progFail; prog = t4BuildReadProgram(&progFail); if (!prog) { readFail = progFail; } else { const GLint locTex = glGetUniformLocation(prog, "uTex"); const GLint locTexel = glGetUniformLocation(prog, "uTexel"); std::string failA, failB; const bool okA = t4ReadTexel(prog, locTex, locTexel, tex, w, h, 0, 0, texWord, &failA); const bool okB = t4ReadTexel(prog, locTex, locTexel, tex, w, h, farX, farY, texCorner, &failB); readDone = okA && okB; if (!okA) readFail = "pixel(0,0): " + failA; if (!okB) readFail += (readFail.empty() ? "" : "; ") + std::string("pixel(w-1,h-1): ") + failB; const std::string rf = readFail.empty() ? std::string("-") : readFail; pr("T4-ahb: texture-path read word=%s corner=%s readDone=%d fail=[%s]", t4Hex4(texWord).c_str(), t4Hex4(texCorner).c_str(), (int)readDone, rf.c_str()); } } // ---- 6. the row's verdict: one decisive leg, both halves named ----------- Leg l; l.name = "ahb"; l.decisive = true; l.attempted = offered; l.readOk = ackRecv && ack.imported && (ack.matched & 3) == 3 && !memcmp(ack.observed, kT4AhbSeed, 4) && !memcmp(ack.observedCorner, kT4AhbSeed, 4); l.writeOk = ackRecv && readDone && !memcmp(texWord, ack.pattern, 4) && !memcmp(texCorner, ack.pattern, 4); if (!offered) { l.fail = "the offer never went out: " + sendFail; } else if (!ackRecv) { l.fail = "no MSG_T4_AHB_ACK: " + ackFail; } else if (!ack.imported) { l.fail = fmt("the container's gbm_bo_import refused the fd: peer note=[%s]", ack.note); } else if (!l.readOk) { l.fail = fmt("the peer did not see the clear colour at both pixels: matched=0x%x observed=%s corner=%s " "want=%s", (unsigned)ack.matched, t4Hex4(ack.observed).c_str(), t4Hex4(ack.observedCorner).c_str(), t4Hex4(kT4AhbSeed).c_str()); } else if (!readDone) { l.fail = "texture-path read of the same image: " + readFail; } else if (!l.writeOk) { l.fail = fmt("texture-path read is not the peer's pattern: word=%s corner=%s pattern=%s", t4Hex4(texWord).c_str(), t4Hex4(texCorner).c_str(), t4Hex4(ack.pattern).c_str()); } std::vector legs{l}; std::string why; const char* status = legVerdict(legs, &why); // Every raw value goes into the row, in the shape the other legs use: the leg // trace first, then what each step actually saw, then the failure. std::string detail = fmt( "%s | AHB[%ux%u layers=%u format=0x%x usage=0x%llx stride=%u rfu0=%u rfu1=%llu] | fourcc=0x%08x [%s] | " "seed=%s | egl[clientBuffer=%p err=0x%04x image=%p err=0x%04x] | rbFbo[status=0x%04x glErr=%s] | " "clear[%s] | offer[sent=%d handleSend=%d fd[%s] note=[%s]] | ack[len=%zu got=%d imported=%d matched=0x%x " "stride=%u observed=%s observedCorner=%s pattern=%s note=[%s] skipped=[%s]] | texRead[done=%d word=%s " "corner=%s fail=[%s]] | %s | peer [%s] %s", legTrace(legs).c_str(), back.width, back.height, back.layers, (unsigned)back.format, (unsigned long long)back.usage, back.stride, back.rfu0, (unsigned long long)back.rfu1, kT4AhbFourcc, plan.note.c_str(), t4Hex4(kT4AhbSeed).c_str(), (void*)cb, (unsigned)cbErr, (void*)img, (unsigned)imgErr, (unsigned)fboStatus, glErrStr(attachErr).c_str(), clearFence.c_str(), (int)offered, sendRc, spDesc.c_str(), off.note, ackGot, (int)ackRecv, ackRecv ? (int)ack.imported : -1, ackRecv ? (int)ack.matched : 0, ack.stride, t4Hex4(ack.observed).c_str(), t4Hex4(ack.observedCorner).c_str(), t4Hex4(ack.pattern).c_str(), ackRecv ? ack.note : ackFail.c_str(), ackSkipped.c_str(), (int)readDone, t4Hex4(texWord).c_str(), t4Hex4(texCorner).c_str(), readFail.empty() ? "-" : readFail.c_str(), why.c_str(), lay.peer, lay.note); record(kT4AhbRow, status, detail); // The EGLImage outlives every GL object that wraps it, and the AHardwareBuffer is // what the image was created from, so the teardown order is the one the row above // already uses. if (prog) glDeleteProgram(prog); if (fboTex) glDeleteFramebuffers(1, &fboTex); if (fboRb) glDeleteFramebuffers(1, &fboRb); if (tex) glDeleteTextures(1, &tex); if (rb) glDeleteRenderbuffers(1, &rb); e.pDestroyImage(g.dpy, img); AHardwareBuffer_release(ahb); } // --------------------------------------------------------------------------- // T4, the other end: the buffer the DISPLAY HOST is about to scan out // --------------------------------------------------------------------------- // // WHY THIS LEG IS THE ONE THE PRODUCT TURNS ON. The rows above ask what a driver will take // FROM a container. This one asks the question the architecture actually depends on: can // the render server draw into the buffer the display host is about to put on the glass? On // an Anland-style host that is not a matter of taste -- the host dequeues its scanout // buffers from its own SurfaceView, and nothing the server allocates for itself will ever be // scanned out. What makes it possible is that such a buffer is a real gralloc allocation, // and the platform hands back an AHardwareBuffer for that handle (measured: handle type 2 // accepts it, on the device, in the untrusted_app domain). // // THE ROUND TRIP IS CLOSED FROM THIS SIDE, because only the host can say whether the drawing // arrived where the screen will look. The host offers; this side imports the image, clears a // colour through it behind a fence, and answers; the host reads both pixels back through its // own view of the same buffer and returns them; this side decides. A row that said OK on // "an EGLImage was created" would be reporting that the platform took the handle, which is // not the question. // Mirrors struct ahb_offer / ahb_ack / ahb_seen in the display host's ahb_bridge.h. The // handle travels ahead of the sideband on the same socket, which is the order both ends read. struct T4HostOffer { uint32_t magic, version, index, width, height, stride, format, usage_lo, usage_hi; char note[64]; } __attribute__((packed)); struct T4HostAck { uint32_t magic, version, index, imported, drawn, fence_ok; uint8_t color[4]; char note[64]; } __attribute__((packed)); struct T4HostSeen { uint32_t magic, version, index, locked; uint8_t observed[4]; uint8_t observed_corner[4]; char note[64]; } __attribute__((packed)); static int t4Pending(int fd) { int n = -1; return ioctl(fd, FIONREAD, &n) == 0 ? n : -1; } // The magic the display host's ahb_bridge.h stamps on all three of its messages. It is NOT // kT4Magic: that one belongs to the dma-buf protocol between this probe and the container // peer, and using it here failed a good round trip while reporting the wrong reason -- the // handle had arrived, the description had arrived, and only the comparison was wrong. static const uint32_t kT4HostMagic = 0x4D474C41u; // "ALGM" in memory order static void runT4Host(GlCtx& g, bool glOk, const char* path) { if (!glOk) { record(kT4HostRow, "SKIP", "no headless GLES context: nothing on this side could take the host's image"); return; } // The entry points this leg is spelled in, loaded here rather than shared with runT4: // the two legs are run on their own (--only-t4 / --only-t4-host), and a load that one of // them needs must not depend on the other having run first. T4Egl e; e.pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR"); e.pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR"); e.pTargetRb = (PFNGLEGLIMAGETARGETRENDERBUFFERSTORAGEOESPROC)eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"); if (!g.hasEgl("EGL_KHR_image_base") || !e.pCreateImage || !e.pDestroyImage || !e.pTargetRb) { record(kT4HostRow, "UNSUPPORTED", fmt("EGL_KHR_image_base=%d eglCreateImageKHR=%d eglDestroyImageKHR=%d " "glEGLImageTargetRenderbufferStorageOES=%d: the host's image cannot be taken and drawn into", (int)g.hasEgl("EGL_KHR_image_base"), (int)(e.pCreateImage != nullptr), (int)(e.pDestroyImage != nullptr), (int)(e.pTargetRb != nullptr))); return; } if (!g.pGetNativeClientBuffer) { record(kT4HostRow, "UNSUPPORTED", "EGL_ANDROID_get_native_client_buffer is not advertised, so an AHardwareBuffer cannot become an " "EGLClientBuffer and the host's image cannot be taken at all"); return; } const int lfd = rtListen(path); if (lfd < 0) { record(kT4HostRow, "FAIL", fmt("could not listen on %s: errno=%d(%s)", path, errno, strerror(errno))); return; } setRecvTimeout(lfd, 60); const int fd = accept(lfd, nullptr, nullptr); const int acceptErr = errno; close(lfd); if (fd < 0) { record(kT4HostRow, "FAIL", fmt("no display host dialled %s within the accept timeout: errno=%d(%s)", path, acceptErr, strerror(acceptErr))); return; } setRecvTimeout(fd, 60); pr("T4-host: a display host dialled %s; waiting for its buffers", path); const T4ColorPlan plan = t4PlanColor(kT4AhbFourcc, kT4AhbSeed); int offered = 0, imported = 0, drew = 0, seen = 0, agreed = 0; std::string trace, fail; for (int i = 0; i < 16; ++i) { const int pendBefore = t4Pending(fd); AHardwareBuffer* ahb = nullptr; const int hrc = AHardwareBuffer_recvHandleFromUnixSocket(fd, &ahb); const int pendAfter = t4Pending(fd); /* The handle message footprint, measured rather than assumed: a receiver that leaves * bytes behind hands the description reader whatever was left. */ pr("T4-host: recvHandle rc=%d pending %d -> %d", hrc, pendBefore, pendAfter); if (hrc != 0 || !ahb) break; T4HostOffer of{}; const bool ofRead = readAll(fd, &of, sizeof(of)); if (!ofRead || of.magic != kT4HostMagic) { pr("T4-host: sideband read=%d magic=0x%08x first=%02x %02x %02x %02x", (int)ofRead, of.magic, ((const unsigned char *)&of)[0], ((const unsigned char *)&of)[1], ((const unsigned char *)&of)[2], ((const unsigned char *)&of)[3]); char why[160]; snprintf(why, sizeof(why), "handle rc=%d (pending %d -> %d) then sideband read=%d magic=0x%08x", hrc, pendBefore, pendAfter, (int)ofRead, of.magic); fail = why; AHardwareBuffer_release(ahb); break; } ++offered; AHardwareBuffer_Desc d{}; AHardwareBuffer_describe(ahb, &d); T4HostAck ack{}; ack.magic = kT4HostMagic; ack.version = 1; ack.index = of.index; EGLClientBuffer cb = g.pGetNativeClientBuffer((const struct AHardwareBuffer*)ahb); const EGLint cbErr = eglGetError(); EGLImageKHR img = EGL_NO_IMAGE_KHR; if (cb) { const EGLint attrs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; img = e.pCreateImage(g.dpy, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, cb, attrs); } const EGLint imgErr = eglGetError(); if (img == EGL_NO_IMAGE_KHR) { trace += fmt("buf%u=image(0x%04x/0x%04x) ", of.index, (unsigned)cbErr, (unsigned)imgErr); snprintf(ack.note, sizeof(ack.note), "clientBuffer=0x%04x image=0x%04x", (unsigned)cbErr, (unsigned)imgErr); (void)writeAll(fd, &ack, sizeof(ack)); AHardwareBuffer_release(ahb); continue; } ++imported; ack.imported = 1; GLuint rb = 0, fbo = 0; glGenRenderbuffers(1, &rb); glBindRenderbuffer(GL_RENDERBUFFER, rb); glDrain(); e.pTargetRb(GL_RENDERBUFFER, (GLeglImageOES)img); const GLenum rbErr = glDrain(); glGenFramebuffers(1, &fbo); glBindFramebuffer(GL_FRAMEBUFFER, fbo); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb); const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); const GLenum attachErr = glDrain(); if (rbErr == GL_NO_ERROR && status == GL_FRAMEBUFFER_COMPLETE && attachErr == GL_NO_ERROR) { const std::string fence = t4WritePath(fbo, d.width, d.height, plan.clear); ack.drawn = 1; ack.fence_ok = fence.find("GL_TIMEOUT_EXPIRED") == std::string::npos && fence.find("GL_WAIT_FAILED") == std::string::npos; memcpy(ack.color, kT4AhbSeed, 4); snprintf(ack.note, sizeof(ack.note), "%ux%u stride=%u(px) cleared %s | %s", d.width, d.height, d.stride, t4Hex4(kT4AhbSeed).c_str(), fence.c_str()); ++drew; } else { snprintf(ack.note, sizeof(ack.note), "rb=0x%04x fbo=0x%04x attach=0x%04x: not drawable", (unsigned)rbErr, (unsigned)status, (unsigned)attachErr); } (void)writeAll(fd, &ack, sizeof(ack)); T4HostSeen back{}; if (readAll(fd, &back, sizeof(back)) && back.magic == kT4HostMagic) { ++seen; const bool first = !memcmp(back.observed, kT4AhbSeed, 4); const bool corner = !memcmp(back.observed_corner, kT4AhbSeed, 4); if (back.locked && first && corner) ++agreed; trace += fmt("buf%u=%s/%s ", of.index, first ? "first-ok" : "first-bad", corner ? "corner-ok" : "corner-bad"); } else { trace += fmt("buf%u=no-readback ", of.index); } if (fbo) glDeleteFramebuffers(1, &fbo); if (rb) glDeleteRenderbuffers(1, &rb); e.pDestroyImage(g.dpy, img); AHardwareBuffer_release(ahb); } close(fd); if (offered == 0) { record(kT4HostRow, "FAIL", fmt("the display host dialled %s but offered no buffer: %s", path, fail.empty() ? "the connection closed first" : fail.c_str())); return; } std::vector legs; Leg leg; leg.name = "host"; leg.decisive = true; leg.attempted = true; leg.readOk = imported == offered && drew == imported; // the server got in and drew leg.writeOk = seen == offered && agreed == seen; // and the host saw it at both pixels if (!leg.writeOk) leg.fail = fmt("drawn=%d/%d hostReadback=%d/%d agreed=%d", drew, offered, seen, offered, agreed); legs.push_back(leg); std::string why; const char* status = legVerdict(legs, &why); record(kT4HostRow, status, fmt("%s | offered=%d imported=%d drawn=%d hostReadback=%d agreed=%d | %s | seed=%s [%s] | server=%s | " "trace: %s", legTrace(legs).c_str(), offered, imported, drew, seen, agreed, why.c_str(), t4Hex4(kT4AhbSeed).c_str(), plan.note.c_str(), selfContext().c_str(), trace.c_str())); } #else // !PROBE_HAVE_AHB // The host build has neither a listener nor an AHardwareBuffer, so both T4 legs report // exactly that instead of inventing a device answer they cannot have. PROBE_HAVE_AHB is // simply the macro that carries rtListen() and the AHardwareBuffer entry points. static void runT4(GlCtx&, bool, const char*) { record(kT4Row, "SKIP", "host build has no listener: rtListen() is compiled with the AHardwareBuffer block, so the " "container peer has nothing to dial"); } static void runT4Host(GlCtx&, bool, const char*) { record(kT4HostRow, "SKIP", "host build has no listener and no AHardwareBuffer: the display host has nothing to dial"); } #endif // PROBE_HAVE_AHB // --------------------------------------------------------------------------- // main // --------------------------------------------------------------------------- static void printSummary() { char model[PROP_VALUE_MAX] = {0}; getProp("ro.product.model", model, sizeof(model)); std::string sec = readSmallFile("/proc/self/attr/current"); printf("\n=== extmem_probe summary (model=%s selinux=%s) ===\n", model, sec.c_str()); printf("NOTE: %s\n", kDomainCaveat); printf("%-34s %-12s %s\n", "ROUTE", "STATUS", "DETAIL"); for (const RouteResult& r : gResults) printf("%-34s %-12s %s\n", r.route.c_str(), r.status.c_str(), r.detail.c_str()); printf("=== end ===\n"); fflush(stdout); } // The trace APK's exec hook (spawn_spike.cpp) runs ` ` from the app // process and cannot pass options. So a positional argument is taken as that marker // path: options are read from `.args` (whitespace-separated, optional), and // at the end a one-line verdict is written to the marker so the hook's report reads OK. static std::vector readArgsFile(const std::string& path) { std::vector out; FILE* f = fopen(path.c_str(), "r"); if (!f) return out; char tok[256]; while (fscanf(f, "%255s", tok) == 1) out.push_back(tok); fclose(f); return out; } int main(int argc, char** argv) { // P11 B2 step 1: exec'd by MobileGLServerService as `libMobileGLServer.so --serve`. if (argc >= 3 && !strcmp(argv[2], "--serve")) return routeServe(argv[1]); uint64_t size = kDefaultSize; bool routeClientMode = false; std::string routeEndpoint; std::string t4Endpoint = "@mgl-t4"; // The other end of T4: the buffer the display host is about to scan out. A path rather // than an abstract name, because the host is the one that dials and that is where its // bridge looks for a listener. bool doT4Host = false; std::string t4HostPath = "/data/local/tmp/mobilegl_bridge.sock"; const char* childRoute = nullptr; bool doT1 = true, doT0 = true, doT3 = true, doGles = true; // T4 waits for a peer that only exists when it is asked for, so it is off unless // --only-t4 turns it on. bool doT4 = false; bool regionsAtEnd = false; uint32_t sustainedRounds = 0; std::string markerPath; std::vector args; for (int i = 1; i < argc; ++i) { if (argv[i][0] != '-' && markerPath.empty()) { markerPath = argv[i]; } else { args.push_back(argv[i]); } } std::vector fileArgs; if (!markerPath.empty()) { fileArgs = readArgsFile(markerPath + ".args"); args.insert(args.end(), fileArgs.begin(), fileArgs.end()); } for (const std::string& s : args) { const char* a = s.c_str(); if (!strncmp(a, "--child=", 8)) { static std::string route; // outlives `args` route = a + 8; childRoute = route.c_str(); } else if (!strcmp(a, "--route-client")) { routeClientMode = true; } else if (!strncmp(a, "--route-client=", 15)) { routeClientMode = true; routeEndpoint = a + 15; } else if (!strncmp(a, "--size=", 7)) { size = strtoull(a + 7, nullptr, 0); } else if (!strncmp(a, "--hold-ms=", 10)) { gHoldMs = (uint32_t)strtoul(a + 10, nullptr, 0); } else if (!strncmp(a, "--region-base=", 14)) { gRegionBase = strtoull(a + 14, nullptr, 0); } else if (!strcmp(a, "--regions-at-end")) { regionsAtEnd = true; } else if (!strncmp(a, "--sustained-lock=", 17)) { sustainedRounds = (uint32_t)strtoul(a + 17, nullptr, 0); } else if (!strcmp(a, "--sustained-lock")) { sustainedRounds = 8; } else if (!strcmp(a, "--only-t1")) { doT0 = doT3 = doT4 = false; } else if (!strcmp(a, "--only-t0")) { doT1 = doT3 = doGles = doT4 = false; } else if (!strcmp(a, "--only-sustained")) { doT1 = doT0 = doT3 = doGles = doT4 = false; if (!sustainedRounds) sustainedRounds = 8; } else if (!strcmp(a, "--only-t3")) { doT1 = doT0 = doGles = doT4 = false; } else if (!strcmp(a, "--only-gles")) { doT1 = doT0 = doT3 = doT4 = false; } else if (!strcmp(a, "--only-t4")) { doT1 = doT0 = doT3 = doGles = false; doT4 = true; } else if (!strncmp(a, "--only-t4=", 10)) { doT1 = doT0 = doT3 = doGles = false; doT4 = true; t4Endpoint = a + 10; } else if (!strcmp(a, "--only-t4-host")) { doT1 = doT0 = doT3 = doGles = doT4 = false; doT4Host = true; } else if (!strncmp(a, "--only-t4-host=", 15)) { doT1 = doT0 = doT3 = doGles = doT4 = false; doT4Host = true; t4HostPath = a + 15; } else if (!strcmp(a, "--no-gles")) { doGles = false; } else if (!strcmp(a, "--help")) { printf("usage: extmem_probe [markerPath] [--size=BYTES] [--regions-at-end] [--sustained-lock[=ROUNDS]]\n" " extmem_probe --route-client[=@endpoint] [--size=BYTES] [--sustained-lock=ROUNDS] [--regions-at-end]\n" " extmem_probe --serve (packaged as libMobileGLServer.so)\n" " [--only-t0|--only-t1|--only-t3|--only-gles|--only-sustained|--only-t4[=@endpoint]\n" " |--only-t4-host[=PATH]] [--no-gles]\n" " --only-t4-host[=PATH]: listen on /data/local/tmp/mobilegl_bridge.sock (default) for a\n" " display host offering the buffer it is about to scan out; the round\n" " trip is closed by that host reading its own pixels back.\n" " --only-t4[=@endpoint]: listen on @mgl-t4 (default) for the droidspaces container peer;\n" " one connection carries both T4 rows: T4-image-import (its dma-buf\n" " into this side) and T4-ahb-image (this side's AHardwareBuffer into\n" " the container, sent as an fd over SCM_RIGHTS)\n" " markerPath: exec-hook mode; options are also read from .args\n"); return 0; } } if (size < kRegionCount * kRegion) size = kRegionCount * kRegion; size = (size + kRegion - 1) / kRegion * kRegion; if (regionsAtEnd && !childRoute) gRegionBase = size - kRegionCount * kRegion; if (routeClientMode) return routeClient(routeEndpoint.c_str(), size, sustainedRounds ? sustainedRounds : 4, regionsAtEnd); // A peer that has already exited must not take this process down with it. signal(SIGPIPE, SIG_IGN); if (childRoute) { static char roleBuf[32]; snprintf(roleBuf, sizeof(roleBuf), "child:%s", childRoute); gRole = roleBuf; int sock = 3; if (!strcmp(childRoute, "t1")) return childT1(sock); if (!strcmp(childRoute, "t1gl")) return childT1Gl(sock); if (!strcmp(childRoute, "t0")) return childT0(sock); if (!strcmp(childRoute, "t0s")) return childT0Sustained(sock); if (!strcmp(childRoute, "t3")) return childT3(sock); if (!strcmp(childRoute, "t3c")) return childT3Client(sock); pr("unknown child route %s", childRoute); return 1; } std::string joined; for (const std::string& s : args) joined += " " + s; pr("extmem_probe: MobileGL disaggregation spike B, size=%llu bytes regionBase=%llu sustainedRounds=%u " "marker=%s args=[%s ]", (unsigned long long)size, (unsigned long long)gRegionBase, sustainedRounds, markerPath.empty() ? "-" : markerPath.c_str(), joined.c_str()); printRunContext(); VkCtx c; bool vkOk = vkCtxInit(c, true); GlCtx g; bool glOk = glCtxInit(g); if (!vkOk) { record("vulkan-init", "FAIL", "no usable Vulkan device"); printSummary(); return 1; } phaseEnumerate(c, g, glOk); pr("=== phase T1: server-exported allocation (opaque fd / dma-buf) ==="); if (doT1) { runT1Parent(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT, "T1-opaque-fd", size); runT1Parent(c, VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT, "T1-dma-buf", size); } pr("=== phase T1-gles: the same export imported as GL buffer storage ==="); if (doGles) runT1GlesParent(c, g, glOk, size); pr("=== phase T0: client-allocated AHardwareBuffer BLOB ==="); if (doT0) runT0Parent(c, g, glOk, size); pr("=== phase T0S: client-allocated AHardwareBuffer, lock held across GPU rounds ==="); if (sustainedRounds) runT0SustainedParent(c, g, glOk, size, sustainedRounds); pr("=== phase T3: VK_EXT_external_memory_host ==="); if (doT3) { runT3Parent(c, size); runT3ClientAllocParent(c, size); } pr("=== phase T4: an image allocated outside Android (dma-buf from the container peer) ==="); if (doT4) runT4(g, glOk, t4Endpoint.c_str()); if (doT4Host) { pr("T4-host: listening on %s for the display host buffers", t4HostPath.c_str()); runT4Host(g, glOk, t4HostPath.c_str()); } glCtxDestroy(g); vkCtxDestroy(c); printSummary(); if (!markerPath.empty()) { int ok = 0, partial = 0, fail = 0, unsup = 0, skip = 0; for (const RouteResult& r : gResults) { if (r.status == "OK") ++ok; else if (r.status == "PARTIAL") ++partial; else if (r.status == "FAIL") ++fail; else if (r.status == "UNSUPPORTED") ++unsup; else ++skip; } FILE* m = fopen(markerPath.c_str(), "w"); if (m) { fprintf(m, "extmem_probe done selinux=%s rows=%zu OK=%d PARTIAL=%d FAIL=%d UNSUPPORTED=%d SKIP=%d\n", readSmallFile("/proc/self/attr/current").c_str(), gResults.size(), ok, partial, fail, unsup, skip); fclose(m); } } return 0; }