[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.
This commit is contained in:
2026-09-05 20:50:04 -04:00
parent 38d4c2372c
commit 6c7ad0a1bf
4 changed files with 2010 additions and 0 deletions
+34
View File
@@ -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()
+80
View File
@@ -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=<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
```sh
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.
+25
View File
@@ -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 <build-dir>/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"
File diff suppressed because it is too large Load Diff