[MG_Remote, android-plugin] (P6): a real phone retraces a real trace across two processes

OpenRA.DirectGLES, transport=spawn, on a Redmi: ssim=1.000000,
mismatchPixels=0, zero Fatal{ lines, three runs for three. The server is a
process the client launched out of the APK's lib/arm64/, and the client's own
log names its pid.

Four things stood between the desktop green and the device green, and every
one of them was invisible on desktop for the same reason: a build tree is not
a deployment, and Linux is not Android.

1. THE SERVER COULD NOT LINK. It links libMobileGL.so and is EXEC'd, not
   dlopen'd, so it gets a fresh linker namespace with the DEFAULT search path
   - and an APK's lib/<abi>/ is not on it. `CANNOT LINK EXECUTABLE ...:
   library "libMobileGL.so" not found: needed by main executable`, before a
   single line of MobileGL ran. On desktop CMake had been giving the target an
   absolute build-tree RPATH, so the WSL server resolved its dependency
   through a path no phone has. $ORIGIN is asked for via BUILD_RPATH (a raw
   -Wl,-rpath,$ORIGIN reaches the generator unquoted and the SHELL eats it -
   the device linker said `normalize_path - invalid input: "$"`), and because
   the NDK drops RPATH from the link anyway, the LAUNCHER states
   LD_LIBRARY_PATH: it is the one component that has already resolved where
   the image lives, and a pair that is not co-located is not a pair.

2. THE RENDEZVOUS NEEDED A WRITABLE DIRECTORY. An Android app has no /tmp, so
   bind() in the child had nowhere to put the node and the client refused by
   name 20 s later. AF_UNIX endpoints now accept the ABSTRACT namespace with a
   leading '@' - the systemd/D-Bus convention - which has no filesystem
   presence at all: no directory, no mode bits, and no stale node to unlink
   after a crash, which is the bind() failure Listen() has been working
   around. Its reach is the network namespace, which for an app is the app.
   StartSpawned mints '@' names; an explicit path still works everywhere, and
   that is what P12's "connect to something somebody else is listening on"
   will use.

3. PBUFFER WAS A DirectGLES-ONLY KNOB. Twice over: trace-replay-ci.sh gated
   `--ez use_pbuffer true` on the backend name, and the JNI demanded a native
   window for DirectVulkan unconditionally. A surface SHAPE is not a backend
   property, and the coupling mattered because the Android GLWS makes a window
   surface whenever gNativeWindow is non-null - so handing Magma a window was
   the same as asking for one. Until P12 a spawned server cannot be given a
   window at all: an ANativeWindow* is a pointer into the CLIENT's process and
   SetWindowHandle is refused on the way across with Fatal{UnmigratedSurface,
   "AndroidNativeWindow@P12"} (Rule H). Which is exactly what happened, one op
   after the server had come up green.

4. THE RUNNER LOOKED IN ONE OF THE TWO OUTPUT DIRECTORIES. assembleTraceRelease
   writes apk/trace/release; CI has staged the same artifact under
   apk/trace/debug. A tree that has built locally has both, and picking the
   wrong one retraces last month's APK and says nothing about it. Newest of
   either, now.

MEASURED AND NOT FIXED: DirectVulkan on this device diverges under BOTH split
transports and does so nondeterministically - inproc scored ssim=0.988998 /
9836 px, spawn scored 0.976494 / 14658 px and then 0.988998 / 9836 px on a
repeat, while monolith with the same pbuffer surface scores 1.000000 / 0 px.
The two transports draw from the same set of outcomes, so this is the split
path on Magma against a real GPU, not the process boundary: P6 neither caused
it nor can fix it. DirectVulkan spawn IS green on desktop (lavapipe, 1.000000).

G14: one test name added, AnAbstractEndpointNeedsNoWritableDirectoryAndLeaves-
NoFile, which pins both halves - the handshake crosses an abstract name, and
nothing is left on the filesystem. A '@' mistaken for a path would have bound
a file of that literal name, passed here, and failed on the one platform this
exists for.
This commit is contained in:
2026-09-21 13:33:57 -04:00
parent 557dc2c109
commit 7c8120cc49
8 changed files with 192 additions and 21 deletions
@@ -170,9 +170,21 @@ Java_top_mobilegl_plugin_trace_TraceReplayActivity_nativeRunTraceReplay(JNIEnv*
request.benchmarkResultPath = ToString(env, benchmarkResultPath);
request.envOverrides = SplitSemicolonList(ToString(env, envOverrides));
const bool needsNativeWindow =
request.backend == "DirectVulkan" ||
(request.backend == "DirectGLES" && !request.usePbuffer);
// THE SURFACE SHAPE DECIDES THIS, NOT THE BACKEND. DirectVulkan used to demand a native
// window unconditionally, which made `use_pbuffer` a DirectGLES-only knob in practice: the
// Android GLWS creates a window surface whenever gNativeWindow is non-null
// (apitrace_glws_android.cpp's createSurface), so handing Magma a window was the same as
// asking for one.
//
// P6 needs the pbuffer path on BOTH backends. Until P12 a spawned server cannot be given a
// window at all - an ANativeWindow* is a pointer into the CLIENT's process, and
// SetWindowHandle is refused on the way across with Fatal{UnmigratedSurface,
// "AndroidNativeWindow@P12"} (Rule H). Measured: the DirectVulkan spawn arm died on exactly
// that, one op after the server had come up green, while DirectGLES with the same flag passed.
//
// Every existing caller is unaffected: nobody asks for a Vulkan pbuffer today, and
// !usePbuffer reproduces the old answer for both backends in every other combination.
const bool needsNativeWindow = !request.usePbuffer;
ANativeWindow *window = needsNativeWindow && surface != nullptr ? ANativeWindow_fromSurface(env, surface) : nullptr;
if (needsNativeWindow && window == nullptr) {
auto result = MakeFailureResult(request, mobilegl_trace::STATUS_RETRACE_FAILED,
+9 -3
View File
@@ -44,7 +44,7 @@ Set MOBILEGL_ESPRYT_USE_ANGLE=1 to run DirectGLES replay with packaged ANGLE
instead of the device system GLES driver.
Set MOBILEGL_TRACE_ANGLE_VARIANT to the packaged ANGLE short hash used by
DirectGLES replay.
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run DirectGLES
Set MOBILEGL_RETRACE_USE_PBUFFER=1 or pass --use-pbuffer to run
against an offscreen EGL pbuffer instead of the Activity surface.
Set MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1,
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1, and MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1 to
@@ -366,7 +366,7 @@ run_retrace() {
use_angle=1
test -n "${MOBILEGL_TRACE_ANGLE_VARIANT:-}" || die "MOBILEGL_TRACE_ANGLE_VARIANT is required for DirectGLES ANGLE replay"
fi
if [ "${MOBILEGL_RETRACE_USE_PBUFFER:-}" = "1" ] && [ "${backend}" = "DirectGLES" ]; then
if [ "${MOBILEGL_RETRACE_USE_PBUFFER:-}" = "1" ]; then
use_pbuffer=1
fi
@@ -392,7 +392,13 @@ run_retrace() {
set -- "$@" --ez use_angle true
set -- "$@" --es angle_variant "${MOBILEGL_TRACE_ANGLE_VARIANT}"
fi
if [ "${use_pbuffer}" -eq 1 ] && [ "${backend}" = "DirectGLES" ]; then
# PBUFFER IS A SURFACE SHAPE, NOT A BACKEND PROPERTY, and the DirectGLES condition that used to
# guard this line made it one. It was harmless while the only caller was the ANGLE lane; P6 needs
# it on BOTH backends, because until P12 the spawn arm can only use pbuffer/surfaceless - an
# ANativeWindow* is a pointer into the CLIENT's process and SetWindowHandle is refused by name
# with Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} on the way across. Magma creates an
# EGL pbuffer exactly as Espryt does.
if [ "${use_pbuffer}" -eq 1 ]; then
set -- "$@" --ez use_pbuffer true
fi
if [ "${avoid_angle_llvmpipe_sampler_mipmap_min_filter}" -eq 1 ] && [ "${backend}" = "DirectGLES" ]; then