[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
+6
View File
@@ -982,6 +982,12 @@ if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# The replay runner also builds for Android: an adb-shell executable that can
# dial a render server on the device (unix or TCP) without any Activity.
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
endif()
# ---------------------------------------------------------------------------
# P0 spike A: the Android delivery chain for a second native executable.
#
@@ -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://")) {
+12 -4
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.22.1)
if(NOT UNIX OR ANDROID)
message(STATUS "MobileGL trace replay runner is only enabled on desktop Unix")
if(NOT UNIX)
message(STATUS "MobileGL trace replay runner is only enabled on Unix (Android included)")
return()
endif()
@@ -139,7 +139,9 @@ target_link_libraries(mobilegl_trace_common PUBLIC
add_library(mobilegl_trace_glproc STATIC
"${APITRACE_GENERATED_DIR}/glproc.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_glproc_mobilegl.cpp")
# Android resolves GL through the same loader glue as the APK runner; desktop
# keeps its own.
"$<IF:$<BOOL:${ANDROID}>,${MOBILEGL_TRACE_SHARED_CPP_DIR}/apitrace_glproc_android.cpp,${MOBILEGL_TRACE_ROOT}/apitrace_glproc_mobilegl.cpp>")
if(APPLE)
target_compile_definitions(mobilegl_trace_glproc PRIVATE GL_SILENCE_DEPRECATION)
endif()
@@ -225,7 +227,9 @@ add_library(mobilegl_trace_glretrace_common STATIC
"${APITRACE_ROOT}/retrace/metric_helper.cpp"
"${APITRACE_ROOT}/retrace/metric_writer.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_fbo_dump.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_glws_egl.cpp"
# Android uses the APK runner's glws/glproc glue (pbuffer, or the server-owned
# null-window surface for a headless replay); desktop uses the EGL one.
"$<IF:$<BOOL:${ANDROID}>,${MOBILEGL_TRACE_SHARED_CPP_DIR}/apitrace_glws_android.cpp,${MOBILEGL_TRACE_ROOT}/apitrace_glws_egl.cpp>"
"${MOBILEGL_TRACE_SHARED_CPP_DIR}/trace_benchmark.cpp")
if(APPLE)
set(MOBILEGL_TRACE_APPLE_FRAMEWORKS
@@ -241,6 +245,7 @@ target_include_directories(mobilegl_trace_glretrace_common PUBLIC
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/helpers"
"${APITRACE_ROOT}/retrace"
"${MOBILEGL_TRACE_ROOT}"
"${MOBILEGL_TRACE_SHARED_CPP_DIR}")
target_compile_definitions(mobilegl_trace_glretrace_common PRIVATE
exit=mobilegl_apitrace_exit
@@ -280,6 +285,9 @@ else()
${MOBILEGL_TRACE_REPLAY_LINK_LIBRARIES}
"-Wl,--end-group")
endif()
if(ANDROID)
target_link_libraries(mobilegl_trace_replay PRIVATE android log)
endif()
# The `K=V;K=V` environment passthrough is the only hand-rolled parse between a CI flag
# and setenv(), and the lane that exercises it end to end is an on-device retrace, which