[Feat] (Android): the render server screen and binary ship in the plugin APK too, not just trace

This commit is contained in:
2026-09-28 22:10:56 -04:00
parent 2fa58940e4
commit 1987bdff5c
16 changed files with 128 additions and 74 deletions
+18
View File
@@ -61,6 +61,24 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
exit 1
fi
# The render server is not trace-only: both flavors carry the control screen, the offscreen
# service, the on-screen display activity, and (split native builds) the supervisor executable.
# The plugin APK ships to users, so its build keeps the three components private (manifest
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
require "$component" "$plugin_manifest" "plugin render server component ($component)"
require "$component" "$trace_manifest" "trace render server component ($component)"
done
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
trace_contents=$(unzip -Z1 "$trace_apk")
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
exit 1
fi
done
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
+1 -1
View File
@@ -682,7 +682,7 @@ if (ANDROID)
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
# P12 (on-screen server window): the APK's MobileGLDisplayActivity - the in-process
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
list(APPEND SOURCE_FILES
@@ -8,7 +8,7 @@
// P12 (on-screen server window), D3/D5/D6/D7. THE JNI GLUE OF THE IN-PROCESS DISPLAY SERVER.
//
// The trace APK's MobileGLDisplayActivity (android:process=":mglwin") owns a SurfaceView and runs
// The APK's MobileGLDisplayActivity (android:process=":mglwin") owns a SurfaceView and runs
// the TCP server on a thread of its own process, so a headless client (MOBILEGL_IPC_SURFACE=server)
// renders straight onto that SurfaceView. This file is the whole native half of that Activity:
//
+10 -1
View File
@@ -35,7 +35,10 @@ fun Project.mobileGlCmakeCompilerLauncher(): String =
// Match the native module's two switches, including INPROC implying the parent
// option. A D/P plugin must ask its launcher to select the transport at runtime;
// compiling MG_Remote alone leaves ConfigLoader's default on monolith.
// compiling MG_Remote alone leaves ConfigLoader's default on monolith. The native module
// builds the split shape by default in a standalone build (it carries the render server),
// so this is only the opt-in for the launcher-selected in-process TRANSPORT: unset, the
// plugin still runs monolith.
fun Project.mobileGlDisaggregatedEnabled(): Boolean {
fun enabled(value: Any?): Boolean = value?.toString()?.trim()?.uppercase().orEmpty() in setOf("1", "ON", "YES", "TRUE", "Y")
return enabled(findProperty("mobilegl.buildDisaggregated") ?: System.getenv("MOBILEGL_BUILD_DISAGGREGATED")) ||
@@ -172,11 +175,17 @@ android {
productFlavors {
create("plugin") {
dimension = "profile"
// The shipped renderer plugin keeps its render-server components private; the
// screen and NavBar start them from inside the app.
manifestPlaceholders["serverComponentsExported"] = "false"
}
create("trace") {
dimension = "profile"
applicationIdSuffix = ".trace"
versionNameSuffix = "-trace"
// Debuggable dev APK: adb (tcp_device_server.py, the CI TCP lane) starts the
// server components directly by explicit component name.
manifestPlaceholders["serverComponentsExported"] = "true"
}
}
@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The render server (ServerControlActivity / MobileGLServerService /
MobileGLDisplayActivity): a TCP listener kept alive by a wake lock in a data-sync
foreground service. -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:hasCode="true"
android:label="${appLabel}"
@@ -22,6 +30,41 @@
android:exported="true"
android:theme="@style/Theme.MobileGLPlugin.NoDisplay" />
<!-- Render server control screen: start/stop the offscreen TCP supervisor
(MobileGLServerService) or the on-screen display server. Not a launcher: the
bottom navigation bar (NavBar) switches between it and PostActivity. Both flavors
ship the server (libMobileGLServer.so + the display server's JNI in libMobileGL.so).
serverComponentsExported: the trace flavor is a debuggable dev APK that adb drives by
explicit component (listen/token/env/onscreen/start/stop extras), so it exports
them. The plugin flavor ships to users, and an exported component would let any app
on the device start a server with an env of its own choosing. singleTop delivers the
adb extras to a running screen through onNewIntent. -->
<activity
android:name=".ServerControlActivity"
android:exported="${serverComponentsExported}"
android:launchMode="singleTop"
android:theme="@style/Theme.MobileGLPlugin" />
<service
android:name=".MobileGLServerService"
android:exported="${serverComponentsExported}"
android:foregroundServiceType="dataSync"
android:process=":mglsrv" />
<!-- P12 (on-screen server window): the ON-SCREEN display server. Its own process (:mglwin)
runs the TCP server on a thread and renders a headless client's window surface
(MOBILEGL_IPC_SURFACE=server) onto its SurfaceView. One server at a time: starting it
stops MobileGLServerService, starting the service kills :mglwin. Software window
rendering keeps HWUI off EGL here (Espryt's teardown terminates the default display);
configChanges keeps the Surface through rotation and overlay changes. -->
<activity
android:name=".MobileGLDisplayActivity"
android:configChanges="@integer/trace_replay_config_changes"
android:exported="${serverComponentsExported}"
android:hardwareAccelerated="false"
android:launchMode="singleTop"
android:process=":mglwin"
android:taskAffinity="${applicationId}.mglwin"
android:theme="@style/Theme.MobileGLPlugin" />
<!-- One benchmark run per process: the backend is latched at MobileGL
init and Espryt teardown kills the process-default EGL display, so
the bench must never share a process with the POST UI. The service
@@ -18,7 +18,7 @@ import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
/** Trace-only entry point for a TCP supervisor; EGL belongs to its session children. */
/** Entry point for the offscreen TCP supervisor; EGL belongs to its session children. */
public final class MobileGLServerService extends Service {
private static final String TAG = "MobileGLServer";
private static final String CHANNEL = "mobilegl-server";
@@ -10,11 +10,9 @@ import android.widget.LinearLayout;
import android.widget.TextView;
/**
* Bottom navigation bar switching between the dev screens (POST and the render
* server control). The bar only appears in flavors that ship both screens: the
* plugin flavor has no ServerControlActivity, so wrap() returns the content
* unchanged there. Tabs switch by reordering the other activity to the front,
* so each screen keeps its state while the other is open.
* Bottom navigation bar switching between the POST screen and the render server
* control screen. Tabs switch by reordering the other activity to the front, so each
* screen keeps its state while the other is open.
*/
public final class NavBar {
public static final int TAB_POST = 0;
@@ -25,19 +23,10 @@ public final class NavBar {
private static final int COLOR_TAB_INACTIVE = 0xFF9E9E9E;
private static final int BAR_HEIGHT_DP = 56;
private static final String SERVER_ACTIVITY = "top.mobilegl.plugin.ServerControlActivity";
private NavBar() {}
/** Returns content wrapped with the bottom bar, or content unchanged in plugin builds. */
/** Returns content wrapped with the bottom bar. */
public static View wrap(Activity activity, View content, int currentTab) {
final Class<?> serverClass;
try {
serverClass = Class.forName(SERVER_ACTIVITY);
} catch (ClassNotFoundException notInThisFlavor) {
return content;
}
LinearLayout root = new LinearLayout(activity);
root.setOrientation(LinearLayout.VERTICAL);
root.addView(content, new LinearLayout.LayoutParams(
@@ -47,7 +36,7 @@ public final class NavBar {
bar.setOrientation(LinearLayout.HORIZONTAL);
bar.setBackgroundColor(COLOR_BAR);
addTab(activity, bar, "POST", PostActivity.class, currentTab == TAB_POST);
addTab(activity, bar, "Render Server", serverClass, currentTab == TAB_SERVER);
addTab(activity, bar, "Render Server", ServerControlActivity.class, currentTab == TAB_SERVER);
root.addView(bar, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, dp(activity, BAR_HEIGHT_DP)));
return root;
@@ -4,7 +4,7 @@ import java.util.LinkedHashMap;
import java.util.Map;
/**
* The server role's environment, shared by the two servers the trace APK can run.
* The server role's environment, shared by the two servers the APK can run.
*
* <p>{@link MobileGLServerService} exec's the offscreen supervisor with it (a ProcessBuilder
* environment); {@link MobileGLDisplayActivity} applies it to its OWN process with Os.setenv /
@@ -2,42 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application>
<!-- Render server control screen: start/stop the offscreen TCP supervisor
(MobileGLServerService) or the on-screen display server. Not the launcher:
PostActivity is the entry icon and the bottom navigation bar switches between
the two screens; exported so adb can drive it by explicit component with
listen/token/env/onscreen/start/stop extras. -->
<activity
android:name=".ServerControlActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.MobileGLPlugin" />
<service
android:name=".MobileGLServerService"
android:exported="true"
android:foregroundServiceType="dataSync"
android:process=":mglsrv" />
<!-- P12 (on-screen server window): the ON-SCREEN display server. Its own process (:mglwin)
runs the TCP server on a thread and renders a headless client's window surface
(MOBILEGL_IPC_SURFACE=server) onto its SurfaceView. One server at a time: starting it
stops MobileGLServerService, starting the service kills :mglwin. Software window
rendering keeps HWUI off EGL here (Espryt's teardown terminates the default display);
configChanges keeps the Surface through rotation and overlay changes. -->
<activity
android:name=".MobileGLDisplayActivity"
android:configChanges="@integer/trace_replay_config_changes"
android:exported="true"
android:hardwareAccelerated="false"
android:launchMode="singleTop"
android:process=":mglwin"
android:taskAffinity="${applicationId}.mglwin"
android:theme="@style/Theme.MobileGLPlugin" />
<activity
android:name=".PluginActivity"
tools:node="remove" />
+6 -2
View File
@@ -30,9 +30,13 @@ subprojects {
externalNativeBuild {
cmake {
// P5 trace A/B: :MobileGL owns libMobileGL.so, not :app's replay runner.
// Both flavors ship the render server, so the split shape is on by default
// (-Pmobilegl.buildDisaggregated=OFF opts out); INPROC follows it, as in
// MobileGL/build.gradle.
val disaggregated = project.findProperty("mobilegl.buildDisaggregated") ?: "ON"
arguments += listOf(
"-DMOBILEGL_BUILD_DISAGGREGATED=${project.findProperty("mobilegl.buildDisaggregated") ?: "OFF"}",
"-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=${project.findProperty("mobilegl.buildDisaggregatedInproc") ?: "OFF"}",
"-DMOBILEGL_BUILD_DISAGGREGATED=$disaggregated",
"-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=${project.findProperty("mobilegl.buildDisaggregatedInproc") ?: disaggregated}",
)
mobileGlCmakeCompilerLauncher().takeIf(String::isNotEmpty)?.let { compilerLauncher ->
arguments += listOf(
+9 -3
View File
@@ -23,13 +23,19 @@ def mobileGlBuildServerSpike = {
(rootProject.findProperty('mobilegl.buildServerSpike') ?: System.getenv('MOBILEGL_BUILD_SERVER_SPIKE') ?: 'OFF') as String
}
// Shared native shape for BOTH plugin and trace APKs. Defaults keep existing
// callers on pull; disaggregated CI explicitly selects all three switches.
// Shared native shape for BOTH plugin and trace APKs. The render server (libMobileGLServer.so and
// the display server's JNI) is part of the split shape, and both flavors of a standalone plugin
// build ship it, so there the split shape is ON unless a build opts out with
// -Pmobilegl.buildDisaggregated=OFF. The runtime transport is unaffected: the app's
// MOBILEGL_TRANSPORT env stays opt-in (app/build.gradle.kts), so the monolith path is still the
// default. The FCL-embedded library keeps its old defaults (pull, no MG_Remote): FCL packages
// neither the server nor its screen.
// Disaggregated CI explicitly selects all three switches.
def mobileGlPipePush = {
(rootProject.findProperty('mobilegl.pipePush') ?: System.getenv('MOBILEGL_PIPE_PUSH_APK') ?: 'OFF') as String
}
def mobileGlBuildDisaggregated = {
(rootProject.findProperty('mobilegl.buildDisaggregated') ?: System.getenv('MOBILEGL_BUILD_DISAGGREGATED') ?: 'OFF') as String
(rootProject.findProperty('mobilegl.buildDisaggregated') ?: System.getenv('MOBILEGL_BUILD_DISAGGREGATED') ?: (standalonePluginBuild ? 'ON' : 'OFF')) as String
}
def mobileGlBuildDisaggregatedInproc = {
(rootProject.findProperty('mobilegl.buildDisaggregatedInproc') ?: System.getenv('MOBILEGL_BUILD_DISAGGREGATED_INPROC') ?: mobileGlBuildDisaggregated()) as String
+1 -1
View File
@@ -31,6 +31,6 @@ ctest --test-dir build-split -L 'integration-(spawn|tcp)' --output-on-failure
## Android
- 三份 APK flavour:pull / push / split(Gradle 属性 `mobilegl.pipePush`、`mobilegl.buildDisaggregated`、`mobilegl.buildDisaggregatedInproc`)。split APK 同样要设 `MOBILEGL_TRANSPORT`,不设就是单进程对照。
- 设备侧 server 有两种形态:离屏的前台 Service(`MobileGLServerService`)与上屏的 `MobileGLDisplayActivity`;同一时刻只有一个。
- 设备侧 server 有两种形态:离屏的前台 Service(`MobileGLServerService`)与上屏的 `MobileGLDisplayActivity`;同一时刻只有一个。plugin 与 trace 两个 flavour 的 APK 都带(standalone 构建默认开 `mobilegl.buildDisaggregated`),入口是 APK 底部导航栏的 “Render Server” 页;plugin flavour 的这三个组件不导出,只有 trace 允许 adb 直接按组件名拉起。
- 电脑当 client、手机当 server 的跨机配置(起 server、设令牌、`adb forward` 备用路线)见 [`../notes/p65/README.md`](../notes/p65/README.md) 的"两边怎么配"。
- 性能对比只用 Redmi `2f7cbe2e`,按 [`pin-verification-2026-09-07.md`](pin-verification-2026-09-07.md) 定频,reboot-clean、同热窗口、配对 A/B。
+32 -12
View File
@@ -9,14 +9,16 @@ import xml.etree.ElementTree as ET
def main():
root = pathlib.Path(__file__).resolve().parents[2]
trace = root / "android-plugin/app/src/trace"
src = root / "android-plugin/app/src"
trace = src / "trace"
main_src = src / "main"
android = "{http://schemas.android.com/apk/res/android}"
manifest = ET.parse(trace / "AndroidManifest.xml")
activity = next(a for a in manifest.findall("application/activity")
if a.get(android + "name") == ".trace.TraceReplayActivity")
config_value = activity.get(android + "configChanges", "0")
if config_value == "@integer/trace_replay_config_changes":
resources = ET.parse(trace / "res/values/config.xml")
resources = ET.parse(main_src / "res/values/config.xml")
config_value = resources.find("integer[@name='trace_replay_config_changes']").text
changes = int(config_value, 0)
# The failing emulator added resource overlays after replay had already started.
@@ -31,34 +33,52 @@ def main():
# finds it by this name to enforce one server at a time), software window rendering (HWUI must
# not share the EGL display Espryt terminates), and the same configChanges as the replay
# Activity (a rotation or an overlay must not destroy the window a session renders into).
display = next((a for a in manifest.findall("application/activity")
# The render server ships in BOTH flavors, so these components live in the main manifest.
# exported is a manifest placeholder (true for the adb-driven trace flavor, false for the
# plugin flavor that ships to users); a literal "true" here would export it from the plugin.
exported_placeholder = "${serverComponentsExported}"
main_manifest = ET.parse(main_src / "AndroidManifest.xml")
display = next((a for a in main_manifest.findall("application/activity")
if a.get(android + "name") == ".MobileGLDisplayActivity"), None)
if display is None:
raise RuntimeError("the trace manifest has no MobileGLDisplayActivity (P12 D7)")
raise RuntimeError("the main manifest has no MobileGLDisplayActivity (P12 D7)")
for attribute, expected in (("process", ":mglwin"), ("hardwareAccelerated", "false"),
("exported", "true"),
("exported", exported_placeholder),
("configChanges", activity.get(android + "configChanges"))):
if display.get(android + attribute) != expected:
raise RuntimeError(f"MobileGLDisplayActivity android:{attribute} must be {expected!r}, "
f"not {display.get(android + attribute)!r}")
# Every trace-flavour Java class that has NO android.* import belongs here, because javac
for tag, name in (("activity", ".ServerControlActivity"), ("service", ".MobileGLServerService")):
component = next((c for c in main_manifest.findall(f"application/{tag}")
if c.get(android + "name") == name), None)
if component is None:
raise RuntimeError(f"the main manifest has no {name}")
if component.get(android + "exported") != exported_placeholder:
raise RuntimeError(f"{name} android:exported must be {exported_placeholder!r}, "
f"not {component.get(android + 'exported')!r}")
# The trace manifest must not redeclare them: a second declaration would override the placeholder.
for tag, name in (("activity", ".ServerControlActivity"), ("service", ".MobileGLServerService"),
("activity", ".MobileGLDisplayActivity")):
if any(c.get(android + "name") == name for c in manifest.findall(f"application/{tag}")):
raise RuntimeError(f"the trace manifest redeclares {name}; it belongs to the main manifest")
# Every Java class that has NO android.* import belongs here, because javac
# plus `java` is the only way any of them is exercised outside an APK on a device. P7 added
# SpawnServerPath: a hand-rolled K=V;K=V parse whose failure mode on device is "the knob had
# no effect", which is why a defect in it survived from P6. P12 added ServerEnvironment: the
# server role's environment both servers build (the service's exec environment, the display
# Activity's own process) and the display's aspect-fit.
java_units = [
("top.mobilegl.plugin.trace", "TraceReplaySession", "TraceReplaySessionTest"),
("top.mobilegl.plugin.trace", "SpawnServerPath", "SpawnServerPathTest"),
("top.mobilegl.plugin", "ServerEnvironment", "ServerEnvironmentTest"),
(trace, "top.mobilegl.plugin.trace", "TraceReplaySession", "TraceReplaySessionTest"),
(trace, "top.mobilegl.plugin.trace", "SpawnServerPath", "SpawnServerPathTest"),
(main_src, "top.mobilegl.plugin", "ServerEnvironment", "ServerEnvironmentTest"),
]
with tempfile.TemporaryDirectory(prefix="mobilegl-replay-lifecycle-") as output:
sources = []
for package, unit, test in java_units:
sources.append(str(trace / "java" / package.replace(".", "/") / f"{unit}.java"))
for source_set, package, unit, test in java_units:
sources.append(str(source_set / "java" / package.replace(".", "/") / f"{unit}.java"))
sources.append(str(root / f"tools/trace_replay/{test}.java"))
subprocess.run(["javac", "--release", "11", "-d", output, *sources], check=True)
for package, _, test in java_units:
for _, package, _, test in java_units:
subprocess.run(["java", "-cp", output, f"{package}.{test}"], check=True)
print("Android replay manifest and lifecycle checks passed")