Neither showed up in the Linux build that verified this library, and both are the kind that would
have been found by the first person to build the phone half - so they are fixed here rather than
left for that person.
bionic only declares memfd_create from API 30 and this library builds against 26. The kernel call is
far older than the declaration, so it is asked for by number: the same call with the name the
headers of this API level do not carry yet.
The library carries the tree's logger, which reaches Android's own log through liblog. The main
target gets that library in its own Android block; a second target built from the same sources needs
it for the same reason, and without it the link is what fails rather than anything in the code.
The channel existed and had no callers. Now the frame the server takes from the host is offered to
the container with the geometry the host decided, the apply loop consumes the releases the
compositor sends back, and the teardown that drops the host frame closes the channel.
Every call is on the optional path: with no container the render path is what it was.
The container's GBM hands out buffers that are the display host's frames, and this is the phone end
of that: the server takes a frame from the host, offers its descriptor to the container over a unix
socket at a path both sides can reach, and takes the release back when the compositor is done with
it.
The channel is optional and silent by design. A compositor must render whether or not a container is
listening, and the render path must never wait on a socket - so it is opened lazily, every send is
bounded, and any error simply marks it down.
The self-allocated backend makes the library stand on its own; this one makes it the thing the
project needs. The display host on the phone owns a pool of frames, the render server draws into
them with Espryt and acks, and the host presents. A gbm_bo created here is one of those frames: the
phone sends its dma-buf fd over a unix socket, the offer carries the geometry the host decided (not
the geometry the caller asked for), and destroying the bo sends the frame back.
Verified against a fake display host on this machine, with no phone involved: the buffer the fake
host fills is the buffer the bo maps - every pixel of the pattern is there - the fd the caller is
handed is a view of that same memory rather than a copy, a device with no frame waiting falls back
to allocating locally, and teardown closes exactly the descriptor it opened. The self-allocated
path still passes its own test unchanged.
The channel is lazy and never blocking: a compositor must work whether or not the phone is there.
kwin never creates an EGL window surface. Its undefined libgbm symbols are seventeen entry points
whose whole purpose is to hand out gbm_bo fds, and those fds are what Anland's display producer
sends to the daemon on the phone - so the question "whose pixels are on the screen" is answered by
"who allocated the bo", and today that is Mesa. The client can be loaded, initialized and rendering
through glvnd with no window surface ever created, which is exactly what was measured.
This is a complete GBM: every public entry point of the vendored Mesa header, with the semantics a
compositor relies on - fds the caller owns, real mmap with refcounting, a swapchain that refuses to
hand out a busy buffer, import by fd, and teardown that leaves no descriptor or mapping behind. The
buffers come from the display host's frames; where there is no host, the device allocates from the
dma-buf heap, falling back to an uncached heap and then to memfd, so the library stands on its own.
The vendored gbm.h is Mesa's, verbatim: the entry points are the ABI, not our idea of it.
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.
Two permissions decide whether a server on Android lives or dies, and both are invisible until
something goes wrong: without POST_NOTIFICATIONS (13+) a foreground service cannot show the
notification that is its reason to exist, and without the battery exemption a dozing phone freezes
the process - the listener stays in /proc/net and stops accepting, which from the other side of the
socket looks exactly like a network fault.
The control screen now shows the state of both and asks for whichever is missing, in the order that
makes sense: the notification prompt is a dialog of ours, the battery one leaves the app entirely,
and coming back from it re-runs the other branch through onResume. A denial is said out loud rather
than left to be discovered.
The manifest carries the two permissions; the service's own notification was already there.
The last thing between the host-framed surface and a run: the sub-byte on the wire that names it.
ServerOwned had one (7) and the host frame had none, so the encoder refused the op with
UnknownWindowKind - a correct refusal, and the reason the first run of MOBILEGL_IPC_SURFACE=host
failed in the codec rather than anywhere near the pixels.
- protocol.fbs: WindowKind::HostFrame = 8, appended, legal on CreateWindowSurface only and always
with nativeToken 0 - the same rules ServerOwned carries, for the same reason: no pointer of any
process's window system crosses (Rule G/H).
- SurfaceRefusal: HostFrameOnSetWindowHandle = 5, its own code rather than reusing the
server-owned one: a refusal that names the wrong owner sends the next reader to the wrong place.
- SurfaceOpCodec: the encode and decode arms, the two wire errors that go with them
(HostFrameTokenNotZero latches as corruption, HostFrameOnSetWindowHandle is a named refusal), and
the range bound that had ServerOwned as its upper limit now ends at HostFrame.
- protocol_generated.h regenerated with the pinned submodule's flatc (scripts/gen_protocol.py),
diff being exactly the two appended enumerators.
Both ends must be rebuilt together, as any protocol revision here: the container's client and the
device's server went out with this on either side.
Three things this path needs that the server-owned one never did, each because the pixels are not
in an EGL surface this time:
- The client's DEFAULT framebuffer has to resolve to the frame. Four places bind the driver's 0
for the default framebuffer; they now ask g_hostFrameFramebufferId, which is 0 everywhere except
while a host-framed surface is current, where it is the frame's framebuffer. Without this every
draw would land in the pbuffer the context was made with - a surface nobody ever sees.
- eglMakeCurrent binds the surface's own default framebuffer behind the engine's binding shadow, so
the frame has to be put back after every make-current, with the shadow invalidated first: a bind
the shadow thinks already happened never reaches the driver.
- The bind itself goes through FramebufferImpl::BindFramebufferId, not a raw glBindFramebuffer, for
the same reason - a raw bind leaves the shadow claiming the previous framebuffer.
Frame lifecycle is otherwise unchanged: the frame becomes the draw target when it arrives, and is
dropped after the host has been answered and has presented it.
Compiles and links (libMobileGL.so, libMobileGLServer.so, exit 0), deployed to the device, and the
container's client is rebuilding against it. The run that shows Plasma through it is next.
The third on-screen shape, and the one this whole line was built toward: Anland keeps Wayland,
input and the display, and the render server draws into the frames the display host allocates.
client (container) asks for a window surface with no window of its own
server (Android) takes a frame from the host over the host frame bridge and binds it as
the draw target of that surface
host (the app that
owns the screen) offers each frame, presents it when the server says it is drawn
The pieces, in the order a frame travels:
- Config: MOBILEGL_IPC_SURFACE=host beside =server, and HostFrameWindowSurfaces() as the one
predicate every client-side consumer asks (EGLImpl accepts a NULL window for either on-screen
shape, and BackendObject_Remote routes the creation to the server instead of sending a handle).
- Wire: the frame-local tag kHostFrameWindowBackend, the mirror of kServerOwnedWindowBackend - it
names no WindowBackend of the pull build's enum, and the server consumes it before the unpack
that would refuse it.
- Server: ApplyHostFrameWindowSurface replaces nothing, unlike the server-owned arm: there is no
window of either side's to substitute, because the backend takes the frames from the bridge.
- Backend: SurfaceKind::HostFrame, InitHostFrameSurface (the EGL context comes from the pbuffer
path; what changes is what it draws INTO) and PresentHostFrame (glFinish, answer the host, take
the next frame) - and SwapEGLBuffers routes to it, because a host-framed surface has no
swapchain to swap: the host is what puts the frame on the glass.
HostFrameTarget and HostFrameBridge, committed just before this, are what the backend uses.
Compiles and links in this tree (libMobileGL.so and libMobileGLServer.so, exit 0). What is NOT
here yet, and is the next thing: the run that puts Plasma on the phone's screen through it.
Two modules, and nothing else changes yet: they are the half of the host-framed surface that is
about the frame itself, kept apart from the surface lifecycle so they can be reviewed and built
on their own.
HostFrameTarget (DirectGLES): an AHardwareBuffer becomes a renderbuffer through an EGLImage and a
framebuffer that is the drawing target while its frame is current. The extension strings decide
first and the four entry points are taken from eglGetProcAddress only after they have, which is
the shape the T0 AHB import in Managers.cpp already uses; a driver that lacks one of them is
answered with its name, never an abort. Every failure path drops what it made, so no GL name and
no EGLImage outlives a refusal.
HostFrameBridge (MG_Backend): the display host's own protocol, restated field for field - a
handle ahead of a fixed-size description on one AF_UNIX connection, then the answer, then the
host's readback of the pixels it can see. The host dials; this side listens on the well-known
name. A handle that arrives without its description is released before the failure is reported,
because a reference held past the connection starves the pool the host draws its frames from.
Why the host allocates the frames at all is measured, not chosen: AHardwareBuffer_createFromHandle
accepts an AHardwareBuffer this process allocated and refuses one the display queue handed over
(gralloc BAD_VALUE) - same process, same descriptor, same handle shape. That result is what these
two modules are built around, and it is written down where the next reader will look for it.
Both compile against this tree's own flags. Wiring them into the surface lifecycle, the swap path
and the client's opt-in is the next step, not this one.
Measured on the Redmi today: the Droidspaces container shares the phone's network namespace, so a
client in the container reaches the server's loopback listener with no adb forward and no LAN
address; the server owns the window (MOBILEGL_IPC_SURFACE=server), the Adreno does the drawing,
and a screenshot of the phone shows the colour that headless client asked for.
The two traps are written down because both cost a run: MOBILEGL_IPC_REQUIRE_SAME_BUILD=1 on the
server turns a Connect handshake's build-stamp warning into Refuse{BuildFingerprint} against a
release APK whose stamp is 0, and the client then dies on a caps gate that names something else.
The leg now reports OK on the device: offered=3 imported=3 drawn=3 hostReadback=3 agreed=3 --
three self-allocated AHardwareBuffers from the display host imported as EGLImages by this side,
drawn into, and read back by the host at both the first pixel and the far corner.
Getting there took one measurement and one correction. The handle message is 256 bytes on this
path and recvHandleFromUnixSocket drains it exactly (pending 256 -> 100, then 0 -> 100, then
0 -> 0), so the description that follows it was never the problem; the comparison was. This leg
was checking the display host's messages against kT4Magic -- the dma-buf protocol's magic, which
belongs to the container peer -- while the host stamps AHB_BRIDGE_MAGIC on all three of its
messages. A perfect round trip was reported as 'the offer's sideband did not follow its handle',
which named the wrong cause and cost the time. kT4HostMagic is that value, and the leg now also
prints the handle's footprint and the bytes it actually read, so the next failure of this shape
cannot be mistaken for the other one.