[Feat] (Trace): the replay runner builds as an adb-shell executable for Android, and unix endpoints carry a headless window

MOBILEGL_BUILD_TRACE_REPLAY now also produces mobilegl_trace_replay on
ANDROID: the same CLI as desktop, linked against the APK runner's
glws/glproc glue. On the device it dials a render server with no
Activity at all - MOBILEGL_IPC_CONTROL=unix:@name with shm data to the
offscreen supervisor (a cross-uid abstract connect needs root or an
SELinux waiver; loopback TCP works from plain adb shell), or
tcp:// with MOBILEGL_IPC_SURFACE=server to the on-screen display server.

glws_android grows the desktop glws_egl's server-owned null-window
createWindowSurface, gated on MOBILEGL_TRACE_SURFACE=window so the APK's
pbuffer mode keeps its old answer, and the config carries EGL_WINDOW_BIT
for it. The control screen refuses unix endpoints in on-screen mode:
the in-process display server serves tcp:// only.

Verified on-device with an OpenRA retrace, ssim=1.0 both ways:
offscreen service on @mgl-cli with the CLI as root, and the display
Activity on tcp://127.0.0.1:40613 with the replay's frames on the
phone's screen.
This commit is contained in:
2026-09-27 06:28:00 -04:00
parent bf6feebb22
commit e3e890ed5d
4 changed files with 53 additions and 6 deletions
@@ -75,6 +75,14 @@ int ResolveHeight(int height) {
return height > 0 ? height : 1;
}
// A HEADLESS command-line replay (no ANativeWindow) that still wants a window surface: the
// window will be the MobileGL server's (MOBILEGL_IPC_SURFACE=server), so the config must
// carry EGL_WINDOW_BIT for the null-window eglCreateWindowSurface in createSurface.
bool WantsServerOwnedWindow() {
const char *mode = std::getenv("MOBILEGL_TRACE_SURFACE");
return gNativeWindow == nullptr && mode != nullptr && std::strcmp(mode, "window") == 0;
}
void *Lookup(const char *name) {
if (gMobileGl == nullptr) {
gMobileGl = dlopen("libMobileGL.so", RTLD_NOW | RTLD_GLOBAL | RTLD_NOLOAD);
@@ -206,6 +214,22 @@ private:
static_cast<const AndroidVisual *>(visual)->config,
gNativeWindow,
nullptr);
} else if (!pbuffer && WantsServerOwnedWindow()) {
// HEADLESS COMMAND-LINE REPLAY (P12 D9, same shape as the desktop glws_egl path):
// the window is the MobileGL SERVER's (MOBILEGL_IPC_SURFACE=server,
// WindowKind::ServerOwned), so the native window is none and the size the trace
// wants rides EGL_WIDTH/EGL_HEIGHT. Gated on MOBILEGL_TRACE_SURFACE=window so the
// APK's pbuffer mode (no ANativeWindow either) keeps its pbuffer.
const EGLint windowAttribs[] = {
EGL_WIDTH, surfaceWidth,
EGL_HEIGHT, surfaceHeight,
EGL_NONE,
};
surface = gEgl.createWindowSurface(
gDisplay,
static_cast<const AndroidVisual *>(visual)->config,
EGLNativeWindowType{},
windowAttribs);
} else {
const EGLint attribs[] = {
EGL_WIDTH, surfaceWidth,
@@ -334,7 +358,8 @@ Visual *createVisual(bool doubleBuffer, unsigned samples, Profile profile) {
}
const EGLint attribs[] = {
EGL_SURFACE_TYPE, gNativeWindow == nullptr ? EGL_PBUFFER_BIT : EGL_WINDOW_BIT,
EGL_SURFACE_TYPE,
gNativeWindow == nullptr && !WantsServerOwnedWindow() ? EGL_PBUFFER_BIT : EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
visual->api == EGL_OPENGL_ES_API ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_BIT,
EGL_RED_SIZE, 8,
@@ -98,7 +98,7 @@ public final class ServerControlActivity extends Activity {
envField = addField(contentLayout, "Extra env (optional)", "", dp(8));
onScreenBox = new CheckBox(this);
onScreenBox.setText("On-screen window (render client frames onto this screen)");
onScreenBox.setText("On-screen window (render client frames onto this screen; tcp:// only)");
onScreenBox.setTextColor(COLOR_TEXT);
onScreenBox.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
LinearLayout.LayoutParams boxParams = new LinearLayout.LayoutParams(
@@ -155,6 +155,14 @@ public final class ServerControlActivity extends Activity {
String token = tokenField.getText().toString();
String env = envField.getText().toString().trim();
// The in-process display server only accepts tcp:// (ServerMain's in-process arm);
// the unix forms belong to the offscreen supervisor.
if (onScreenBox.isChecked() && !endpoint.startsWith("tcp://")) {
setStatus("The on-screen display server serves tcp:// only; use a tcp:// endpoint "
+ "or uncheck On-screen window.", COLOR_FAIL);
return;
}
String probeDisplay;
Probe probe;
if (endpoint.startsWith("tcp://")) {