- 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.
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_COHERENTVkDeviceMemorybe 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) thevkGetPhysicalDeviceExternalBufferPropertiesverdict for the buffer usage MobileGL actually needs. Then a headless EGL pbuffer context reportsGL_EXT_memory_object{,_fd},GL_EXT_external_buffer,GL_EXT_buffer_storage,GL_OES_EGL_image_external{,_essl3}andEGL_ANDROID_get_native_client_buffer. - T1 — server exports. Allocates a
HOST_VISIBLE|HOST_COHERENTbuffer memory withVkExportMemoryAllocateInfo, maps it, writes a pattern, exports an fd withvkGetMemoryFdKHR(opaque-fd, then dma-buf), hands the fd to a second process overSCM_RIGHTS, and has that process (a)mmap()the fd and (b) import it into its ownVkDeviceMemoryandvkMapMemoryit. 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
AHardwareBufferBLOB (CPU_READ_OFTEN|CPU_WRITE_OFTEN|GPU_DATA_BUFFER), writes a pattern underAHardwareBuffer_lock, and sends it withAHardwareBuffer_sendHandleToUnixSocket. The first process reads it back three ways — CPU lock,VkDeviceMemoryimported throughVK_ANDROID_external_memory_android_hardware_buffer, and a GL buffer created witheglGetNativeClientBufferANDROID+glBufferStorageExternalEXTmapped persistent/coherent — writes through each, and the allocating process verifies every write. - T3 — host pointer import. If
VK_EXT_external_memory_hostis advertised, imports a memfd-backed, alignment-correctedmmapregion as aVkDeviceMemoryand 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
mmapand the importedVkDeviceMemorybefore 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=4096with 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.