mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
162 lines
13 KiB
Markdown
162 lines
13 KiB
Markdown
# P12 subset "on-screen server window" — implementation brief (2026-09-23)
|
|
|
|
Base: `feat/disaggregated@9f669e52` (P7 closed). Package tree `~/w7/p12-onscreen`, branch `p12/onscreen`,
|
|
build dirs `build-split` (disaggregated, ITEST tcp port 40813) and `build-linux` (pull build, for G1), both
|
|
already built at base. G1 base: `.text` 0xa52203, symbols `~/w7/logs/p12/pull-syms-base.txt`.
|
|
Gate: `bash ~/w7/notes/p12/gate.sh ~/w7/p12-onscreen <tag>`.
|
|
|
|
## User rulings (binding)
|
|
|
|
1. The Android server APK creates an ANativeWindow (its own SurfaceView) and renders the IPC render stream
|
|
ON SCREEN on it. **Both rendering paths stay available**: the existing offscreen (pbuffer) path and the
|
|
new on-screen path.
|
|
2. **Only one of them is active at a time**, decided at server initialization / context creation.
|
|
3. Topology: **in-process server** — the display Activity's own app process runs the TCP server on a thread
|
|
and renders straight into its SurfaceView; sessions run sequentially in that process. (Not fork + AHB.)
|
|
4. Client side: **the client opts in via a new `WindowKind::ServerOwned`** (control protocol revision 3,
|
|
client env knob). In this mode the client can be **fully headless**: it asks the server to create the
|
|
real (window) context/surface and gets the surface geometry etc. from the server — as if the client were
|
|
rendering directly on the server's GPU and screen.
|
|
|
|
## Design
|
|
|
|
### D1 Client knob and headless window surfaces
|
|
- New IPC knob `MOBILEGL_IPC_SURFACE=offscreen|server` (default `offscreen`), added to `MG_Config::IpcTable`
|
|
(`Config.h`, "a later phase's knob is added HERE") + `ConfigLoader.cpp`, printed in the IPC config log line.
|
|
Meaningful only when the client talks to a remote server (spawn/tcp); ignored (logged) otherwise.
|
|
- With `server`: the client EGL layer's `eglCreateWindowSurface` / `eglCreatePlatformWindowSurface` accept ANY
|
|
native window, **including NULL** (today NULL is `EGL_BAD_NATIVE_WINDOW`, `EGLImpl.cpp:130-133`) — a headless
|
|
client has no window. `BackendObject_Remote::CreateEGLWindowSurface` then sends **one** `CreateWindowSurface`
|
|
with `WindowKind::ServerOwned`, `nativeToken = 0`, width/height = the size the client asked for (the
|
|
existing `WindowHandle.Width/Height` from `EGL_WIDTH/EGL_HEIGHT` attribs; 0/0 = "the server's own size"), and
|
|
**does not send `SetWindowHandle`**. The client's native window value never reaches the wire (Rule G/H).
|
|
- `eglCreatePbufferSurface` stays a pbuffer in both modes (offscreen). See D4 for the one-mode-per-session rule.
|
|
- With `offscreen` (default) nothing changes, byte for byte.
|
|
- Geometry flows back: when a `kEventSurfaceChanged` with non-zero W/H arrives for the session's server-owned
|
|
surface, the client updates the EGL state's surface size (so `eglQuerySurface(EGL_WIDTH/HEIGHT)` answers the
|
|
server's real size; `state->ResizeSurface` as used at `EGLImpl.cpp:724`) in addition to the default-FB
|
|
reallocation `ApplySurfaceChangedToClient` already does (`ClientSession.cpp:306-347`). Make sure the client
|
|
has the size before `eglCreateWindowSurface` returns (the create path already drains events,
|
|
`BackendObject_Remote.cpp:224`).
|
|
|
|
### D2 Wire
|
|
- Append `ServerOwned = 7` to `WindowKind` (`protocol.fbs:239-250`; append-only), regenerate + commit the
|
|
generated header, bump `MOBILEGL_PROTOCOL_CONTROL_REVISION` 2 -> 3 (`mg_protocol_base.h`, with its comment
|
|
row), `protocol_revision_pin.py --write`. wireFingerprint moves: APK and host redeploy together.
|
|
- Codec (`SurfaceOpCodec.{h,cpp}`): encoder maps the client's server-owned request to `ServerOwned`; decoder
|
|
accepts `ServerOwned` only on `CreateWindowSurface` (on `SetWindowHandle` it is a named refusal), only with
|
|
`nativeToken == 0` (non-zero = `ProtocolCorruption`, latched like the other peer-corruption rows).
|
|
Update the static_assert / out-of-range bound. Keep `AndroidNativeWindow@P12` / `MetalLayer@P12` refusals.
|
|
- Do NOT add a `WindowBackend` enumerator in `MG_Backend/BackendObject.h` (pull build, G1). Carry the
|
|
server-owned request in the MG_Remote frame with a MG_Remote-local marker; on the server it becomes
|
|
`WindowBackend::Android` + the server's own `ANativeWindow*`, substituted ON THE SERVER at apply time. No
|
|
pointer ever crosses the wire.
|
|
- D8 (X11/Win32/None whitelist, CONTRACT-P6 §7.2) is OUT of scope (separate follow-up).
|
|
|
|
### D3 Server display (the on-screen server's window)
|
|
- New `MG_Remote/Server/ServerDisplay.{h,cpp}`: process-wide holder of the server-owned window, set only by the
|
|
in-process display server (Android). API sketch: `Attach(ANativeWindow*)` (acquire), `Detach()` (blocking,
|
|
see D6), `HasDisplay()`, a geometry request hook (native -> Java up-call: `SurfaceHolder.setFixedSize(w,h)`
|
|
for w,h > 0, `setSizeFromLayout()` for 0/0), and `AcquireFor(w,h, deadline)` that waits (bounded, ~10 s,
|
|
wakes on detach/shutdown) until a window is attached and reports the requested size (or any size for 0/0).
|
|
Linux/host and the exec'd supervisor have no display: `HasDisplay()` is false.
|
|
- `ServerLoop` `CreateWindowSurface` arm, for a ServerOwned frame: no display -> reply failure with a distinct
|
|
result and a named log `Refuse ServerOwned: this server owns no display` (NOT a latch — it is a
|
|
configuration error, not peer corruption); otherwise substitute the window and call the backend's
|
|
`CreateEGLWindowSurface` exactly as monolith Android does (Register -> Activate -> SetWindowHandle ->
|
|
InitWindowSurface). Log an arm proof once per surface: `MG_Remote server: surface=window WxH owner=server`
|
|
(and `surface=pbuffer WxH` on the pbuffer arm). The apply thread may wait for the window; the existing
|
|
`SurfaceProgress` heartbeat must keep the client's reply budget alive while it does.
|
|
- Reporting size: Magma already publishes swapchain extent in `OnSurfaceChanged` (`SwapchainObject.cpp:323-339`).
|
|
Espryt publishes format only (`DirectGLES.cpp:15939-15950`): publish W/H (via `QueryCurrentSurfaceSize`) for a
|
|
window surface in the disaggregated build only — `DirectGLES.cpp` is in the pull build, so guard it
|
|
(`#if MOBILEGL_BUILD_DISAGGREGATED` or equivalent) and keep G1 byte-identical. Consider whether it must be
|
|
limited to server-owned windows (publishing W/H switches the client to the reallocating arm).
|
|
- Espryt: `sameHandle` dedup (`BackendObject_DirectGLES.cpp:962-967`) keys on surface + window; with the
|
|
server's pointer it dedups correctly. A new surface token re-creates the context (existing behaviour).
|
|
- Magma: a new renderer on the SAME ANativeWindow must not be created while the old renderer's VkSurface/
|
|
swapchain still holds it (`VK_ERROR_NATIVE_WINDOW_IN_USE_KHR`); check `BackendObject_DirectVulkan.cpp:337-352`
|
|
ordering and fix if needed (guard for G1 if that file is in the pull build).
|
|
|
|
### D4 One mode per session ("only one active", decided at context creation)
|
|
- A session's surface mode latches at its FIRST surface creation: ServerOwned -> on-screen; pbuffer -> offscreen.
|
|
A later surface of the other kind in the same session is refused by name (`SurfaceModeMismatch`, reply
|
|
failure, logged on both sides, no latch). A server with no display serves only offscreen sessions.
|
|
- Server-level: the offscreen supervisor (exec'd, `MobileGLServerService`) and the on-screen display server
|
|
(in-process, `MobileGLDisplayActivity`) are mutually exclusive on the device (D8).
|
|
|
|
### D5 In-process server
|
|
- `RunSession` (`ServerMain.cpp:268-638`, `[[noreturn]]`, `_exit` on every path) becomes returning (an exit
|
|
code); every fork site becomes `_exit(RunSession(...))` so the forked shapes are unchanged.
|
|
- A new exported entry for the in-process server (e.g. `mobilegl_server_serve_inprocess(endpoint, ...)`), same
|
|
env preconditions as `mobilegl_server_main` (`:1035-1049`), TCP only, `--serve` semantics with THREADS
|
|
instead of fork: pre-auth before hand-off unchanged, one session at a time, an authenticated second Hello
|
|
while a session is live -> `Refuse{Busy}`. `TcpSupervisor`'s fork body closes fds per fd-table
|
|
(`:990-995, 1009`); the thread hand-off must not. Reuse TcpSupervisor with a pluggable hand-off if tractable.
|
|
- `FatalFunnel`: add `ResetSessionLatch()`; the in-process server arms the latch for every session and resets
|
|
it between sessions. Unarmed `SessionFail` still aborts (documented: in-process mode loses crash isolation).
|
|
- Backend pinned for the process lifetime: an explicit extra/env, else the first session's backend becomes the
|
|
pin (Espryt and Magma must never share a process, `BenchService.java:12-17`); mismatch -> the existing
|
|
pinned-backend refusal (`:371-375`).
|
|
- Everything else per `map-apk-process` §4 (config init per session, server role, PipeStats restartable).
|
|
- Must be testable on the Linux host: run the in-process entry on a thread in a test (pbuffer sessions) and
|
|
prove two sequential sessions, latch reset (second session serves after the first latched), and Busy.
|
|
|
|
### D6 Window lost
|
|
- Java `surfaceDestroyed` -> JNI `Detach()` blocks (bounded ~3 s) until the apply thread has released the
|
|
backend surface (post onto the apply thread; `ReleaseEGLResources`-style), then the live on-screen session is
|
|
latched by name (new `FatalFamilies.def` row, e.g. `ServerWindowLost`) so the client gets a clean
|
|
device-lost. The display server keeps listening; the next on-screen session waits for a window (D3) or is
|
|
refused by name. `surfaceCreated` re-attaches. A session that never had a window is unaffected.
|
|
|
|
### D7 Android (trace flavor)
|
|
- New `MobileGLDisplayActivity` (trace flavor), `android:process=":mglwin"`, exported like the service,
|
|
fullscreen SurfaceView, `FLAG_KEEP_SCREEN_ON`, `configChanges` like `TraceReplayActivity`,
|
|
`android:hardwareAccelerated="false"` (keeps HWUI off EGL: Espryt teardown `eglTerminate`s the default
|
|
display, `main/AndroidManifest.xml:25-28`). Extras: `listen`, `token`, `env` (same `K=V;K` grammar as
|
|
`MobileGLServerService.applyEnvironment` — share the helper), optional `backend`. Applies env with
|
|
`Os.setenv/unsetenv` (ROLE=server, DIAL=no, LOG_FORWARD=1, scrub list, TOKEN, LOG_FILE_PATH) BEFORE
|
|
`System.loadLibrary("MobileGL")`. Aspect-fit (letterbox) the SurfaceView when a fixed buffer size is set.
|
|
- JNI in libMobileGL (Android + disaggregated only, next to `DriverPostJni.cpp` in `CMakeLists.txt`):
|
|
start/stop the in-process server thread, attach/detach the window (`ANativeWindow_fromSurface`, acquire/
|
|
release), the geometry up-call.
|
|
- Readiness line in logcat (`listening on tcp://...`) like the supervisor's.
|
|
|
|
### D8 Only one server active on the device
|
|
- The Activity on start stops `MobileGLServerService`; the service on start kills `:mglwin`
|
|
(`ActivityManager.getRunningAppProcesses` + `Process.killProcess`, same uid) and waits for it to go.
|
|
- Forked session children get `prctl(PR_SET_PDEATHSIG, SIGKILL)` (+ the getppid race check) so they die with
|
|
their supervisor (fixes the documented child leak, `notes/p65/code-review-findings.md:9`).
|
|
- The in-process listen retries `EADDRINUSE` for up to ~5 s (the previous server may still be dying).
|
|
|
|
### D9 Tools
|
|
- `tools/trace_replay/tcp_device_server.py --surface window|pbuffer` (default pbuffer = today): window starts
|
|
the Activity (`am start -n <pkg>/top.mobilegl.plugin.MobileGLDisplayActivity` + the same extras).
|
|
- Linux glws `tools/trace_replay/apitrace_glws_egl.cpp`: `MOBILEGL_TRACE_SURFACE=window` on Linux creates
|
|
`eglCreateWindowSurface(dpy, cfg, (EGLNativeWindowType)0, {EGL_WIDTH,w,EGL_HEIGHT,h})` (only valid with
|
|
`MOBILEGL_IPC_SURFACE=server`; otherwise the create fails by name); `resize()` destroys + recreates at the new
|
|
size. So `trace_replay --window-surface` + `MOBILEGL_IPC_SURFACE=server` replays a trace on the phone screen.
|
|
|
|
## Constraints (all must hold)
|
|
- G1: pull build `.text` 0xa52203 and 0 symbols added/removed. Every edit to a file compiled into the pull
|
|
build (`MG_Backend/*`, `MG_Impl/*`, `MG_State/*`, `DirectGLES.cpp`, ...) must be guarded so the pull build is
|
|
byte-identical. Measure with the gate.
|
|
- fatal_census (79, down-ratchet; every new `Fatal{Word` needs a `FatalFamilies.def` row), link ratchet 171
|
|
monotone, spawn_lane_parity 0 errors (device-only or new arms must be named there), protocol revision pin,
|
|
wire_declines_audit, hygiene (stdio grep in MG_Backend/MG_State, gen_pipe clean, doc citations strict).
|
|
- fbs append-only; no branching on link kind above `Transport/`; handshake refusals are `Refuse`, never aborts.
|
|
- Every existing lane stays green (base numbers at 9f669e52: unit 2568, integration-split 322, spawn 239,
|
|
tcp 243, magma-split 125, magma-spawn 104, magma-tcp 89, magma-full-split 558, integration-gpu 2188 x 2).
|
|
- Red-once per behaviour (a test that is red without the fix and green with it), noted in the report.
|
|
- Commits: single-line `[Scope, Scope] (P12): <what>` (e.g. `[MG_Remote, MG_Test] (P12): ...`), NO trailers of any
|
|
kind (no Co-Authored-By). Several focused commits are fine.
|
|
|
|
## Working rules
|
|
- Edit files through `\\wsl.localhost\Arch\home\swung\w7\p12-onscreen\...` with Read/Edit/Write (UNC is writable).
|
|
- Run commands as `MSYS_NO_PATHCONV=1 wsl -d arch --cd /home/swung -- bash -lc '<cmd>'`. The Windows command
|
|
line mangles heredocs and nested quotes: put anything non-trivial in a script file (Write via UNC, then run
|
|
it; strip CR with `sed -i 's/\r$//'` if needed). Long jobs: `setsid nohup ... &` and poll the log.
|
|
- git ONLY inside `/home/swung/w7/p12-onscreen` via `wsl`. NEVER touch `~/w7/pipe` or the Windows checkouts.
|
|
Never `git lfs pull`. Never push.
|
|
- The phone (Redmi 2f7cbe2e) is shared: any adb work runs under `flock -w 3600 /home/swung/w7/locks/2f7cbe2e.lock <cmd>`.
|