[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
+28 -2
View File
@@ -96,7 +96,7 @@ namespace MobileGL::MG_Remote::Server {
int removed = 0;
};
ScrubbedEnv BuildChildEnv() {
ScrubbedEnv BuildChildEnv(const std::string& imageDir) {
// `environ` is glibc/bionic's, declared by <unistd.h> at global
// scope. Re-declaring it inside this anonymous namespace made it a
// NEW symbol that nothing defines - which the linker caught, and
@@ -115,6 +115,29 @@ namespace MobileGL::MG_Remote::Server {
// §3.1 is the whole argument for why those are two axes.
env.storage.emplace_back("MOBILEGL_IPC_ROLE=server");
env.storage.emplace_back("MOBILEGL_IPC_DIAL=no");
// LD_LIBRARY_PATH, BECAUSE THE SERVER IS EXEC'd AND NOT dlopen'd.
//
// It links libMobileGL.so, and an exec'd process gets a FRESH linker
// namespace with the default search path - not the one the app built
// for its own libraries. An APK's lib/<abi>/ is not on that path, and
// the failure is total and immediate:
//
// CANNOT LINK EXECUTABLE ".../libMobileGLServer.so":
// library "libMobileGL.so" not found: needed by main executable
//
// It is invisible on desktop, where CMake gives build-tree targets an
// absolute RPATH of their own - so the WSL server had been resolving
// its dependency through a path no phone has.
//
// $ORIGIN WOULD BE THE TIDIER ANSWER and the target asks for it, but
// the NDK toolchain drops RPATH from the link: measured with readelf
// on the APK's own copy, which carries NEEDED libMobileGL.so and no
// RUNPATH at all. So the launcher states it instead - 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.
if (!imageDir.empty()) {
env.storage.emplace_back("LD_LIBRARY_PATH=" + imageDir);
}
env.pointers.reserve(env.storage.size() + 1);
for (auto& entry : env.storage) {
env.pointers.push_back(entry.data());
@@ -183,7 +206,10 @@ namespace MobileGL::MG_Remote::Server {
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
ScrubbedEnv env = BuildChildEnv();
// The image's directory, which is where its libMobileGL.so is.
const std::string::size_type slash = image.find_last_of('/');
ScrubbedEnv env =
BuildChildEnv(slash == std::string::npos ? std::string() : image.substr(0, slash));
std::string argv0 = image;
std::string argv1 = endpoint;
char* argv[] = {argv0.data(), argv1.data(), nullptr};