diff --git a/.github/scripts/validate-plugin-apks.sh b/.github/scripts/validate-plugin-apks.sh index bc63ec86..52a975dd 100644 --- a/.github/scripts/validate-plugin-apks.sh +++ b/.github/scripts/validate-plugin-apks.sh @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 210eb994..984f35f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Remote/Server/DisplayServerJni.cpp b/MobileGL/MG_Remote/Server/DisplayServerJni.cpp index 84452236..ee953abb 100644 --- a/MobileGL/MG_Remote/Server/DisplayServerJni.cpp +++ b/MobileGL/MG_Remote/Server/DisplayServerJni.cpp @@ -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: // diff --git a/android-plugin/app/build.gradle.kts b/android-plugin/app/build.gradle.kts index 01dd85b9..c6756f72 100644 --- a/android-plugin/app/build.gradle.kts +++ b/android-plugin/app/build.gradle.kts @@ -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" } } diff --git a/android-plugin/app/src/main/AndroidManifest.xml b/android-plugin/app/src/main/AndroidManifest.xml index 36715b97..a6155a1b 100644 --- a/android-plugin/app/src/main/AndroidManifest.xml +++ b/android-plugin/app/src/main/AndroidManifest.xml @@ -1,6 +1,14 @@ + + + + + + + + + + + + - - - - diff --git a/android-plugin/build.gradle.kts b/android-plugin/build.gradle.kts index f118094d..8ba5c02b 100644 --- a/android-plugin/build.gradle.kts +++ b/android-plugin/build.gradle.kts @@ -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( diff --git a/build.gradle b/build.gradle index 75e35afd..aa8fdb94 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/docs/Disaggregated/guide/build-and-run.md b/docs/Disaggregated/guide/build-and-run.md index d16234a1..3518f263 100644 --- a/docs/Disaggregated/guide/build-and-run.md +++ b/docs/Disaggregated/guide/build-and-run.md @@ -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。 diff --git a/tools/trace_replay/test_android_lifecycle.py b/tools/trace_replay/test_android_lifecycle.py index 32c448b8..f8ba52ce 100644 --- a/tools/trace_replay/test_android_lifecycle.py +++ b/tools/trace_replay/test_android_lifecycle.py @@ -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")