18 KiB
Client-side surfaces in split mode: map, open questions and change list (commit 9f669e52)
Tags used below: [V] means I read it in code. [I] means it is my inference. Paths are relative to MobileGL-disagg/.
1. How the client's EGL layer creates surfaces in split mode
Entry points. These are MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp:12-15 (eglCreateWindowSurface), :49-51 (eglMakeCurrent), :112-117 (eglSwapBuffers) and :120-122 (eglCreatePbufferSurface). Each forwards to MG_Impl/EGLImpl/EGLImpl.cpp. [V]
Window surface (EGLImpl.cpp:116-160). [V]
- A null native window is refused with
EGL_BAD_NATIVE_WINDOW(:130-133). A surfaceless client therefore cannot reach this path at all. - The
WindowHandleis built as{Backend = DetectWindowBackend(), Handle = window, Width/Height = attrib or 0}(:135-140). DetectWindowBackendis a compile-time#ifchain (:59-71): Android, then Apple (MetalLayer), then Win32, then Linux (X11).- Next come
state->CreateWindowSurface(:142) and thenbackendObject->CreateEGLWindowSurface(:153).CreatePlatformWindowSurface(:673-717) follows the same shape. - The client's EGL state records a window surface with no size (
MG_State/EGLState/Core.cpp:811-839; defaultsWidth/Height = 0atCore.h:181-182).eglQuerySurface(EGL_WIDTH)answers from that record (Core.cpp:1076-1082), so it returns 0 for a window unlessResizePlatformWindowSurfacehas run (EGLImpl.cpp:719-739).
Pbuffer (EGLImpl.cpp:453-476). The size defaults to 1x1 when no attribute is given (:458-459). It then goes through backendObject->CreateEGLPbufferSurface(surface, w, h) (:469). [V]
Remote backend (MG_Remote/Client/BackendObject_Remote.cpp). [V]
CreateEGLWindowSurface(:210-226):- waits for the apply thread to catch up (
:214); - calls
Server::ServerSetWindowHandle(handle)unconditionally (:215); - calls
ServerCreateEGLWindowSurface(:216); - calls
DrainPublishedEvents()(:224) so the server's surface-changed event is applied immediately; - finally runs the base-class bookkeeping (
:225).
- waits for the apply thread to catch up (
CreateEGLPbufferSurface(:237-244) has the same shape without the window handle.MakeEGLCurrent(:246-277) forwards first, then pumps the control plane, drains events and refreshes caps (:266-275).SwapEGLBuffersis deliberately not forwarded (:279-288). Present travels as a record.SetEGLSwapIntervalis forwarded (:290-298).
Forwarders (MG_Remote/Server/ServerLoop.cpp). [V]
PackWindowHandle(:1023-1028) storeswindowBackend = (Int)handle.BackendandnativeToken = (Uint64)handle.Handle, plus width and height.ServerCreateEGLWindowSurface(:1304-1311),ServerCreateEGLPbufferSurface(:1323-1331) andServerSetWindowHandle(:1399-1404) all funnel intoRunSurfaceControlFrame.- Under spawn or tcp,
RunSurfaceControlFramediverts tom_remoteSink(:793-795).ClientSessioninstalls that sink atClientSession.cpp:1140.
Wire encoding (ClientSession::RunRemoteSurfaceControlFrame, ClientSession.cpp:1205-1413). [V]
- It mints a sequence number (
:1212-1214) and callsEncodeSurfaceOpFrame(:1217). An encode error returnsMOBILEGL_ERR_UNSUPPORTED(:1218-1226). EncodeSurfaceOpFrame(Protocol/SurfaceOpCodec.cpp:125-148) maps the op to aWindowKind:- a pbuffer op is written as
WindowKind::Pbuffer(:133-135); - a window or SetWindowHandle op goes through
WireWindowKindForWindowBackend(:102-111): Android→AndroidNativeWindow, X11→X11, MetalLayer→MetalLayer, Win32→Win32Hwnd, Unknown→None; nativeTokenis copied raw intoSurfaceOp.nativeToken(:142-144; schema atprotocol.fbs:239-268).
- a pbuffer op is written as
- There is no client-side refusal. An Android client's
ANativeWindow*encodes cleanly and is only refused on the server. This contradicts CONTRACT-P6 §7.3 (MG_Remote/CONTRACT-P6.md:451-456).
Reply wait and cold-start budget. [V]
ControlReplyBudgetMs(ClientSession.cpp:124-134): the three bring-up ops (CreatePbufferSurface, CreateWindowSurface, MakeCurrent) getmax(MOBILEGL_IPC_CONTROL_TIMEOUT_MS=5000, MOBILEGL_IPC_COLD_START_MS=20000)until the first successful MakeCurrent setsm_serverBackendWarm(:1411). Knobs are parsed atConfigLoader.cpp:432-433.- Before sending, the client flushes and waits out its command prefix (
:1238-1244). - Each
SurfaceProgressrestarts the budget, up tomax(budget, kBarrierTimeoutMs)(:1265-1276,:1352-1375). A progress message naming the wrong seq is a protocol mismatch. - A
CapsSnapshotarriving mid-wait is adopted (:1341-1351), andFatallatches device-lost (:1376-1390). - After a
SurfaceReply, the client waits for event delivery up toeventHead(:1405-1408). SurfaceReply.defaultFbis dead: the encoder writes 0 (SurfaceOpCodec.cpp:192-193) and the decoder never reads it (:199-205).
Default framebuffer size (MG_Impl/Init.cpp:26-50). [V]
- The client's framebuffer 0 is a 512x512 RGBA8 placeholder with
Depth32FStencil8attachments. - The consumer is
ApplySurfaceChangedToClient(ClientSession.cpp:306-347), reached from thekEventSurfaceChangedcase ofDrainEventRing(:407-420). The payload isEventSurfaceChangedHead{Width, Height, InternalFormat, ...}(Transport/EventRing.h:91-100).- When Width and Height are non-zero, it reallocates all three attachments at that extent (
:316-338). - When they are zero, it changes formats only and the 512x512 extent is kept (
:339-346).
- When Width and Height are non-zero, it reallocates all three attachments at that extent (
- DirectGLES posts format only (
MG_Backend/DirectGLES/DirectGLES.cpp:15938-15951). Magma posts the swapchain extent (DirectVulkan/Renderer/SwapchainObject.cpp:323-339). - So under Espryt the client's default FBO is 512x512 even for pbuffers today.
- The event never updates the client EGL state's
SurfaceObject.Width/Height.
Present. [V]
- The
MGPPresentrecord carries onlyFrameSerial(MG_Pipe/MGPipeTypes.h:1768-1771), emitted atEmitTables.cpp:1116-1132. There is no surface identity in it. - On the server, DirectGLES calls
eglSwapBuffers(g_Display, g_Surface)(DirectGLES.cpp:16842). The swap therefore hits whatever surface is current when the record is applied.
2. How the Linux/WSL client replays traces today
glretrace windowing layer. The build uses its own EGL layer, tools/trace_replay/apitrace_glws_egl.cpp, compiled into mobilegl_trace_glretrace_common (tools/trace_replay/CMakeLists.txt:206-249). [V]
- It uses
MOBILEGL_TRACE_SURFACE=windowto choose between window and pbuffer (:321-324,:432-438,:592,:616). - The window path exists only under
__APPLE__(:443-466). On Linux it always callseglCreatePbufferSurface(w, h)(:467-476), even when the config was chosen withEGL_WINDOW_BIT. resize()destroys and recreates the pbuffer (:387-400). A trace can therefore produce several pbuffers over its lifetime.
Knobs. [V]
trace_replay_core.cpp:169setsMOBILEGL_TRACE_SURFACEfromrequest.usePbuffer, which defaults to true (trace_replay_core.hpp:58).- The CLI flags are
--window-surfaceand--pbuffer-surface(trace_replay_cli.cpp:22-24,:142-145). - glretrace is not run with apitrace's own
--pbufferflag. The surface choice is made entirely inside the custom layer.
Transport setup. [V]
run_trace_case.cmake:46-60setsMOBILEGL_TRANSPORT,IPC_SERVER_PATH,IPC_CONTROL,DATA=stream,TOKEN,REQUIRE_SAME_BUILDandLOG_FORWARD.- ctest adds
EGL_PLATFORM=surfacelessfor DirectGLES (tools/trace_replay/CMakeLists.txt:440-447). run_tcp_matrix.py:118-139setsMOBILEGL_TRANSPORT=spawnplus atcp://endpoint, and adds run-ahead and verb-barrier when requested.- The local server fixture
scripts/ci/tcp_server_fixture.py:60-77pinsEGL_PLATFORM=surfaceless, removesDISPLAYandWAYLAND_DISPLAY, and runs<server> <endpoint> --serve.
Integration harness (MG_IntegrationTest/Harness/HeadlessGL.cpp). [V]
- On Linux it always uses a pbuffer (
UseWindowSurfacereturns false at:138-139; pbuffer at:276-277) and forcesEGL_PLATFORM=surfaceless(:181-196). - On Android it uses a window from an
AImageReader(:136-137,:268-273). - The Split, Spawn, Tcp and TcpDevice arms are all host clients (
MG_IntegrationTest/CMakeLists.txt:2329-2336).
Can a Linux client create a window surface at all? Yes, but only mechanically, and no lane does. [V]
- It would announce
WindowKind::X11plus an XID. - At HEAD, X11 is NOT a named refusal.
WindowBackendForWireWindowKindaccepts X11 and Win32 (SurfaceOpCodec.cpp:113-123).DecodeWireSurfaceOprefuses onlyAndroidNativeWindowandMetalLayer(:165-175, latched at:217-228). - An accepted token is cast straight to
void*byUnpackWindowHandle(ServerLoop.cpp:1050-1058) and used byCreateEGLWindowSurface/SetWindowHandle(:1126-1138,:1232-1241). That is a wild pointer in the server process. - P6-ENDSTATE-REVIEW §2.1 (
docs/Disaggregated/P6-ENDSTATE-REVIEW.md:62-79) and CONTRACT-P6 §7.1/§7.2 "D8" (MG_Remote/CONTRACT-P6.md:426-449) specify an allow-list:- X11 →
Fatal{UnmigratedSurface,"X11@P12"}; Noneon a window op is a Fatal;nativeToken != 0is a Fatal;- reason: "Rule G": a value whose meaning comes from the peer's window system never crosses the wire.
- X11 →
- None of D8 is implemented.
- Android clients hit
AndroidNativeWindow@P12. That is why the device lanes require--use-pbuffer(run_android_retrace_local.py:604-611,trace_replay_jni.cpp:180-187,tools/device_bench/p6/README.md:117-120).
Surfaceless client and "show my framebuffer on the server's screen": options.
- A. Server-side policy, no client or wire change. [I]
- The APK (session config) says "present". The
CreatePbufferSurfacecase ofApplySurfaceControlFrame(ServerLoop.cpp:1144-1151) then creates the session's surface as a window surface on the APK'sANativeWindow, afterANativeWindow_setBuffersGeometry(win, frame.width, frame.height, fmt). - This keeps the client-fixed pbuffer size authoritative. SurfaceFlinger scales the buffer to the view, so client viewports, readbacks and goldens stay valid.
- The offscreen path is unchanged when the policy is off.
- The client keeps believing it has a pbuffer (EGL state, swap behaviour).
- The APK (session config) says "present". The
- B. Client opt-in, the P12 shape. [I]
- Add a new knob, e.g.
MOBILEGL_IPC_SURFACE=server, inMG_Config::Ipc. With it set,BackendObject_Remote::CreateEGLPbufferSurfaceandCreateEGLWindowSurfacesendCreateWindowSurfacewith an appendedWindowKind::ServerOwnedandnativeToken=0, and skipServerSetWindowHandle. - The server refuses by name if it owns no window.
- This needs a schema append and revision bump, and should land together with D8.
- Size: for a pbuffer, the client's width and height are the requested buffer geometry. For a client window, the server's size has to flow back through
kEventSurfaceChangedwith a non-zero W/H, and into the client EGL state's surface size, which nothing updates today.
- Add a new knob, e.g.
- Recommendation. [I] Use B's wire shape, gated by a server switch, with A's sizing rule (the client size wins via
setBuffersGeometry). Map exactly one surface per session to the view: the first pbuffer or window created, or the one current at Present. All others stay offscreen, because anANativeWindowaccepts only one connected EGL surface. - Multiple pbuffers, verified caveat. DirectGLES destroys and recreates the native context whenever a different surface is created (
BackendObject_DirectGLES.cpp:969-972,:1001-1004). glretrace'sresize()recreation therefore already costs a context today.
3. Tests and gates that pin today's behaviour
| Test / gate | Location | What it pins | Effect of the goal / D8 |
|---|---|---|---|
WindowBackendAndWireWindowKindMapExplicitlyBothWays |
MG_Test/Wire/SurfaceControlFrameTest.cpp:193-228 |
Mapping table, including X11 and Win32 | Needs a ServerOwned row [V] |
EveryFramedOpRoundTripsThroughTheWireEnvelope |
SurfaceControlFrameTest.cpp:230-274 |
X11 plus token 0xDEADBEEF decodes as None | Breaks under D8 [V] |
AnAndroidNativeWindowOnTheWireIsRefusedByName, AMetalLayerOnTheWireIsRefusedByName, AWireWindowKindThatNamesNoBackendFailsDecodeByName |
:320-390 |
Current refusals | Keep [V] |
ServerLoopEglTest.SuccessfulVoidWireOperationsReplyWithSuccessAndOriginalSequence |
ServerLoopTest.cpp:1713-1740 |
SetWindowHandle with WindowKind::None answers ok |
Breaks under D8 [V] |
| PeerLatchTest rows | PeerLatchTest.cpp:1249-1261; sender at :476-495, token 0x1234 |
AndroidNativeWindow and MetalLayer latched | Add X11, Win32, ServerOwned-without-a-window [V] |
| S5 | CONTRACT-P6 :597 |
Requires X11 to be refused and "token never reached UnpackWindowHandle" |
Not implemented; only the AndroidNativeWindow row exists [V] |
ph_latch_sites.py |
scripts/ci/ph_latch_sites.py:65-67 |
"SurfaceOp.windowBackend" declared unreachable |
Still true if the codec keeps mapping [V] |
fatal_census |
scripts/ci/fatal_census_baseline.json:65 |
Family UnmigratedSurface already present |
New refusal strings in that family need no new family [V] |
protocol_revision_pin |
scripts/ci/protocol_revision_pin.py, pins json |
Revisions 1 and 2 only | Any protocol.fbs append needs a bump plus --write [V] |
spawn_lane_parity |
scripts/ci/spawn_lane_parity.py:34, :43 |
Split and Spawn case sets must match | A device-only on-screen arm must be named there or kept outside the integration-* labels [V] |
| Pbuffer bring-up tests | ServerSpawnTest.cpp:994-1070, ServerLoopTest.cpp:1627, :1871, :1936, :2188 |
Pbuffer bring-up only | Stays green (offscreen default) [V] |
OPEN QUESTIONS / RISKS
- D8 is specified but not landed. Landing it breaks two existing tests (see §3). Should it ship before or together with ServerOwned? [V]
- Espryt's size gap. The client default FBO stays at 512x512 and a client window's
eglQuerySurfacereturns 0. The server window's size needs a return path:kEventSurfaceChangedwith W/H, plus an update of the client EGL state's surface size. [V] - Readback after swap on a window surface is undefined (
EGL_BUFFER_DESTROYED). The retrace snapshot timing inapitrace_fbo_dump.cpphas not been checked, so goldens could break on the on-screen path. [I] - Present pacing. A vsync-blocked server swap slows credit return (
MOBILEGL_IPC_PRESENT_CREDIT). This is good for display, but on-screen benchmark numbers are not comparable to offscreen ones. [I] - Server-side hazards.
ForgetCurrentTupleafter a surface is (re)created (ServerLoop.cpp:1136,:1150) and the DirectGLES process-globalg_Surfaceinteract with any SurfaceViewsurfaceDestroyed/surfaceChangedthat happens mid-session. [I] - The pbuffer-to-window mapping, if silent, is a semantic lie (EGL state says pbuffer, the swap really presents). It must be an explicit, logged mode that
runtime_mode_proof-style gates can prove. [I]
MINIMAL CHANGE LIST
MG_Remote/Protocol/protocol.fbs:249: appendServerOwned = 7. BumpMOBILEGL_PROTOCOL_CONTROL_REVISION(mg_protocol_base.h), regenerateprotocol_generated.h, runprotocol_revision_pin.py --write.MG_Backend/BackendObject.h:569-577: addWindowBackend::ServerOwnedbeforeWindowBackendCount. The range check inServerLoop.cpp:1039-1048follows automatically.Protocol/SurfaceOpCodec.cpp:- add both mapping rows (
:102-123); - move the out-of-range upper bound from MetalLayer to ServerOwned (
:178-179) and thestatic_assert(:58); - implement D8: X11 and Win32 give
UnmigratedSurface"X11@P12"/"Win32Hwnd@P12";Noneon a window op andnativeToken != 0giveProtocolCorruption, with latches next to:217-235; - refuse a ServerOwned op when the server has no window (named).
- add both mapping rows (
Client/BackendObject_Remote.cpp:210-244:- when the new knob is set on spawn/tcp, send CreateWindowSurface / ServerOwned with token 0 and skip
ServerSetWindowHandle(:215); - for a pbuffer, carry the client's w and h as the requested geometry;
- mirror the refusal of
AndroidNativeWindow/X11 on the client, before sending (CONTRACT-P6 §7.3).
- when the new knob is set on spawn/tcp, send CreateWindowSurface / ServerOwned with token 0 and skip
Config.h~:596andConfigLoader.cpp~:433: add the knob (e.g.MOBILEGL_IPC_SURFACE=offscreen|server, defaultoffscreen) and log it in the IPC line (:476-484).Client/ClientSession.cpp:306-347: when a surface-changed event carries W/H for the session's server-owned surface, also resize the client EGL state's surface (state->ResizeSurface, as used atEGLImpl.cpp:724).DirectGLES.cpp:15947-15950: publish the surface width and height (currently format only). Server side, and needed so the client's default FBO stops being 512x512.- Server side (other slices; listed for completeness):
ServerLoop.cpp:1126-1151: a ServerOwned arm that substitutes the APK'sANativeWindowand callssetBuffersGeometry;BackendObject.cpp:232-238register point andBackendObject_DirectGLES.cpp:962-964sameHandlekey;- a CreateEGLWindowSurface arm per backend (DirectGLES
:955, VulkanBackendObject_DirectVulkan.cpp:424).
- Trace tooling (optional): no glws change is needed for approach B, because it maps pbuffers. Update the help text in
run_android_retrace_local.py:604-611anddevice_bench/p6/README.md:117-120. - Tests:
- update
SurfaceControlFrameTest.cpp:193-274(X11 becomes a refusal, add a ServerOwned row) andServerLoopTest.cpp:1713-1740(dropSetWindowHandle/Nonefrom the "void ok" list); - add PeerLatch rows for X11, Win32 and ServerOwned-without-a-window (this is S5);
- add a unit test proving the client knob's encode, and a device-only TcpDevice on-screen smoke test named in
spawn_lane_parity.py.
- update