- eglSwapBuffersWithDamageEXT is an extension entry point that Android's
framework libEGL does not export through dlsym; requiring it was a
bring-up FATAL on TB321FU. Resolve EXT, fall back to the KHR spelling
(same function, interchangeable signature), then to plain
eglSwapBuffers (a zero-rectangle damage call is defined to be exactly
that). The GLES dlsym fallback gains a self-reference guard for
MobileGL installed under a GLES soname, where dlsym would otherwise
hand the backend the frontend's own entry points and recurse forever.
- NormalizePixelFormat maps GL_STENCIL_INDEX8/GL_STENCIL_INDEX to
GL_STENCIL_INDEX/GL_UNSIGNED_BYTE instead of falling through to
RGBA/UBYTE, which no driver can pair with stencil-only storage.
- Tests pin the EXT->KHR->none preference order and the stencil pair.
- The anland handoff is rewritten to the post-P14 landed state: device
evidence (win-smoke green screen, Konsole on screen, eglinfo/glxinfo),
the remote-only recovery procedure, and the known gaps (Plasma session
bring-up, cursor scale, the no-CPU-blit requirement, resize/surface
loss, AVC audit).
A session that dies without its teardown (crash, SIGKILLed client) leaks
its native window surface; the server-owned display window is one
ANativeWindow for the process's lifetime, and Adreno refuses a second
eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW - on TB321FU this
made every session after the first die with 'MobileGL could not acquire
the app Surface 300b'. InitWindowSurface now finds and destroys any
surface still bound to the window it is about to use, and the session
registry records the window with each surface to make that possible.
The document still described the state before the state-ownership work
landed: it called the unified server design unfinished, listed the
KernelSU grant as unsolved, said the directory bind had not taken effect,
and presented MobileGLAppsService plus the per-app fork worker as pending
drafts. None of that is true now.
Rewritten against the current tree and the device:
- header: architecture landed per the user's correction, acceptance still
open; the rejected fork-worker/AppsService path is removed and the
unified server lives in the MobileGL library (design 11-state-ownership,
commits d52c031c / 4ed1c9ce / 5c6399e2 / ff212061 / 5a0f13fd, plus the
landed 6fb880cd / ebc552a0 that used to be listed as uncommitted)
- MobileGL: HEAD moved to 5a0f13fd; the state-ownership section now records
what S1-S6 landed and the debts that remain (process-level unit shadows,
g_backendContextGeneration, per-record ContextSlot, the glIsBuffer probe,
the window half of pbuffer->window)
- anland: 2ffa1f5 and 33e496c pushed; the draft inventory is now a landed
inventory plus the three uncommitted KWin-acceptance fixes
- device: KernelSU grant resolved (reinstall changed the uid to 10270), the
clone restarted with the directory bind effective, the 5a0f13fd APK
serving @anland-mobilegl with six completed client sessions, and the
client library installed with a stale build stamp
- new: concurrency warning (parallel sessions have overwritten the dist and
the source tar), the current desktop-session.service failure state, and a
rewritten continuation order
Verified: doc citations resolve (scripts/check_doc_citations.py), device
facts checked against HA27Q3LQ at refresh time.
GL_ARB_shader_objects/vertex_shader/fragment_shader and
GL_ARB_texture_non_power_of_two are core since GL 2.0 and trivially true
for the 4.6 this backend claims, but KWin 6's EglContext::checkSupported()
requires exactly these strings before it brings up compositing. Found on
TB321FU: kwin_wayland --anland failed with 'Could not initialize rendering
context' after a successful context create + makeCurrent on the
server-owned window surface.
The last piece of state ownership this phase owed: the eight DirectGLES twin
tables resolved a handle process-wide, so two contexts of one session that do
NOT share a share group - which have independent name spaces, so the same
{slot, gen} in both is legal - were handed the SAME twin, driver ids,
persistent map and pooled store included. Two contexts that DO share a group
were served by whichever of them twinned first. Design and per-slice shape:
docs/Disaggregated/design/11-state-ownership.md (S6).
Key: DirectGLES.h's TwinKey{SessionKey, ShareGroupKey} - the same two words
the applier's object records are filed under, with the group derived by
MGPipeApplierShareGroupKeyFor so a record and the twin describing it can never
land in different buckets. Session is in the key because a twin names a driver
id and no driver id crosses a connection.
Resolution: SessionRuntime.cpp's ResolveThreadTwinKey, installed by
InstallSessionRuntimeHooks beside the S2 PipeInputs probe, the S4 native
tuple key and the S5 applier key. It reads the ThreadSessionScope's session
and CurrentContextToken() - apply-thread state only, never MG_State, so the
frontend-keyed-lookup prohibition on the apply thread still holds. A thread
that belongs to no session answers {0, 0}, which is the process singleton this
replaces; the monolith and every unit case are unchanged by construction.
Shape: SlotTables.h's SlotTableRegistry holds one SharedPtr bucket per key,
with a thread-local last-key memo. It forwards every handle-keyed entry -
GetOrCreate, FindByHandle, LiveGenAt, HandleOf, StateForHandle,
NoteStateForHandle, ReleaseByHandle, ForEachLive, the counters - to the
CALLING THREAD's bucket, so the ~150 call sites in Managers.cpp and
DirectGLES.cpp are literally unchanged. The holder list stays process-wide: a
death notice is about an object, not a bucket, and each shared holder is one
bucket. ForEachLive now walks the calling thread's bucket (what a context can
see is its own group); the death and diagnostic walks take the new
ForEachBucket.
DropEveryTwinForEndedServerSession is session-scoped: it runs on the apply
thread of the session that is ending, while S2 lets neighbour sessions keep
rendering in the same process. No session on the thread keeps the old
process-wide drop verbatim. Managers.h's "a texture is owned by exactly one
context (share groups are not implemented)" is corrected: a texture belongs to
a share group and its sibling contexts legitimately mutate it.
Tests: MG_Test/Wire/TwinOwnershipTest.cpp, five cases driving the real twin
tables with no EGL and no wire - the same handle in two nonshared groups is
two twins and neither overwrites the other; two contexts of one group resolve
ONE twin; a per-session drop takes only that session's; a thread with no
session still gets the process-wide table; and the six-registry template
repeats the first three. Negative control run and reverted: making the
registry answer a single {0, 0} bucket turns four of the five red.
Full host ctest against a stashed baseline, compared by test name: 0 new
failures. The one name in the difference set
(LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime) is a
pre-existing flake, failing on the baseline too with the identical assertion.
The 103-case acceptance set plus the five new cases is 108/108 green.
Remaining debts (documented): the unit shadows (g_activeTextureUnit,
g_boundTexturesCache, g_boundSamplersCache) are still process-global; the
driver-level glIsBuffer probe is still not usable on this host, so sharing is
proven at the twin layer (the twin IS the object holding the driver id); the
backend context/sync generations are still process-wide; the per-record
ContextSlot is still S1's edge declaration.
Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.
S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.
S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.
S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.
S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.
S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.
Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.
Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
Wayland Present() failure now returns EGL_BAD_SURFACE instead of being
dropped. In spawn mode a GLX X drawable is backed by a remote pbuffer so
the XID is never mistaken for an Android ANativeWindow; X11 pixel present
and remote resize are still unimplemented.
The whole stack assumes one process = one server = one session = one
context = one share group. The anland unified server needs that equation
broken in the library instead of fork-per-app workers. This document fixes
the four ownership layers (session, context, share group, thread current),
the wire changes (per-record context slot, CreateContext/DestroyContext
SurfaceOps, context-tagged reverse events), and the staged plan P14a-d
with acceptance tests.
A Wayland client's window is its own wl_surface, and what the compositor shows is the wl_buffer
attached to it - but the pixels of a MobileGL frame are drawn where the backend is, on a split
build in another process on another OS. So far a wl_egl_window was taken for an X11 window and
its client-side address sent to the Android server (EGL_BAD_NATIVE_WINDOW at best), and under
glvnd a Wayland display never reached this vendor at all.
A window on a Wayland display - asked for as EGL_PLATFORM_WAYLAND, or a wl_display recognised by
its first word being wl_display_interface - is now a window surface in the EGL state at the
wl_egl_window's size with a pbuffer of that size behind it in the backend, whatever
MOBILEGL_IPC_SURFACE says. At every swap the frame is read back out of the default framebuffer
through this library's own glReadPixels (the application's read framebuffer, pack buffer and pack
state put back as they were), its rows flipped and swizzled into a wl_shm buffer, and attached,
damaged and committed on the application's wl_surface. Three buffers rotate on their release
events, dispatched on an event queue of the surface's own. libwayland-client is loaded at run
time, so neither the build nor the library depends on it. EGL_KHR_platform_wayland and
EGL_EXT_platform_wayland are announced on Linux.
Measured: glmark2-wayland runs its build scene at 13 FPS (400x300) and the window shows the frame
under kwin. A window resize is not followed yet; the surface keeps its creation size.
Two defects, both found by asking the client directly rather than by reading its log.
The backend refused to initialize any EGLDisplay but the first - "already bound to a different
EGLDisplay" - and the field it compared against has no reader anywhere else in the tree: the domain
rule was the whole of the rule. What it cost was a compositor, which asks for the display it will
draw into AFTER probing another one. The probe succeeded, the real display was refused, and the
caller reported the refusal one layer up as "no usable DRM render device" - a message that names
where it stopped, not why.
The second: glvnd asks a vendor for entry points from two places. getProcAddress is how it builds
its own static EGL table, and getDispatchAddress is how it answers eglGetProcAddress for every name
it does not know itself - which is every extension entry point, the device queries among them. This
side answered the second from a GL-only table, so an application asking for eglQueryDeviceStringEXT
got a null for a function the vendor implements.
On a system with libglvnd the libEGL.so.1 an application links is a dispatcher, not an
implementation, and the supported way to be the implementation it talks to is to be a vendor
library it loads and routes to. On this machine it is also the ONLY way that reaches a caller which
does not link EGL directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its
entry points at startup, so a library injected with LD_PRELOAD is invisible to it however loudly it
is named - measured, with the client in kwin's maps and not one call into it.
So this is the vendor half of that ABI: __egl_Main answers glvnd with the entry points by name, the
platforms this implementation serves, and the device it is associated with - the render node of the
machine, because that is what lets a compositor match its own node to the display it is about to
draw into, and because glvnd drops a vendor that does not answer at all.
Two details are the difference between a vendor that is loaded and a vendor that is used. glvnd
requires setDispatchIndex to exist even when a vendor has nothing to record, which this one has -
its entry points are the implementation, they never re-enter glvnd. And a vendor must hand over ITS
OWN entry points: with default visibility a reference inside this library resolves against the whole
process, where another libEGL that loaded first wins. That is what -Bsymbolic-functions is for;
declaring the entry points protected says the same thing but does not compile against the platform
headers, which declare them default.
A vendor string and an eglQueryString answer have to agree, and they are read by two different
parties: the system's libglvnd reads the first to decide which vendor a platform request should be
routed to, and an application reads the second before it decides to make that request at all. Kept
apart they drift, and the drift has a signature - a library that is loaded, initialized, and never
called, while the caller reports that it found no provider for a function it never reached.
The list gains the device family for the same reason: a compositor asks which devices exist and
reads the DRM node off one before it asks for a display on it, so an implementation that answers
"no devices" is one it cannot use no matter how well the implementation draws.
Two MIT headers from libglvnd, verbatim, because MobileGL is about to implement the vendor half of
glvnd's EGL ABI and the distribution ships the runtime without them.