diff --git a/tools/spikes/extmem_probe/CMakeLists.txt b/tools/spikes/extmem_probe/CMakeLists.txt new file mode 100644 index 00000000..2f8242d3 --- /dev/null +++ b/tools/spikes/extmem_probe/CMakeLists.txt @@ -0,0 +1,34 @@ +# Standalone NDK command-line probe for MobileGL disaggregation spike B +# (plan-B §8.3 / §11 P0). Deliberately NOT part of the MobileGL build graph: +# it links nothing from the project and is configured on its own, e.g. +# +# cmake -S tools/spikes/extmem_probe -B /tmp/extmem-build -G Ninja \ +# -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ +# -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 \ +# -DCMAKE_BUILD_TYPE=RelWithDebInfo +# +# See build_android.sh for the exact invocation used on the devices. + +cmake_minimum_required(VERSION 3.16) +project(extmem_probe LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(extmem_probe extmem_probe.cpp) + +# PIE is the NDK default for executables; make it explicit so a stale toolchain +# cannot produce something Android's linker refuses to run. +target_compile_options(extmem_probe PRIVATE -fPIE -Wall -Wextra -Wno-unused-parameter) +target_link_options(extmem_probe PRIVATE -pie) + +if(ANDROID) + target_link_libraries(extmem_probe PRIVATE vulkan EGL GLESv3 android log) +else() + # Host build: T0 (AHardwareBuffer) compiles out, T1/T3 stay. Its only purpose is + # to validate this harness against a driver that is known to implement the + # routes (lavapipe), so that a device-side FAIL is attributable to the device + # driver and not to the probe. It is NOT a substitute for a device run. + message(STATUS "extmem_probe: host build -- T1/T3 harness validation only, T0 disabled") + target_link_libraries(extmem_probe PRIVATE vulkan EGL GLESv2) +endif() diff --git a/tools/spikes/extmem_probe/README.md b/tools/spikes/extmem_probe/README.md new file mode 100644 index 00000000..a9a6b74f --- /dev/null +++ b/tools/spikes/extmem_probe/README.md @@ -0,0 +1,80 @@ +# extmem_probe — disaggregation spike B (external memory) + +A standalone Android command-line probe that answers one question per device: + +> Can a server-allocated `HOST_VISIBLE|HOST_COHERENT` `VkDeviceMemory` be shared +> with another process and mapped there, and by which route? + +This is the P0 spike that decides the `AcquirePersistentMap` tier in plan B §8.3 +(T0 = server imports a client allocation, T1 = server exports its own, T2 = give +up and return `nullptr`). It links nothing from MobileGL and is not part of the +project's CMake build graph. + +## What it does + +* **phase A — enumeration.** Vulkan device identity + memory types, and per + handle type (`OPAQUE_FD`, `DMA_BUF`, `HOST_ALLOCATION`, `AHARDWAREBUFFER`) the + `vkGetPhysicalDeviceExternalBufferProperties` verdict for the buffer usage + MobileGL actually needs. Then a headless EGL pbuffer context reports + `GL_EXT_memory_object{,_fd}`, `GL_EXT_external_buffer`, `GL_EXT_buffer_storage`, + `GL_OES_EGL_image_external{,_essl3}` and `EGL_ANDROID_get_native_client_buffer`. +* **T1 — server exports.** Allocates a `HOST_VISIBLE|HOST_COHERENT` buffer memory + with `VkExportMemoryAllocateInfo`, maps it, writes a pattern, exports an fd with + `vkGetMemoryFdKHR` (opaque-fd, then dma-buf), hands the fd to a second process + over `SCM_RIGHTS`, and has that process (a) `mmap()` the fd and (b) import it + into its own `VkDeviceMemory` and `vkMapMemory` it. Both sides write and both + sides compare, so a one-directional or copy-on-import mapping is caught. +* **T0 — server imports.** The second process allocates an `AHardwareBuffer` BLOB + (`CPU_READ_OFTEN|CPU_WRITE_OFTEN|GPU_DATA_BUFFER`), writes a pattern under + `AHardwareBuffer_lock`, and sends it with + `AHardwareBuffer_sendHandleToUnixSocket`. The first process reads it back three + ways — CPU lock, `VkDeviceMemory` imported through + `VK_ANDROID_external_memory_android_hardware_buffer`, and a GL buffer created + with `eglGetNativeClientBufferANDROID` + `glBufferStorageExternalEXT` mapped + persistent/coherent — writes through each, and the allocating process verifies + every write. +* **T3 — host pointer import.** If `VK_EXT_external_memory_host` is advertised, + imports a memfd-backed, alignment-corrected `mmap` region as a `VkDeviceMemory` + and maps it; also passes the memfd to the second process for a cross-process + round trip. + +Process topology mirrors the target design (the client spawns the server): the +probe re-execs `/proc/self/exe --child=` and hands the child one end of a +`socketpair` on fd 3. A bare `fork()` is not usable — neither side's Vulkan +driver survives it, and both sides need live Vulkan. + +## Build and run + +```sh +ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 ./build_android.sh /tmp/extmem-build +adb -s push /tmp/extmem-build/extmem_probe /data/local/tmp/p0-extmem/ +adb -s shell /data/local/tmp/p0-extmem/extmem_probe +``` + +There is also a host build (`cmake -S . -B ` with no toolchain file). It +compiles T0 out — `AHardwareBuffer` is Android-only — and exists for exactly one +reason: running T1/T3 against a driver that is known to implement them +(lavapipe: `VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json +EGL_PLATFORM=surfaceless`) proves the harness reports a working route as +working, which is what makes a device-side `FAIL` attributable to the device +driver rather than to this program. It is not a substitute for a device run. + +Options: `--size=BYTES` (default 65536; the payload is split into 4 KiB regions, +one per writer), `--only-t0` / `--only-t1` / `--only-t3`. + +Output is a per-route `RESULT ` line stream plus a +summary table; `status` is one of `OK`, `PARTIAL`, `UNSUPPORTED`, `FAIL`, `SKIP`. +Driver error codes are printed verbatim (`VkResult` names, `errno`, GL enums) — +that is the payload of the spike, so do not summarise them away. + +Two details worth knowing when reading T1 output: + +* the child reads through *both* the plain `mmap` and the imported + `VkDeviceMemory` before it writes through either, because a driver whose + exported fd maps at an offset would otherwise have its payload overwritten by + the probe's own first write, and the second read would report a false failure; +* when the direct compare fails, the child scans the mapping for the exporter's + payload and reports `payloadAt=`. `payloadAt=4096` with a clean Vulkan + import (lavapipe's answer) means the fd is shareable but its offset-0 is not + the allocation's base — a route that only works if that offset is + discoverable, which opaque-fd does not promise. diff --git a/tools/spikes/extmem_probe/build_android.sh b/tools/spikes/extmem_probe/build_android.sh new file mode 100755 index 00000000..acebf315 --- /dev/null +++ b/tools/spikes/extmem_probe/build_android.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Build the spike-B external-memory probe for arm64 Android. +# +# ANDROID_NDK=/path/to/ndk ./build_android.sh [build-dir] +# +# Produces /extmem_probe, a PIE arm64-v8a executable that depends +# only on the platform (libvulkan / libEGL / libGLESv3 / libandroid / liblog). +set -e + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD="${1:-$HERE/build-android}" +NDK="${ANDROID_NDK:-$HOME/android-sdk/ndk/27.3.13750724}" + +if [ ! -f "$NDK/build/cmake/android.toolchain.cmake" ]; then + echo "NDK not found at $NDK (set ANDROID_NDK)" >&2 + exit 1 +fi + +cmake -S "$HERE" -B "$BUILD" -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE="$NDK/build/cmake/android.toolchain.cmake" \ + -DANDROID_ABI=arm64-v8a \ + -DANDROID_PLATFORM=android-30 \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake --build "$BUILD" -j "$(nproc)" +echo "built: $BUILD/extmem_probe" diff --git a/tools/spikes/extmem_probe/extmem_probe.cpp b/tools/spikes/extmem_probe/extmem_probe.cpp new file mode 100644 index 00000000..2e28f0e9 --- /dev/null +++ b/tools/spikes/extmem_probe/extmem_probe.cpp @@ -0,0 +1,1871 @@ +// extmem_probe -- MobileGL disaggregation P0 spike B (plan-B §8.3, §11 P0). +// +// Question this program answers, per device: +// Can a server-allocated HOST_VISIBLE|HOST_COHERENT VkDeviceMemory be shared +// with another process and mapped there, 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) +// 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 over +// a memfd-backed mmap region) +// +// 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/T3 +// harness itself can be validated against a driver that is known to implement +// those routes (lavapipe), 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 + +#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 + +// --------------------------------------------------------------------------- +// 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, ...) { + 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, ...) { + char buf[1024]; + va_list ap; + va_start(ap, f); + vsnprintf(buf, sizeof(buf), f, ap); + va_end(ap); + return std::string(buf); +} + +static const char* 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: { + static char tmp[32]; + snprintf(tmp, sizeof(tmp), "VkResult(%d)", (int)r); + return tmp; + } + } +} + +// --------------------------------------------------------------------------- +// payload patterns +// --------------------------------------------------------------------------- + +static const uint64_t kRegion = 4096; // bytes per verification region +static const uint64_t kDefaultSize = 65536; + +// region indices inside the shared allocation +enum { + REG_A = 0, // first writer's payload + REG_B = 1, // importer write through the plain host mapping (mmap / AHB lock) + REG_C = 2, // importer write through the imported Vulkan mapping + REG_D = 3, // importer write through the imported GL mapping +}; + +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; +} + +static void writeRegion(void* base, int region, uint32_t seed) { + fillPattern((uint8_t*)base + region * kRegion, kRegion, seed); +} +static int64_t checkRegion(const void* base, int region, uint32_t seed) { + return checkPattern((const uint8_t*)base + region * kRegion, kRegion, seed); +} + +// --------------------------------------------------------------------------- +// 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_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; + 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)); + return false; + } + + 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)); + return false; + } + + 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", uuid, + (unsigned long long)c.minImportedHostPointerAlignment); + } + return true; +} + +static void vkCtxDestroy(VkCtx& c) { + if (c.device) vkDestroyDevice(c.device, nullptr); + if (c.instance) vkDestroyInstance(c.instance, nullptr); + 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; + +// --------------------------------------------------------------------------- +// 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; + + 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; + } +}; + +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"); + 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; +} + +// --------------------------------------------------------------------------- +// 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 self[512]; + ssize_t n = readlink("/proc/self/exe", self, sizeof(self) - 1); + if (n <= 0) _exit(90); + self[n] = 0; + char* argv[] = {self, arg, 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); +} + +// --------------------------------------------------------------------------- +// T1 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; +}; + +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 + 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 + int32_t wroteB; + int32_t wroteC; + char note[384]; +}; + +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 writtenMask; // bit0=B bit1=C bit2=D +}; + +struct T0Result { + int32_t lockOk; + int32_t lockErr; + int64_t mismatchB; + int64_t mismatchC; + int64_t mismatchD; + char note[192]; +}; + +struct T3Offer { + uint64_t size; + uint32_t seedA; + uint32_t seedB; +}; + +struct T3Result { + int32_t mmapOk; + int32_t mmapErrno; + int64_t mismatch; + char note[192]; +}; + +// --------------------------------------------------------------------------- +// Phase A: enumeration +// --------------------------------------------------------------------------- + +static const char* memFlagStr(VkMemoryPropertyFlags f) { + static char b[128]; + b[0] = 0; + if (f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) strcat(b, "DEVICE_LOCAL "); + if (f & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) strcat(b, "HOST_VISIBLE "); + if (f & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) strcat(b, "HOST_COHERENT "); + if (f & VK_MEMORY_PROPERTY_HOST_CACHED_BIT) strcat(b, "HOST_CACHED "); + if (f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT) strcat(b, "LAZY "); + if (f & VK_MEMORY_PROPERTY_PROTECTED_BIT) strcat(b, "PROTECTED "); + return b; +} + +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; + char feat[96]; + feat[0] = 0; + if (p.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) strcat(feat, "DEDICATED_ONLY "); + if (p.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) strcat(feat, "EXPORTABLE "); + if (p.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) strcat(feat, "IMPORTABLE "); + if (!feat[0]) strcat(feat, ""); + pr(" externalBuffer[%s]: features=%s exportFrom=0x%x compatible=0x%x", name, feat, + 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)); + } + + // 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"); + 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); +} + +// --------------------------------------------------------------------------- +// T1 parent +// --------------------------------------------------------------------------- + +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; + } + + // exportability report first -- a driver that says "not exportable" here and + // still returns an fd is a driver bug we want on the record. + VkPhysicalDeviceExternalBufferInfo ebi{}; + ebi.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_BUFFER_INFO; + ebi.usage = kProbeBufferUsage; + ebi.handleType = handleType; + VkExternalBufferProperties ebp{}; + ebp.sType = VK_STRUCTURE_TYPE_EXTERNAL_BUFFER_PROPERTIES; + vkGetPhysicalDeviceExternalBufferProperties(c.phys, &ebi, &ebp); + bool advertisedExportable = + (ebp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT) != 0; + pr("T1[%s] advertisedExportable=%d importable=%d", routeName, (int)advertisedExportable, + (int)((ebp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT) != 0)); + + VkExternalMemoryBufferCreateInfo ext{}; + ext.sType = VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO; + ext.handleTypes = handleType; + 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; + + VkBuffer buf = VK_NULL_HANDLE; + VkResult r = vkCreateBuffer(c.device, &bci, nullptr, &buf); + if (r != VK_SUCCESS) { + record(routeName, "FAIL", fmt("vkCreateBuffer(external)=%s", vkStr(r))); + return; + } + VkMemoryRequirements req{}; + vkGetBufferMemoryRequirements(c.device, buf, &req); + int typeIdx = pickMemType(c.memProps, req.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (typeIdx < 0) { + vkDestroyBuffer(c.device, buf, nullptr); + record(routeName, "FAIL", fmt("no HOST_VISIBLE|HOST_COHERENT type in bits=0x%x", req.memoryTypeBits)); + return; + } + pr("T1[%s] memReq size=%llu align=%llu typeBits=0x%x -> type %d", routeName, + (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 = handleType; + VkMemoryDedicatedAllocateInfo dedicated{}; + dedicated.sType = VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; + dedicated.buffer = buf; + bool needDedicated = + (ebp.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_DEDICATED_ONLY_BIT) != 0; + if (needDedicated) exportInfo.pNext = &dedicated; + + VkMemoryAllocateInfo mai{}; + mai.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + mai.pNext = &exportInfo; + mai.allocationSize = req.size; + mai.memoryTypeIndex = (uint32_t)typeIdx; + + VkDeviceMemory mem = VK_NULL_HANDLE; + r = vkAllocateMemory(c.device, &mai, nullptr, &mem); + if (r != VK_SUCCESS) { + vkDestroyBuffer(c.device, buf, nullptr); + record(routeName, advertisedExportable ? "FAIL" : "UNSUPPORTED", + fmt("vkAllocateMemory(export)=%s (advertisedExportable=%d)", vkStr(r), (int)advertisedExportable)); + return; + } + r = vkBindBufferMemory(c.device, buf, mem, 0); + if (r != VK_SUCCESS) pr("T1[%s] vkBindBufferMemory=%s (continuing)", routeName, vkStr(r)); + + void* host = nullptr; + r = vkMapMemory(c.device, mem, 0, VK_WHOLE_SIZE, 0, &host); + if (r != VK_SUCCESS) { + vkFreeMemory(c.device, mem, nullptr); + vkDestroyBuffer(c.device, buf, nullptr); + record(routeName, "FAIL", fmt("server-side vkMapMemory=%s", vkStr(r))); + return; + } + const uint32_t seedA = 0xA5A50001u, seedB = 0xB0B00002u, seedC = 0xC0C00003u; + memset(host, 0, (size_t)size); + writeRegion(host, REG_A, seedA); + + VkMemoryGetFdInfoKHR gfi{}; + gfi.sType = VK_STRUCTURE_TYPE_MEMORY_GET_FD_INFO_KHR; + gfi.memory = mem; + gfi.handleType = handleType; + int fd = -1; + r = c.pGetMemoryFdKHR(c.device, &gfi, &fd); + if (r != VK_SUCCESS || fd < 0) { + vkUnmapMemory(c.device, mem); + vkFreeMemory(c.device, mem, nullptr); + vkDestroyBuffer(c.device, buf, nullptr); + record(routeName, "UNSUPPORTED", fmt("vkGetMemoryFdKHR=%s fd=%d (advertisedExportable=%d)", vkStr(r), fd, + (int)advertisedExportable)); + return; + } + pr("T1[%s] exported fd=%d -> %s", routeName, fd, describeFd(fd).c_str()); + + int sock = -1; + pid_t pid = spawnChild("t1", &sock); + if (pid < 0) { + close(fd); + vkUnmapMemory(c.device, mem); + vkFreeMemory(c.device, mem, nullptr); + vkDestroyBuffer(c.device, buf, nullptr); + record(routeName, "FAIL", "spawnChild failed"); + return; + } + + T1Offer offer{}; + offer.allocationSize = req.size; + offer.bufferSize = size; + offer.handleType = (uint32_t)handleType; + offer.seedA = seedA; + offer.seedB = seedB; + offer.seedC = seedC; + offer.memoryTypeIndex = (uint32_t)typeIdx; + offer.memoryTypeBits = req.memoryTypeBits; + + std::string detail; + const char* status = "FAIL"; + if (!sendMsg(sock, MSG_T1_OFFER, &offer, sizeof(offer), fd)) { + detail = fmt("sendMsg(offer) errno=%d", errno); + } else { + close(fd); + fd = -1; + uint32_t tag = 0; + T1Result res{}; + size_t got = 0; + if (!recvMsg(sock, &tag, &res, sizeof(res), &got, nullptr) || tag != MSG_T1_RESULT || + got != sizeof(res)) { + detail = fmt("no T1 result from child (errno=%d, %s)", errno, reapChild(pid).c_str()); + pid = -1; + } else { + // 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(host, REG_B, seedB) : -2; + int64_t backC = res.wroteC ? checkRegion(host, REG_C, seedC) : -2; + + detail = fmt( + "mmap=%s(errno=%d,cmp=%lld,payloadAt=%lld,back=%lld) vkimport=%s(fdProps=%s bits=0x%x bind=%s " + "map=%s cmp=%lld back=%lld) %s", + res.mmapOk ? "ok" : "fail", res.mmapErrno, (long long)res.mmapMismatch, + (long long)res.mmapPatternOffset, (long long)backB, + res.importResult == VK_SUCCESS ? "ok" : vkStr((VkResult)res.importResult), + vkStr((VkResult)res.fdPropsResult), res.fdMemoryTypeBits, vkStr((VkResult)res.bindResult), + vkStr((VkResult)res.mapResult), (long long)res.vkMismatch, (long long)backC, res.note); + + bool mmapPath = res.mmapOk && res.mmapMismatch == -1 && backB == -1; + bool vkPath = res.importResult == VK_SUCCESS && res.mapResult == VK_SUCCESS && res.vkMismatch == -1 && + backC == -1; + if (mmapPath && vkPath) { + status = "OK"; + } else if (mmapPath || vkPath) { + status = "PARTIAL"; + } else if (!res.mmapOk && res.importResult != VK_SUCCESS) { + status = "FAIL"; + } else { + status = "PARTIAL"; + } + } + } + if (pid > 0) { + sendMsg(sock, MSG_BYE, nullptr, 0, -1); + detail += " "; + detail += reapChild(pid); + } + close(sock); + if (fd >= 0) close(fd); + vkUnmapMemory(c.device, mem); + vkFreeMemory(c.device, mem, nullptr); + vkDestroyBuffer(c.device, buf, nullptr); + 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.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; + void* p = mmap(nullptr, mappedLen, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (p == MAP_FAILED) { + res.mmapOk = 0; + res.mmapErrno = errno; + pr("child: mmap(MAP_SHARED) failed errno=%d (%s)", errno, strerror(errno)); + // 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; + note += " [mmap needed bufferSize not allocationSize]"; + } + } 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 (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; + } + } + } + } + + // (2) import the same fd into a child-side VkDeviceMemory and map it + VkCtx c; + if (!vkCtxInit(c, false)) { + if (p != MAP_FAILED) { + writeRegion(p, REG_B, offer.seedB); + res.wroteB = 1; + msync(p, (size_t)mappedLen, MS_SYNC); + } + 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; + if (p != MAP_FAILED) { + writeRegion(p, REG_B, offer.seedB); + res.wroteB = 1; + msync(p, (size_t)mappedLen, MS_SYNC); + } + snprintf(res.note, sizeof(res.note), "%s | child vkCreateBuffer=%s", note.c_str(), vkStr(r)); + 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); + if (p != MAP_FAILED) { + writeRegion(p, REG_B, offer.seedB); + res.wroteB = 1; + msync(p, (size_t)mappedLen, MS_SYNC); + } + snprintf(res.note, sizeof(res.note), "%s | import=%s type=%d bits=0x%x", note.c_str(), vkStr(r), + 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); + 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 + if (p != MAP_FAILED) { + writeRegion(p, REG_B, offer.seedB); + res.wroteB = 1; + msync(p, (size_t)mappedLen, MS_SYNC); + } + 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; +} + +// --------------------------------------------------------------------------- +// 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 = -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); + 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; + + int sock = -1; + pid_t pid = spawnChild("t0", &sock); + if (pid < 0) { + record("T0-ahb-blob-transfer", "FAIL", "spawnChild failed"); + return; + } + T0Request rq{}; + rq.size = size; + rq.seedA = seedA; + if (!sendMsg(sock, MSG_T0_REQUEST, &rq, sizeof(rq), -1)) { + record("T0-ahb-blob-transfer", "FAIL", 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) { + record("T0-ahb-blob-transfer", "FAIL", fmt("no alloc reply errno=%d %s", errno, reapChild(pid).c_str())); + close(sock); + return; + } + if (alloc.allocOk != 1) { + record("T0-ahb-blob-transfer", "FAIL", 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) { + record("T0-ahb-blob-transfer", "FAIL", 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-blob-transfer", "OK", fmt("socket handoff of a %llu-byte BLOB works (%s)", + (unsigned long long)size, alloc.note)); + + // (a) CPU path: AHardwareBuffer_lock on the receiving side + uint32_t writtenMask = 0; + { + void* p = nullptr; + rc = AHardwareBuffer_lock(ahb, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN, + -1, nullptr, &p); + if (rc != 0 || !p) { + record("T0-ahb-cpu-lock", "FAIL", fmt("AHardwareBuffer_lock rc=%d errno=%d", rc, errno)); + } else { + int64_t cmp = checkRegion(p, REG_A, seedA); + writeRegion(p, REG_D, seedD); + writtenMask |= 4; + AHardwareBuffer_unlock(ahb, nullptr); + record("T0-ahb-cpu-lock", cmp == -1 ? "OK" : "FAIL", + fmt("cross-process CPU read of the child's payload, mismatch=%lld", (long long)cmp)); + } + } + + // (b) Vulkan import + if (!c.hasAhb || !c.pGetAhbProps) { + record("T0-ahb-vulkan-import", "UNSUPPORTED", + "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); + if (r != VK_SUCCESS) { + record("T0-ahb-vulkan-import", "FAIL", fmt("vkGetAndroidHardwareBufferPropertiesANDROID=%s", vkStr(r))); + } 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) { + record("T0-ahb-vulkan-import", "FAIL", fmt("vkCreateBuffer(AHB external)=%s", vkStr(r))); + } 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) { + record("T0-ahb-vulkan-import", "FAIL", + fmt("vkAllocateMemory(import AHB)=%s typeIdx=%d bits=0x%x", vkStr(r), typeIdx, + props.memoryTypeBits)); + } else { + VkResult br = vkBindBufferMemory(c.device, buf, mem, 0); + void* host = nullptr; + VkResult mr = vkMapMemory(c.device, mem, 0, VK_WHOLE_SIZE, 0, &host); + if (mr == VK_SUCCESS && host) { + int64_t cmp = checkRegion(host, REG_A, seedA); + writeRegion(host, REG_B, seedB); + writtenMask |= 1; + vkUnmapMemory(c.device, mem); + record("T0-ahb-vulkan-import", cmp == -1 ? "OK" : "PARTIAL", + fmt("imported+mapped (hostVisibleType=%d bind=%s) payload mismatch=%lld", + (int)hostVisible, vkStr(br), (long long)cmp)); + } else { + record("T0-ahb-vulkan-import", "PARTIAL", + fmt("import ok, vkMapMemory=%s (bind=%s hostVisibleType=%d bits=0x%x)", vkStr(mr), + vkStr(br), (int)hostVisible, props.memoryTypeBits)); + } + vkFreeMemory(c.device, mem, nullptr); + } + vkDestroyBuffer(c.device, buf, nullptr); + } + } + } + + // (c) GL import through EGL_ANDROID_get_native_client_buffer + EXT_external_buffer + if (!glOk) { + record("T0-ahb-gl-import", "SKIP", "no GL context"); + } else if (!g.hasGl("GL_EXT_external_buffer") || !g.pBufferStorageExternal || !g.pGetNativeClientBuffer) { + record("T0-ahb-gl-import", "UNSUPPORTED", + 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))); + } else { + EGLClientBuffer cb = g.pGetNativeClientBuffer(ahb); + if (!cb) { + record("T0-ahb-gl-import", "FAIL", fmt("eglGetNativeClientBufferANDROID=NULL egl=0x%04x", eglGetError())); + } else { + GLuint b = 0; + glGenBuffers(1, &b); + glBindBuffer(GL_ARRAY_BUFFER, b); + while (glGetError() != GL_NO_ERROR) {} + 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 err = glGetError(); + if (err != GL_NO_ERROR) { + record("T0-ahb-gl-import", "FAIL", fmt("glBufferStorageExternalEXT -> GL error 0x%04x", err)); + } 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 merr = glGetError(); + if (!m) { + record("T0-ahb-gl-import", "PARTIAL", + fmt("storage ok, glMapBufferRange returned NULL (GL error 0x%04x)", merr)); + } else { + int64_t cmp = checkRegion(m, REG_A, seedA); + writeRegion(m, REG_C, seedC); + writtenMask |= 2; + glUnmapBuffer(GL_ARRAY_BUFFER); + glFinish(); + record("T0-ahb-gl-import", cmp == -1 ? "OK" : "PARTIAL", + fmt("persistent-coherent GL map of the client AHB, payload mismatch=%lld", + (long long)cmp)); + } + } + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteBuffers(1, &b); + } + } + + // (d) ask the child to verify everything the parent wrote + T0Verify ver{}; + ver.seedB = seedB; + ver.seedC = seedC; + ver.seedD = seedD; + ver.writtenMask = writtenMask; + 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 { + T0Result res{}; + if (!recvMsg(sock, &tag, &res, sizeof(res), &got, nullptr) || tag != MSG_T0_RESULT) { + wbDetail = fmt("no verify reply errno=%d", errno); + } else { + bool anyChecked = false, allOk = true; + auto acc = [&](int64_t v) { + if (v == -2) return; + anyChecked = true; + if (v != -1) allOk = false; + }; + acc(res.mismatchB); + acc(res.mismatchC); + acc(res.mismatchD); + wbStatus = !anyChecked ? "SKIP" : (allOk ? "OK" : "FAIL"); + wbDetail = fmt("mask=0x%x vkWrite=%lld glWrite=%lld cpuWrite=%lld (lock=%d)", writtenMask, + (long long)res.mismatchB, (long long)res.mismatchC, (long long)res.mismatchD, + res.lockOk); + } + } + record("T0-ahb-writeback-to-client", wbStatus, wbDetail); + + sendMsg(sock, MSG_BYE, nullptr, 0, -1); + std::string reap = reapChild(pid); + pr("T0 %s", reap.c_str()); + AHardwareBuffer_release(ahb); + close(sock); +} + +#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"); +} + +#endif // PROBE_HAVE_AHB + +// --------------------------------------------------------------------------- +// T3: VK_EXT_external_memory_host over a memfd-backed mapping +// --------------------------------------------------------------------------- + +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; + 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); + writeRegion(p, REG_B, offer.seedB); + munmap(p, (size_t)offer.size); + } + sendMsg(sock, MSG_T3_RESULT, &res, sizeof(res), -1); + close(fd); + return 0; +} + +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"); + 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; + } + // reserve an aligned window, then place the memfd inside it + void* reserve = mmap(nullptr, (size_t)(mapSize + align), PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (reserve == MAP_FAILED) { + record("T3-external-memory-host", "FAIL", fmt("reserve mmap errno=%d", errno)); + close(memfd); + return; + } + uintptr_t base = ((uintptr_t)reserve + align - 1) & ~(uintptr_t)(align - 1); + void* host = mmap((void*)base, (size_t)mapSize, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, memfd, 0); + if (host == MAP_FAILED) { + record("T3-external-memory-host", "FAIL", fmt("mmap(memfd, MAP_FIXED) errno=%d", errno)); + munmap(reserve, (size_t)(mapSize + align)); + close(memfd); + return; + } + const uint32_t seedA = 0x33330001u, seedB = 0x33330002u; + memset(host, 0, (size_t)mapSize); + writeRegion(host, REG_A, seedA); + + VkMemoryHostPointerPropertiesEXT hp{}; + hp.sType = VK_STRUCTURE_TYPE_MEMORY_HOST_POINTER_PROPERTIES_EXT; + VkResult r = c.pGetHostPtrProps(c.device, VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT, host, &hp); + if (r != VK_SUCCESS) { + record("T3-external-memory-host", "FAIL", fmt("vkGetMemoryHostPointerPropertiesEXT=%s align=%llu", vkStr(r), + (unsigned long long)align)); + } else { + 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; + VkBuffer buf = VK_NULL_HANDLE; + VkResult cr = vkCreateBuffer(c.device, &bci, nullptr, &buf); + VkMemoryRequirements req{}; + if (cr == VK_SUCCESS) vkGetBufferMemoryRequirements(c.device, buf, &req); + uint32_t bits = hp.memoryTypeBits & (cr == VK_SUCCESS ? req.memoryTypeBits : 0xFFFFFFFFu); + int typeIdx = pickMemType(c.memProps, bits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + if (typeIdx < 0) typeIdx = pickMemType(c.memProps, bits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + if (typeIdx < 0) { + record("T3-external-memory-host", "FAIL", + fmt("no host-visible type in hostPtrBits=0x%x & reqBits=0x%x", hp.memoryTypeBits, + req.memoryTypeBits)); + } else { + 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)typeIdx; + VkDeviceMemory mem = VK_NULL_HANDLE; + VkResult ar = vkAllocateMemory(c.device, &mai, nullptr, &mem); + if (ar != VK_SUCCESS) { + record("T3-external-memory-host", "FAIL", + fmt("vkAllocateMemory(import host ptr)=%s type=%d bits=0x%x align=%llu", vkStr(ar), typeIdx, + bits, (unsigned long long)align)); + } else { + VkResult br = (cr == VK_SUCCESS) ? vkBindBufferMemory(c.device, buf, mem, 0) : VK_SUCCESS; + void* mapped = nullptr; + VkResult mr = vkMapMemory(c.device, mem, 0, VK_WHOLE_SIZE, 0, &mapped); + int64_t cmp = -3; + if (mr == VK_SUCCESS && mapped) cmp = checkRegion(mapped, REG_A, seedA); + if (mr == VK_SUCCESS) vkUnmapMemory(c.device, mem); + record("T3-external-memory-host", (mr == VK_SUCCESS && cmp == -1) ? "OK" : "PARTIAL", + fmt("import ok (align=%llu type=%d bind=%s) vkMapMemory=%s mismatch=%lld", + (unsigned long long)align, typeIdx, vkStr(br), vkStr(mr), (long long)cmp)); + vkFreeMemory(c.device, mem, nullptr); + } + } + if (cr == VK_SUCCESS) vkDestroyBuffer(c.device, buf, nullptr); + } + + // 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; + 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; + record("T3-memfd-cross-process", (res.mmapOk && res.mismatch == -1 && back == -1) ? "OK" : "FAIL", + fmt("child mmap=%d errno=%d cmp=%lld writeback=%lld [%s]", res.mmapOk, res.mmapErrno, + (long long)res.mismatch, (long long)back, res.note)); + } + } + sendMsg(sock, MSG_BYE, nullptr, 0, -1); + reapChild(pid); + close(sock); + } + + munmap(host, (size_t)mapSize); + munmap(reserve, (size_t)(mapSize + align)); + close(memfd); +} + +// --------------------------------------------------------------------------- +// main +// --------------------------------------------------------------------------- + +static void printSummary() { + char model[PROP_VALUE_MAX] = {0}; + getProp("ro.product.model", model, sizeof(model)); + printf("\n=== extmem_probe summary (model=%s) ===\n", model); + 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); +} + +int main(int argc, char** argv) { + uint64_t size = kDefaultSize; + const char* childRoute = nullptr; + bool doT1 = true, doT0 = true, doT3 = true; + for (int i = 1; i < argc; ++i) { + if (!strncmp(argv[i], "--child=", 8)) { + childRoute = argv[i] + 8; + } else if (!strncmp(argv[i], "--size=", 7)) { + size = strtoull(argv[i] + 7, nullptr, 0); + } else if (!strcmp(argv[i], "--only-t1")) { + doT0 = doT3 = false; + } else if (!strcmp(argv[i], "--only-t0")) { + doT1 = doT3 = false; + } else if (!strcmp(argv[i], "--only-t3")) { + doT1 = doT0 = false; + } else if (!strcmp(argv[i], "--help")) { + printf("usage: extmem_probe [--size=BYTES] [--only-t0|--only-t1|--only-t3]\n"); + return 0; + } + } + if (size < 4 * kRegion) size = 4 * kRegion; + + // 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, "t0")) return childT0(sock); + if (!strcmp(childRoute, "t3")) return childT3(sock); + pr("unknown child route %s", childRoute); + return 1; + } + + pr("extmem_probe: MobileGL disaggregation spike B, size=%llu bytes", (unsigned long long)size); + + 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 T0: client-allocated AHardwareBuffer BLOB ==="); + if (doT0) runT0Parent(c, g, glOk, size); + pr("=== phase T3: VK_EXT_external_memory_host ==="); + if (doT3) runT3Parent(c, size); + + glCtxDestroy(g); + vkCtxDestroy(c); + printSummary(); + return 0; +}