mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
17 KiB
17 KiB
MobileGL-disagg: Android APK and process model for an on-screen server path (commit 9f669e52)
Paths below are relative to C:\Users\geekerwan\AndroidStudioProjects\FoldCraftLauncher\MobileGL-disagg\. [V] means I read it in the code. [I] means I inferred it. [A] is an Android platform fact I'm confident of but did not check in this tree.
1. How the APK is built and packaged [V]
- The app module has
minSdk = 26,targetSdk = 34and ABI defaultarm64-v8a(android-plugin/app/build.gradle.kts:124-125, 24-31, 152-154). The library module and CMake also pin API 26 (build.gradle:43,CMakeLists.txt:80). - There are two flavors,
pluginandtrace(.tracesuffix), atbuild.gradle.kts:171-181. The app's own CMake issrc/trace/cpp/CMakeLists.txtand buildslibtrace_replay_runner.sofor both flavors (build.gradle.kts:204-209). Thepluginflavor then strips it (:229-233). libMobileGL.socomes fromimplementation(project(":MobileGL"))(build.gradle.kts:236), which is the rootbuild.gradle:com.android.libraryusing the rootCMakeLists.txt. The disaggregated/inproc/push switches are passed inbuild.gradle:58-66.useLegacyPackaging = true(build.gradle.kts:211-216) extracts the.sofiles tonativeLibraryDir, and the exec path needs that.libMobileGLServer.soisadd_executable(MobileGLServer ServerEntry.cpp), built whenMOBILEGL_BUILD_DISAGGREGATED AND NOT WIN32. It is PIE, links libMobileGL, hasRPATH $ORIGIN, and on Android usesPREFIX lib SUFFIX .soinsideCMAKE_LIBRARY_OUTPUT_DIRECTORY(CMakeLists.txt:1000-1053).main()inMG_Remote/Server/ServerEntry.cpp:19just callsmobilegl_server_main.MOBILEGL_BUILD_SERVER_SPIKEbuilds a separatelibMobileGLServerSpike.sostub (CMakeLists.txt:1055-1080), passed only by the trace flavor (build.gradle:96-110).- libMobileGL already carries Android JNI code (
DriverPostJni.cppandDriverBenchJni.cpp,CMakeLists.txt:675-680) and linksandroidandlog(:866-867).
2. Existing ownership of a Surface: TraceReplayActivity [V]
- The Activity runs in the default process (trace manifest
:67-76, noandroid:process). It loadstrace_replay_runner(TraceReplayActivity.java:22-24) and creates aSurfaceView(:41-42). - If
width/heightextras are given, it callssetFixedSize(:82-84). surfaceCreatedandsurfaceChangedboth callscheduleReplay, which starts the replay after 250 ms, only once (:85-104, 121-134).surfaceDestroyedis empty (:96-98).- The session survives Activity recreation through
onRetainNonConfigurationInstance(:69-79, 106-109). However, a recreated Activity's new Surface is never handed to the running replay, because of thehasStartedearly return (:122). configChanges=0x80000f80(res/values/config.xml:3-10) exists precisely so rotation and overlay changes don't recreate the Activity and kill the Surface.- JNI (
trace_replay_jni.cpp):- A per-process lease blocks concurrent replays (
:125-133). - With
use_pbufferthere is no window at all (:187). Otherwise it callsANativeWindow_fromSurface, or fails if the surface is already gone (:188-196). - The
:179-183comment records that a spawned server cannot take a window until P12.
- A per-process lease blocks concurrent replays (
- GLWS (
apitrace_glws_android.cpp):- A global
gNativeWindowis acquired and released (:60, 414-425). createSurfacechooseseglCreateWindowSurfaceplussetBuffersGeometrywhen there is a window, else a pbuffer (:198-219). The config'sEGL_SURFACE_TYPEfollows the same choice (:337).- The
eglSwapBuffersresult is ignored (:185-195). - The app's own glws calls
eglTerminateincleanup(:315-321).
- A global
- Conclusion: today's harness is one-shot. It never re-targets a new Surface; after a destroy it keeps rendering into an abandoned one, and the window reference only prevents a use-after-free.
3. Today's server process model [V]
- Service:
MobileGLServerServiceis an exported foreground service in:mglsrv(trace manifest:11-15) with a wake lock (MobileGLServerService.java:26-39).- It runs
ProcessBuilder(nativeLibraryDir/libMobileGLServer.so, endpoint, "--serve")(:60-61). - Environment it sets:
ROLE=server,DIAL=no,LOG_FORWARD=1; removesTRANSPORT,CONTROL,ENDPOINT,SERVER_PATH,RING_MB,STAGE_MB; setsTOKEN,LOG_FILE_PATH,LD_LIBRARY_PATH(:63-75). - Extras are
listen,tokenandenv(grammarK=V;K,:42-50). A reader thread pipes stdout to logcat and callsstopSelfon exit (:78-88).onDestroycallsdestroy()(:96-102). - It is driven by
tools/trace_replay/tcp_device_server.py:77-79.
- It runs
mobilegl_server_main(ServerMain.cpp:1028-1139):- It forces
ROLE=server(:1035-1038). - Hard preconditions:
DIAL=no, else exit 64; the scrub list, else exit 65 (:1039-1049). tcp://with--serverunsTcpSupervisor::Run(:1072-1079). It authenticates each Hello before forking (Judge:910-976), thenFork(:978-1010).- In the fork child it closes the listener and every other pending fd and calls
RunSessionwithHandoffSource. In the parent it callsCloseLocalCopyon its copy of the connection. - Unix
--serveforks per session (:1123-1133). - Single-session shape (no
--serve): the process is the session, with no fork. TCP usesListenerSource(:1096-1105); unix usesDataSource::None(:1106-1108).
- It forces
RunSessionis[[noreturn]](:268-638). Every exit path calls_exit:ExitBeforeAccept(:235-239), and:412, 429, 437, 455, 482, 637.- Per-session process-global setup:
MG_ConfigLoader::Init,Transport=Spawn,MGPipeSetServerProcessRole(true),ArmSessionLatch,PipeStats::Init,ActiveBackendType=hellowith the pinned-backend refusal, thenInitServerRoleForSpawn(:356-377). - The log forwarder is a global (
:474-476, 634).
- Per-session process-global setup:
- Design intent: one process per session means Espryt's process-level
eglTerminateteardown is isolated. This is D9 (docs/Disaggregated/ROADMAP.md:103).
4. Could a session run inside an app process (no fork, no exec)?
What RunSession and the server assume [V unless marked]:
_exiton every path (§3). Inside an app it would kill the Activity at the end of every session, so a returning variant is needed.- Session latch is process-global and never reset.
g_latchArmed/g_latched/g_latchCount/g_latchedLine(FatalFunnel.cpp:99-107, 145) have no reset function.- After one latched session, every later session in that process is declined:
ServerApplyWireSurfaceOpreturnsMISMATCH(SurfaceOpCodec.cpp:211), and the latch also stops the apply loop. - Unarmed faults go to
SessionFail, which callsstd::abort()(FatalFunnel.cpp:131-133, 168-180) and would take down the UI process.
- Server role is per process.
g_serverProcessRolemakes every thread a server thread (PipeInputs.cpp:357-375). That is harmless only in a process where nothing else calls libMobileGL. [I] - Backend latch.
BenchService.java:12-17says Espryt and Magma can never share a process. Sessions that switch backend within one process are unsafe; pin the backend withMOBILEGL_BACKEND_TYPE, which is already enforced atServerMain.cpp:371-375. - D9 / eglTerminate.
- Espryt uses
eglGetDisplay(EGL_DEFAULT_DISPLAY)(DirectGLES.cpp:16177). DestroyEGLContextcallseglTerminate(:16876-16919), and it runs at the start of every surface init (InitDisplayAndContext→:16159).- The repo claims this terminates HWUI's display (
main/AndroidManifest.xml:25-28,BenchService.java:14-16). [A, medium confidence] AOSP libEGL reference-countseglTerminateper display, which would contradict that. This has to be measured. The ANGLE path loadslibEGL_angle.soseparately (Loader.cpp:570-580).
- Espryt uses
- Everything else is reusable (so no blockers there):
ServerSession::Closeresets everything (ServerSession.cpp:968-1007).ServerLoop::Stopresets the backend (ServerLoop.cpp:920-926; theCreateBackendguard is at:169-172).PipeStatsInit/Shutdown can be restarted (PipeStats.cpp:381-422).- Sockets use
MSG_NOSIGNAL, so there is no SIGPIPE hazard (SocketTransport.cpp:466,FdPassing.cpp:151). - The server installs no signal handlers (the only
sigactionis client-side,PersistentMapTracker.cpp:423). - There is no Android load-time constructor in libMobileGL.
- Environment and logging. Reads from env include
DIAL, the scrub list,ROLE(cached at first log),LOG_FORWARD,BACKEND_TYPE,PreAuthKnobs::FromEnvironmentandMOBILEGL_TEST_*. In-process these must be set withOs.setenvor JNIsetenvbefore the first libMobileGL call and before any server thread starts. [I] - Supervisor re-used with threads instead of fork. The
Forkbody assumes separate fd tables: the child closes the listener and pending fds, and the parent callsCloseLocalCopy. Doing either on a thread would close the supervisor's or the session's live fds (ServerMain.cpp:990-995, 1009). The single-session shape also closes the listener after Accept (:402-405). - Different driver in an app process. [A]
GraphicsEnvironment(ANGLE and updatable-driver selection) applies only to app processes, and the driver sees a different process name (the logcat evidence has Adreno printing "Process Name: libMobileGLServer.so"). Numbers measured in the exec'd server may not transfer.
5. Options
- (a) Session inside the app process that owns the SurfaceView (JNI, background thread, no fork). Feasible.
- Precedent:
BenchService(:bench,System.loadLibrary("MobileGL"), JNI in libMobileGL atDriverBenchJni.cpp:327,System.exitafter each run;BenchService.java:45-73). - The window is
ANativeWindow_fromSurfacein the same process, which is exactly what ROADMAP P12 assumes: "server app owns its own SurfaceView" (ROADMAP.md:46,P6-ENDSTATE-REVIEW.md:313). - Isolation lost:
- Per-session address-space reset (D9, backend latch, session latch).
- Crash containment: an abort or driver crash now kills the display Activity too.
- Privilege separation between the display UI and code handling network bytes.
- Precedent:
- (b) Exec'd supervisor plus fork child, and pass the Surface to it. Not feasible at minSdk 26.
- [A] The NDK API to parcel an
ANativeWindow(ANativeWindow_writeToParcel/readFromParcel) is API 34, and minSdk is 26 (verified). - [A] An untrusted app cannot register with servicemanager.
- [A] libbinder refuses use in a fork child of a process that already used binder. The exec'd server does use binder freshly for gralloc and SurfaceFlinger (verified in logcat:
.trace-work/p7w6/bsl-spawn/.../logcat.txt:1778-1814), but it has no channel over which to receive a Surface. - A Java
Surfaceis Parcelable (AIDL Activity →:mglsrvservice) [A], but that only lands the window in:mglsrv's Java process, not in its exec'd children. That collapses into option (a).
- [A] The NDK API to parcel an
- (c) Child renders into AHardwareBuffers, sends them over a unix socket, app presents. Feasible at API 26.
AHardwareBuffer_sendHandleToUnixSocketis API 26 [A], and the repo notes it atP6-ENDSTATE-REVIEW.md:138.- Presenting at minSdk 26 needs an EGLImage (
EGL_ANDROID_get_native_client_buffer) and a GL blit in the app, becauseASurfaceTransaction_setBufferis API 29 [A]. - It keeps fork-per-session and D9 intact. But it requires a new buffer-ring/fence protocol, an Espryt default-framebuffer-to-AHB FBO, Magma AHB import, and one copy per frame. It is the largest change.
- (d) Existing precedent in the tree: only
BenchServicefor in-process work and the one-shotTraceReplayActivity. There is no AIDL, Messenger or AHB code. - Recommendation: (a), in a dedicated process such as
:mglwinholding the Activity and the session thread. The offscreen FGS, exec and fork path in:mglsrvstays unchanged, and the user picks a path by which component they start.- Stage 1: one session per process launch (BenchService-style exit), which keeps D9, the backend latch and the latch semantics.
- Stage 2: sequential sessions in one process, after the latch reset and the
eglTerminate/HWUI measurement.
6. Surface lifecycle
- [A] After
surfaceDestroyedreturns, the window must no longer be rendered to. The callback has to block until the apply thread has released the EGL surface or swapchain; this is the GLSurfaceView pattern. - Backgrounding destroys the surface and returning creates a new producer. Rotation with
configChangesgives onlysurfaceChanged. - While there is no surface, the session must keep consuming records. It should render offscreen and drop presents (or apply backpressure) rather than fail.
- Current backends make re-targeting expensive:
- Espryt: any different window surface runs
DestroyEGLContextpluseglTerminate, which loses every GL object (BackendObject_DirectGLES.cpp:962-972,DirectGLES.cpp:16159). - Magma:
InitWindowSurfacereplaces the wholeVulkanRenderer(BackendObject_DirectVulkan.cpp:343-350).
- Espryt: any different window surface runs
- [A] Once the Activity is backgrounded, the cached-app freezer can freeze
:mglwinwith no hangup (repo:P6-CONTRACT-DRAFT.md:128). Hold a foreground service in:mglwinfor the duration of a session.
OPEN QUESTIONS / RISKS
- Does Espryt's
eglTerminate(EGL_DEFAULT_DISPLAY)break HWUI in the same process? The repo comment and my AOSP understanding disagree; measure on Adreno and Redmi. - A window/context swap that keeps the context (Espryt) and a swapchain-only recreate (Magma) are prerequisites for surviving backgrounding. Without them a surface loss ends the session.
kEventSurfaceChangedhas no width/height, so the client's default framebuffer size would not follow the server window (ROADMAP.md:46).- The client still sends
SetWindowHandleunconditionally (BackendObject_Remote.cpp:215). Android handles latch by name (SurfaceOpCodec.cpp:165-171). X11 and Win32 tokens pass through toUnpackWindowHandle(ServerLoop.cpp:1050-1058). A server-owned substitution rule is needed. - Driver and profile differences in an app process versus the exec'd server (§4.9).
- Both
MobileGLServerServiceand the new Activity are exported with no permission, so any app on the device can start a network listener. - A single-session in-process listener cannot answer Busy while a session is live. A thread-based
TcpSupervisorhas the fd-table hazards in §4.8.
MINIMAL CHANGE LIST
android-plugin/app/src/trace/AndroidManifest.xml: addServerDisplayActivity(android:process=":mglwin",configChanges=@integer/trace_replay_config_changes, exported, fullscreen). Optionally add a foreground service in:mglwin. LeaveMobileGLServerServiceas the offscreen path.- New
android-plugin/app/src/trace/java/top/mobilegl/plugin/ServerDisplayActivity.java:- Parse
listen/token/envusing theapplyEnvironmentgrammar. Apply them withOs.setenv/unsetenv(ROLE=server,DIAL=no,LOG_FORWARD=1, scrub list,TOKEN,LOG_FILE_PATH, pinnedBACKEND_TYPE) beforeSystem.loadLibrary("MobileGL"). - Start a server thread calling
nativeServe(endpoint). surfaceCreated/surfaceChanged→nativeSetWindow(surface, w, h).surfaceDestroyed→nativeWindowLost(), which blocks.- On session end,
finish()and exit the process (stage 1).
- Parse
- New
MobileGL/MG_Remote/Server/ServerWindowJni.cpp(Android and disaggregated only), listed inCMakeLists.txt:675-680underMOBILEGL_BUILD_DISAGGREGATED:ANativeWindow_fromSurfaceand acquire/release, plus calls into the new server API. ServerMain.cpp:- Make
RunSessionreturn its exit code; fork sites become_exit(RunSession(...)), andExitBeforeAcceptreturns. - Add
extern "C" int mobilegl_server_serve_inprocess(const char* endpoint). It runs the same env checks as:1035-1049, then the single-session shape (:1096-1108) without_exit. Stage 2 wraps it in a loop that keeps the listener open.
- Make
FatalFunnel.{h,cpp}: addResetSessionLatch(), needed for stage 2.- Server window slot (e.g.
ServerLoop.{h,cpp}):- A process-global server-owned
ANativeWindow*with generation and size. TheCreateWindowSurface/UnpackWindowHandlearm (ServerLoop.cpp:1126-1138, 1050-1058) substitutes it when it is set. - New internal
SurfaceControlOps,WindowArrivedandWindowLost, posted from JNI throughRunSurfaceControlFrameand executed on the apply thread.
- A process-global server-owned
- Backends (other slices): an Espryt window swap that keeps the context (
BackendObject_DirectGLES.cpp:948-972,DirectGLES.cpp:16159/16225); Magma swapchain-only recreate (BackendObject_DirectVulkan.cpp:337-352); surface-changed width/height;WindowKind::ServerOwnedplus a client-side change atBackendObject_Remote.cpp:215. tools/trace_replay/tcp_device_server.py: a--windowmode that runsam start -n …/.ServerDisplayActivitywith the same extras.build.gradle.kts/build.gradle: no changes needed.