Files
MobileGL/tools/spikes/extmem_probe
swung0x48 6c7ad0a1bf [Feat] (Spikes): add the standalone external-memory probe that decides the persistent-map tier
- Plan B §11 P0 requires spike B ("external memory 导出,两台设备") to run before the
  persistent-map decision of §8.3 can be taken: T0 (server imports a client
  allocation), T1 (server exports its own HOST_VISIBLE|HOST_COHERENT allocation)
  or T2 (AcquirePersistentMap returns nullptr, making the §5.10 client-side block
  push mandatory). §8.3 says the answer must be measured on the two campaign
  devices, and that a platform unknown must not block the interface work.
- tools/spikes/extmem_probe/ is a self-contained NDK command-line program: it
  links vulkan/EGL/GLESv3/android/log and nothing from MobileGL, is configured by
  its own CMakeLists with the android toolchain file, and is deliberately absent
  from the project's build graph (the root CMakeLists only pulls in
  tools/trace_replay), so the default ALL target is untouched.
- Phase A enumerates VK_KHR_external_memory_fd, VK_EXT_external_memory_dma_buf,
  VK_EXT_external_memory_host, VK_ANDROID_external_memory_android_hardware_buffer
  and, through a headless EGL pbuffer context, 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, plus the memory-type table and the
  vkGetPhysicalDeviceExternalBufferProperties verdict per handle type for the exact
  buffer usage set MobileGL needs.
- Route T1 allocates a HOST_VISIBLE|HOST_COHERENT buffer memory with
  VkExportMemoryAllocateInfo, writes a pattern through vkMapMemory, exports an fd
  with vkGetMemoryFdKHR (opaque-fd and, where advertised, dma-buf), hands it to a
  second process over SCM_RIGHTS, and has that process both mmap() the fd and
  import it into its own VkDeviceMemory + vkMapMemory. Both sides write and both
  sides compare, so a copy-on-import or one-directional mapping is reported as
  PARTIAL rather than as success.
- Route T0 has the second process allocate an AHardwareBuffer BLOB
  (CPU_READ_OFTEN|CPU_WRITE_OFTEN|GPU_DATA_BUFFER), send it with
  AHardwareBuffer_sendHandleToUnixSocket, and the first process import it three
  ways -- AHardwareBuffer_lock, VkDeviceMemory via
  VK_ANDROID_external_memory_android_hardware_buffer, and a GL buffer via
  eglGetNativeClientBufferANDROID + glBufferStorageExternalEXT mapped
  persistent/coherent -- with a write-back leg the allocating process verifies.
- Route T3 covers VK_EXT_external_memory_host: a memfd-backed mmap region aligned
  to minImportedHostPointerAlignment, imported through
  VkImportMemoryHostPointerInfoEXT, plus the same memfd handed to another process.
- The second process is /proc/self/exe re-exec'd with --child=<route> and one end
  of a socketpair on fd 3. A bare fork() is not usable: neither side's Vulkan
  driver survives fork, and both sides need live Vulkan. It is also the topology
  the transport actually has (§8.1, inheriting PLAN.md §11.1-§11.6: the client
  spawns the server), so the probe measures the arrangement that would ship.
- The probe also builds for the host with T0 compiled out. That is not scope
  creep: a negative device result is only worth something if the harness is known
  to report a working route as working. Running it on lavapipe did that, and paid
  for itself immediately by exposing two harness bugs that would have produced
  false negatives on the devices -- (a) the child wrote through its plain mmap
  before reading through the Vulkan import, so on a driver whose exported fd maps
  at an offset the probe overwrote the very payload the second read compares
  (lavapipe reports payloadAt=4096); reads through both mappings now precede
  writes through either, and the offset is searched for and reported; (b) an
  export failure on a handle type the driver never advertised as EXPORTABLE was
  classified FAIL instead of UNSUPPORTED (lavapipe's dma-buf answer).
- Output is a RESULT/summary table carrying the raw driver verdicts (VkResult
  names, errno, GL enums) because those codes -- not a pass/fail bit -- are what
  §8.3 needs in order to pick the tier.
- Built with NDK 27.3.13750724 for arm64-v8a / android-30, RelWithDebInfo, PIE,
  warning-clean; host build clang/RelWithDebInfo, warning-clean.
2026-09-05 20:50:04 -04:00
..

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=<route> 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

ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 ./build_android.sh /tmp/extmem-build
adb -s <serial> push /tmp/extmem-build/extmem_probe /data/local/tmp/p0-extmem/
adb -s <serial> shell /data/local/tmp/p0-extmem/extmem_probe

There is also a host build (cmake -S . -B <dir> 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 <route> <status> <detail> 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=<offset>. 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.