Compare commits

...
Author SHA1 Message Date
BZLZHH 7fd7f71d6c [Feature] (EGL): Wayland window surfaces, presented through wl_shm
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.
2026-10-01 10:59:07 -04:00
BZLZHH 353daa265e [Fix] (Pipe): the records two families keep of their own are forgotten with the session too
MGPipeForgetAllPublications cleared the publication latch, but two death helpers do not ask the
latch: vertex elements ask the vertex-input emitter's RecordIsPublished and buffers ask the
resource tracker's WasPublished.  A VAO and its buffers destroyed with the GL context after the
split roles were down still found themselves published there, and glmark2's eglTerminate stopped
on the same assertion eglinfo's did (ResolveVertexElements, then ResolveResourceIn for a buffer).

Both records are now dropped beside the latch: the emitter's latches whole, the tracker's
Published flags.
2026-10-01 10:59:06 -04:00
BZLZHH 69728a4f9b [Fix] (Remote): the create window starts empty with each session
The window of creates whose answers are read later is process-global, and every entry names a
sequence number of the session that wrote it.  A process can run several sessions one after
another - eglinfo terminates one platform display and initializes the next - and an entry left by
the previous session made the new session's first create wait, in DrainCreateWindow, for a
sequence number the new server would not reach: the second session hung on its first texture
create.

InstallClientWireTables, which runs at the start of every session, now empties the window and the
create suspects; their answers and their objects died with the session that wrote them.
2026-10-01 10:31:20 -04:00
BZLZHH c6f828532a [Fix] (Pipe): nothing is published once the split session is gone
MobileGL::Destroy shuts the split roles down first and resets the GL context after, by design.  The
frontend objects that die in that reset still found their handles published, so their deletes were
routed to whichever applier was installed by then - the monolith one, which never had those
resources and asserted ("ResourceDestroy named {slot, gen}") from eglTerminate.  The abort also
threw away the program's buffered output, so eglinfo printed nothing but the assertion.

Every twin the server held died with the session, so ShutdownSplitRoles now clears the publication
latch (MGPipeForgetAllPublications): the deletes that follow are local bookkeeping only.
2026-10-01 10:31:20 -04:00
BZLZHH 5bb28140cb [Feature] (Remote): the TCP supervisor serves sessions side by side
A desktop is a compositor AND the GL programs run inside it, each a client with a session of its
own.  The TCP supervisor answered every authenticated Hello after the first with Refuse{Busy}, so
with kwin connected any other program - eglinfo, glxinfo, glmark2 from konsole - waited out its
Welcome and died on Fatal{CapsBeforeFirstSnapshot}.

The fork shape now keeps a list of live children and forks for a Hello while fewer than eight are
live.  What made it a single slot was the data connection: a DataBind names its session only by
the data nonce, which the child minted, so the supervisor could hand it to nothing but "the" child.
The nonce is now minted by the supervisor just before fork(); the child adopts it
(PresetDataNonceForThisProcess) instead of minting its own, and a DataBind goes to the child whose
nonce it presents.  Nothing on the wire changes.  The thread shape (the in-process display server)
still serves one session, Busy for a second as before.
2026-10-01 10:31:20 -04:00
BZLZHH bf7fc6bf6b [Fix] (DirectGLES): an imported external texture is attached as the host frame canvas
The arms that bind the display host's frame in place of an imported texture were gated on the
attach target being 0x8D65, but an external texture is attached through the GL_TEXTURE_2D upload
target, so they never ran.  The texture's twin on the server has no storage - the dma-buf behind it
never crosses - and kwin's framebuffers over it were incomplete (status 0x8cd7, every draw
GL_INVALID_FRAMEBUFFER_OPERATION), so nothing it drew landed anywhere.

The record arm (SyncAttachmentSurface) now recognises a texture point whose target is
TextureExternalOES and attaches the host frame canvas before any twin adoption or storage sync,
which have nothing to offer a texture with no storage; it takes the host frame first if none is held
yet.  The legacy object arm keys on the texture object's own target the same way.  Measured: all
four of kwin's daemon-buffer framebuffers complete, and the desktop it draws reaches the glass.
2026-10-01 09:47:32 -04:00
BZLZHH b72d4c5dff [Feature] (DirectGLES): a stable canvas in front of the host frames
Every present hands the frame back to the display host and takes a different one, and the frame's
renderbuffer is deleted with it.  A compositor attaches its framebuffers once (kwin: one per daemon
buffer, for the life of the process), so attachments bound to a frame's own renderbuffer named a
deleted object from the second frame on.

The backend now keeps a canvas - a renderbuffer and framebuffer of its own, made at the first
frame's size and kept for the session.  The host-frame globals (the default-framebuffer redirect,
the storage external attachments are bound to) name the canvas instead of the frame, and keep
naming it between a frame being handed back and the next one arriving.  PresentHostFrame copies the
canvas into the frame before its fence; the copy puts back the compositor's framebuffer bindings,
scissor test and rasterizer discard exactly as they were.  Without a canvas the frame itself stays
the draw target, as before.
2026-10-01 09:47:24 -04:00
BZLZHH c88c6ff196 [Fix] (Remote): the present record presents a host-framed surface
A split client's swap reaches the server as a present record, and ServerVerbSink::OnPresent only
ran the backend's Present - an eglSwapBuffers of the pbuffer the context was made with.
BackendObject::SwapEGLBuffers is the only route to PresentHostFrame and a split client never takes
it, so over TCP the display host was never told a frame was drawn: the app offered thousands of
frames and presented none.

BackendObject::PresentHostFrameIfCurrent presents the host frame when the current surface is
host-framed and is a no-op otherwise; OnPresent calls it after Present and before the credit goes
back, so a client released by the credit is released by a frame that is on its way to the glass.
2026-10-01 09:47:18 -04:00
BZLZHH 16488d5801 [Feature] (GL): glFlush presents the display host's frame for a compositor that never swaps
kwin draws the desktop into the display host's frame and then calls glFlush - and glFlush alone;
it never calls eglSwapBuffers.  The stream reached the server and the server drew, but the display
host was never told a frame was done, so nothing reached the glass.  When the surfaceless-bind hook
has taken the host frame, glFlush now swaps that surface after flushing the stream: that swap is the
host's "frame is done".

A swap of a surface the EGL state does not know used to return EGL_BAD_SURFACE without a word - the
whole reason the host frame's presents were invisible - so that refusal is now reported once.
2026-10-01 09:47:12 -04:00
BZLZHH c16a51da62 [Feature] (EGL): a surfaceless compositor takes the display host's frame surface
A compositor on the anland route never creates a window surface: it imports the display host's
dma-bufs, draws into them through framebuffers and binds its contexts with EGL_NO_SURFACE.  The
host frame bridge is only opened by a host-framed surface being created and activated, so with no
surface the bridge stayed shut and the display host offered its frames into a name nobody served.

So the first surfaceless bind of a real context, in a session that asked for host-framed surfaces,
makes the surface eglCreateWindowSurface would have made - registered in the EGL state AND created
through the backend's window-surface entry, which the remote backend routes to the host frame - and
every later surfaceless bind binds it, because the compositor rebinds with EGL_NO_SURFACE all the
time and a bind without it leaves nothing current to present.  Registering it in the EGL state is
not optional: a backend-only token was refused by the state (EGL_BAD_SURFACE) on every bind and
every swap.

Only the compositor takes the host frame.  Desktops are full of processes that bind with no surface
(konsole, plasmashell, kactivitymanagerd), and a second process taking it made the activation fail;
the compositor's launcher marks it with MOBILEGL_HOST_FRAME_CLIENT=1.
2026-10-01 09:47:06 -04:00
BZLZHH 08e45dc9c6 [Fix] (GLState): an external texture is a 2D texture that remembers it is external
GL_TEXTURE_EXTERNAL_OES had a target of its own but nothing behind it: the texture factory had no
row for it (the compositor died on "Unimplemented texture type" the moment it bound one), the
attach path had no upload target for 0x8D65 (validation failed, the attachment was never recorded
and the compositor read GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT), and nothing downstream
could tell an imported texture from an ordinary one.

An external texture is now a TextureObject2D created with the TextureExternalOES target, attached
through the 2D upload target - which the three framebuffer-texture validations accept for an
external object - and the target crosses the pipe (TexExternalOES maps back on the server) so the
render server can recognise it.  To the driver it stays a 2D texture: MapToBackendTextureTarget
folds it to Texture2D like the other targets ES does not have.
2026-10-01 09:46:57 -04:00
BZLZHH b2ce268c86 [Fix] (GLState): a global buffer target asked through the indexed lookup is a warning, not an abort
Only four buffer targets have per-index binding points (Uniform, TransformFeedback, AtomicCounter,
ShaderStorage).  A caller that asks GetBindingPoint for any other target is reading the wrong
accessor, not passing a bad enum - and kwin reached exactly that right after its vertex-attrib
setup, where the assertion killed the whole compositor with Trace/breakpoint trap.

The answer is now a one-time warning and index 0 of the indexed array: the caller gets a valid
slot, the global binding is untouched, and the mismatch stays visible in the log.
2026-10-01 09:46:46 -04:00
BZLZHH df58f21d83 [Feature] (EGL): the vendor answers what a compositor resolves, and the imported texture is complete
A compositor's bring-up is a fixed sequence of questions that all have to be answered by the same
library: which devices exist, which display a device serves, what its DRM node is, which platform
the display comes from, which configs exist, a context, a surfaceless make-current, the GL entry
points for that context, and finally the dma-buf import it draws with.  Every missing answer in that
list cost a run to find, and each is measured rather than assumed - an unanswered device query was a
call through a null pointer, an unanswered GL name left glvnd's dispatch table empty so glGetString
answered NULL through libGL while the same context answered through eglGetProcAddress, an unanswered
node string produced "couldn't find dev node for drm device", and a rejected
GL_TEXTURE_EXTERNAL_OES binding is why the framebuffer over an imported dma-buf was reported
incomplete.

The EGL side is whole now: the device-query family is published as real entry points with the DRM
node named, the dispatch hook answers GL names as well as EGL ones, and the swap-and-sync family the
compositor checks between its context and its first surface - EGL_KHR_fence_sync, EGL_KHR_wait_sync,
the damage variants, EGL_EXT_buffer_age - is implemented and announced.  A surfaceless context is
recorded as what it is rather than as a surface that does not exist.  These entry points log what
they are asked at DEBUG level, because the next bring-up should not need a disassembler.

GL_TEXTURE_EXTERNAL_OES is a target of its own (its TextureTarget, its pipe row, the GLenum mapping,
and the OES_EGL_image extension named), which is what lets the compositor's bind succeed at all; the
texture an imported image backs is marked backed, so a framebuffer built over it is complete.  The
client half of the import is new beside it: each plane's descriptor is described and shared with the
render server over the session transport, which is the change the server side has been waiting for.
2026-10-01 18:15:55 +08:00
gold 29a89cc10a [Fix] (gbm): the two defects only the Android build could find
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.
2026-10-01 08:23:04 +08:00
gold 8e3db2cb87 [Feature] (gbm): the server offers each taken frame to the container
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.
2026-10-01 03:42:18 +08:00
gold 1ba7b1a96f [Feature] (gbm): the server sends each frame's dma-buf fd to the container
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.
2026-10-01 03:37:50 +08:00
gold 1a7b4b6a2b [Feature] (gbm): the host-frame backend - the compositor's buffers are the phone's frames
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.
2026-10-01 03:27:22 +08:00
gold 55e5165e2f [Feature] (gbm): MobileGL's own GBM, so the compositor's buffers are the display host's frames
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.
2026-10-01 03:16:55 +08:00
gold 2860d09051 [Fix] (EGL): a second display is served, and glvnd gets its answers from one table
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.
2026-10-01 02:58:06 +08:00
gold c9206dd955 [Feature] (EGL): MobileGL serves EGL through libglvnd
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.
2026-10-01 02:58:06 +08:00
gold dfbda7ccb4 [Feature] (EGL): the platform and device extensions are one list, spelled once
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.
2026-10-01 02:58:06 +08:00
gold 6a702afa72 [Chore] (glvnd): vendor the EGL vendor ABI headers
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.
2026-10-01 02:58:06 +08:00
gold ab685fb0e9 [Feature] (plugin): the server survives a dozing phone - a notification the user allows and a battery exemption the user grants
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.
2026-10-01 01:50:14 +08:00
gold 1ae7a8eaa4 [Feature] (disaggregated): WindowKind::HostFrame - the host-framed surface crosses the wire
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.
2026-10-01 01:26:02 +08:00
gold ead7a10b68 [Feature] (disaggregated): the default framebuffer on a host-framed surface is the host's frame
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.
2026-10-01 01:14:51 +08:00
gold 035f1b07ae [Feature] (disaggregated): MOBILEGL_IPC_SURFACE=host - the display host owns the screen and the frames
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.
2026-10-01 01:11:21 +08:00
gold c7ab618ab0 [Feature] (disaggregated): the host's frames as a draw target, and the socket they arrive on
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.
2026-10-01 00:57:14 +08:00
BZLZHH 2e43df6125 [Docs] (disaggregated): the container as a client of the phone's on-screen server, and the two things that cost a run
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.
2026-10-01 00:39:49 +08:00
BZLZHH 015b91f08d [Test] (extmem_probe): T4-host-image closes the round trip, and its first verdict was wrong for one constant
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.
2026-10-01 00:34:40 +08:00
BZLZHH 987b7ad13c [Test] (extmem_probe): T4-host-image is the other end - the buffer the display host is about to scan out, imported and drawn into, with the round trip closed by that host reading its own pixels back. 2026-09-30 23:53:49 +08:00
BZLZHH d26cf1e31b [Test] (extmem_probe): T4 asks the wrap question too - a driver without EGL_EXT_image_dma_buf_import is tried through AHardwareBuffer_createFromHandle before the offer is called untakeable, and both handle types are recorded so the answer names which one the platform reads. 2026-09-30 23:26:26 +08:00
BZLZHH 8196818e9f [Test] (extmem_probe): the T4 leg asks whether the render server can draw into an image allocated outside Android - and the device answers twice: this driver has no dma-buf import, Android-side allocation works end to end, and the offer must carry the plane layout rather than one linear plane. 2026-09-30 23:00:55 +08:00
81 changed files with 13066 additions and 121 deletions
+538
View File
@@ -0,0 +1,538 @@
/*
* Copyright © 2011 Intel Corporation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice (including the next
* paragraph) shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* Authors:
* Benjamin Franzke <benjaminfranzke@googlemail.com>
*/
#ifndef _GBM_H_
#define _GBM_H_
#define __GBM__ 1
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* \file gbm.h
* \brief Generic Buffer Manager
*/
struct gbm_device;
struct gbm_bo;
struct gbm_surface;
/**
* \mainpage The Generic Buffer Manager
*
* This module provides an abstraction that the caller can use to request a
* buffer from the underlying memory management system for the platform.
*
* This allows the creation of portable code whilst still allowing access to
* the underlying memory manager.
*/
/**
* Abstraction representing the handle to a buffer allocated by the
* manager
*/
union gbm_bo_handle {
void *ptr;
int32_t s32;
uint32_t u32;
int64_t s64;
uint64_t u64;
};
/** Format of the allocated buffer */
enum gbm_bo_format {
/** RGB with 8 bits per channel in a 32 bit value */
GBM_BO_FORMAT_XRGB8888,
/** ARGB with 8 bits per channel in a 32 bit value */
GBM_BO_FORMAT_ARGB8888
};
/**
* The FourCC format codes are taken from the drm_fourcc.h definition, and
* re-namespaced. New GBM formats must not be added, unless they are
* identical ports from drm_fourcc.
*/
#define __gbm_fourcc_code(a,b,c,d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \
((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
#define GBM_FORMAT_BIG_ENDIAN (1<<31) /* format is big endian instead of little endian */
/* color index */
#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
/* 8 bpp Red */
#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
/* 16 bpp Red */
#define GBM_FORMAT_R16 __gbm_fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */
/* 16 bpp RG */
#define GBM_FORMAT_GR88 __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
/* 32 bpp RG */
#define GBM_FORMAT_RG1616 __gbm_fourcc_code('R', 'G', '3', '2') /* [31:0] R:G 16:16 little endian */
#define GBM_FORMAT_GR1616 __gbm_fourcc_code('G', 'R', '3', '2') /* [31:0] G:R 16:16 little endian */
/* 8 bpp RGB */
#define GBM_FORMAT_RGB332 __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */
#define GBM_FORMAT_BGR233 __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */
/* 16 bpp RGB */
#define GBM_FORMAT_XRGB4444 __gbm_fourcc_code('X', 'R', '1', '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */
#define GBM_FORMAT_XBGR4444 __gbm_fourcc_code('X', 'B', '1', '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */
#define GBM_FORMAT_RGBX4444 __gbm_fourcc_code('R', 'X', '1', '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */
#define GBM_FORMAT_BGRX4444 __gbm_fourcc_code('B', 'X', '1', '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */
#define GBM_FORMAT_ARGB4444 __gbm_fourcc_code('A', 'R', '1', '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */
#define GBM_FORMAT_ABGR4444 __gbm_fourcc_code('A', 'B', '1', '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */
#define GBM_FORMAT_RGBA4444 __gbm_fourcc_code('R', 'A', '1', '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */
#define GBM_FORMAT_BGRA4444 __gbm_fourcc_code('B', 'A', '1', '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */
#define GBM_FORMAT_XRGB1555 __gbm_fourcc_code('X', 'R', '1', '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */
#define GBM_FORMAT_XBGR1555 __gbm_fourcc_code('X', 'B', '1', '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */
#define GBM_FORMAT_RGBX5551 __gbm_fourcc_code('R', 'X', '1', '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */
#define GBM_FORMAT_BGRX5551 __gbm_fourcc_code('B', 'X', '1', '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */
#define GBM_FORMAT_ARGB1555 __gbm_fourcc_code('A', 'R', '1', '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */
#define GBM_FORMAT_ABGR1555 __gbm_fourcc_code('A', 'B', '1', '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */
#define GBM_FORMAT_RGBA5551 __gbm_fourcc_code('R', 'A', '1', '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */
#define GBM_FORMAT_BGRA5551 __gbm_fourcc_code('B', 'A', '1', '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */
#define GBM_FORMAT_RGB565 __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */
#define GBM_FORMAT_BGR565 __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */
/* 24 bpp RGB */
#define GBM_FORMAT_RGB888 __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */
#define GBM_FORMAT_BGR888 __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */
/* 32 bpp RGB */
#define GBM_FORMAT_XRGB8888 __gbm_fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
#define GBM_FORMAT_XBGR8888 __gbm_fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
#define GBM_FORMAT_RGBX8888 __gbm_fourcc_code('R', 'X', '2', '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */
#define GBM_FORMAT_BGRX8888 __gbm_fourcc_code('B', 'X', '2', '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */
#define GBM_FORMAT_ARGB8888 __gbm_fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
#define GBM_FORMAT_ABGR8888 __gbm_fourcc_code('A', 'B', '2', '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */
#define GBM_FORMAT_RGBA8888 __gbm_fourcc_code('R', 'A', '2', '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */
#define GBM_FORMAT_BGRA8888 __gbm_fourcc_code('B', 'A', '2', '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */
#define GBM_FORMAT_XRGB2101010 __gbm_fourcc_code('X', 'R', '3', '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */
#define GBM_FORMAT_XBGR2101010 __gbm_fourcc_code('X', 'B', '3', '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */
#define GBM_FORMAT_RGBX1010102 __gbm_fourcc_code('R', 'X', '3', '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */
#define GBM_FORMAT_BGRX1010102 __gbm_fourcc_code('B', 'X', '3', '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */
#define GBM_FORMAT_ARGB2101010 __gbm_fourcc_code('A', 'R', '3', '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */
#define GBM_FORMAT_ABGR2101010 __gbm_fourcc_code('A', 'B', '3', '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */
#define GBM_FORMAT_RGBA1010102 __gbm_fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
#define GBM_FORMAT_BGRA1010102 __gbm_fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
/* 64 bpp RGB */
#define GBM_FORMAT_XBGR16161616 __gbm_fourcc_code('X', 'B', '4', '8') /* [63:0] x:B:G:R 16:16:16:16 little endian */
#define GBM_FORMAT_ABGR16161616 __gbm_fourcc_code('A', 'B', '4', '8') /* [63:0] A:B:G:R 16:16:16:16 little endian */
/*
* Floating point 64bpp RGB
* IEEE 754-2008 binary16 half-precision float
* [15:0] sign:exponent:mantissa 1:5:10
*/
#define GBM_FORMAT_XBGR16161616F __gbm_fourcc_code('X', 'B', '4', 'H') /* [63:0] x:B:G:R 16:16:16:16 little endian */
#define GBM_FORMAT_ABGR16161616F __gbm_fourcc_code('A', 'B', '4', 'H') /* [63:0] A:B:G:R 16:16:16:16 little endian */
/* packed YCbCr */
#define GBM_FORMAT_YUYV __gbm_fourcc_code('Y', 'U', 'Y', 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */
#define GBM_FORMAT_YVYU __gbm_fourcc_code('Y', 'V', 'Y', 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */
#define GBM_FORMAT_UYVY __gbm_fourcc_code('U', 'Y', 'V', 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */
#define GBM_FORMAT_VYUY __gbm_fourcc_code('V', 'Y', 'U', 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */
#define GBM_FORMAT_AYUV __gbm_fourcc_code('A', 'Y', 'U', 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */
/*
* 2 plane YCbCr
* index 0 = Y plane, [7:0] Y
* index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian
* or
* index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian
*/
#define GBM_FORMAT_NV12 __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */
#define GBM_FORMAT_NV21 __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */
#define GBM_FORMAT_NV16 __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */
#define GBM_FORMAT_NV61 __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */
/*
* 3 plane YCbCr
* index 0: Y plane, [7:0] Y
* index 1: Cb plane, [7:0] Cb
* index 2: Cr plane, [7:0] Cr
* or
* index 1: Cr plane, [7:0] Cr
* index 2: Cb plane, [7:0] Cb
*/
#define GBM_FORMAT_YUV410 __gbm_fourcc_code('Y', 'U', 'V', '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU410 __gbm_fourcc_code('Y', 'V', 'U', '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV411 __gbm_fourcc_code('Y', 'U', '1', '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU411 __gbm_fourcc_code('Y', 'V', '1', '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV420 __gbm_fourcc_code('Y', 'U', '1', '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU420 __gbm_fourcc_code('Y', 'V', '1', '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV422 __gbm_fourcc_code('Y', 'U', '1', '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU422 __gbm_fourcc_code('Y', 'V', '1', '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */
#define GBM_FORMAT_YUV444 __gbm_fourcc_code('Y', 'U', '2', '4') /* non-subsampled Cb (1) and Cr (2) planes */
#define GBM_FORMAT_YVU444 __gbm_fourcc_code('Y', 'V', '2', '4') /* non-subsampled Cr (1) and Cb (2) planes */
struct gbm_format_name_desc {
char name[5];
};
/**
* Flags to indicate the intended use for the buffer - these are passed into
* gbm_bo_create(). The caller must set the union of all the flags that are
* appropriate
*
* \sa Use gbm_device_is_format_supported() to check if the combination of format
* and use flags are supported
*/
enum gbm_bo_flags {
/**
* Buffer is going to be presented to the screen using an API such as KMS
*/
GBM_BO_USE_SCANOUT = (1 << 0),
/**
* Buffer is going to be used as cursor
*/
GBM_BO_USE_CURSOR = (1 << 1),
/**
* Deprecated
*/
GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
/**
* Buffer is to be used for rendering - for example it is going to be used
* as the storage for a color buffer
*/
GBM_BO_USE_RENDERING = (1 << 2),
/**
* Buffer can be used for gbm_bo_write. This is guaranteed to work
* with GBM_BO_USE_CURSOR, but may not work for other combinations.
*/
GBM_BO_USE_WRITE = (1 << 3),
/**
* Buffer is linear, i.e. not tiled.
*/
GBM_BO_USE_LINEAR = (1 << 4),
/**
* Buffer is protected, i.e. encrypted and not readable by CPU or any
* other non-secure / non-trusted components nor by non-trusted OpenGL,
* OpenCL, and Vulkan applications.
*/
GBM_BO_USE_PROTECTED = (1 << 5),
/**
* The buffer will be used for front buffer rendering. On some
* platforms this may (for example) disable framebuffer compression
* to avoid problems with compression flags data being out of sync
* with pixel data.
*/
GBM_BO_USE_FRONT_RENDERING = (1 << 6),
/**
* Allow the driver to select fixed-rate compression parameters.
*/
GBM_BO_FIXED_COMPRESSION_DEFAULT = (1 << 7),
/**
* Fixed-rate compression: at least 1bpc, less than 2bpc
*/
GBM_BO_FIXED_COMPRESSION_1BPC = (2 << 7),
/**
* Fixed-rate compression: at least 2bpc, less than 3bpc
*/
GBM_BO_FIXED_COMPRESSION_2BPC = (3 << 7),
/**
* Fixed-rate compression: at least 3bpc, less than 4bpc
*/
GBM_BO_FIXED_COMPRESSION_3BPC = (4 << 7),
/**
* Fixed-rate compression: at least 4bpc, less than 5bpc
*/
GBM_BO_FIXED_COMPRESSION_4BPC = (5 << 7),
/**
* Fixed-rate compression: at least 5bpc, less than 6bpc
*/
GBM_BO_FIXED_COMPRESSION_5BPC = (6 << 7),
/**
* Fixed-rate compression: at least 6bpc, less than 7bpc
*/
GBM_BO_FIXED_COMPRESSION_6BPC = (7 << 7),
/**
* Fixed-rate compression: at least 7bpc, less than 8bpc
*/
GBM_BO_FIXED_COMPRESSION_7BPC = (8 << 7),
/**
* Fixed-rate compression: at least 8bpc, less than 9bpc
*/
GBM_BO_FIXED_COMPRESSION_8BPC = (9 << 7),
/**
* Fixed-rate compression: at least 9bpc, less than 10bpc
*/
GBM_BO_FIXED_COMPRESSION_9BPC = (10 << 7),
/**
* Fixed-rate compression: at least 10bpc, less than 11bpc
*/
GBM_BO_FIXED_COMPRESSION_10BPC = (11 << 7),
/**
* Fixed-rate compression: at least 11bpc, less than 12bpc
*/
GBM_BO_FIXED_COMPRESSION_11BPC = (12 << 7),
/**
* Fixed-rate compression: at least 12bpc, no maximum rate
*/
GBM_BO_FIXED_COMPRESSION_12BPC = (13 << 7),
/* next available value is (1 << 11) */
};
#define GBM_BO_FIXED_COMPRESSION_MASK (((1 << 11) - 1) & ~((1 << 7) - 1))
int
gbm_device_get_fd(struct gbm_device *gbm);
const char *
gbm_device_get_backend_name(struct gbm_device *gbm);
int
gbm_device_is_format_supported(struct gbm_device *gbm,
uint32_t format, uint32_t flags);
int
gbm_device_get_format_modifier_plane_count(struct gbm_device *gbm,
uint32_t format,
uint64_t modifier);
void
gbm_device_destroy(struct gbm_device *gbm);
struct gbm_device *
gbm_create_device(int fd);
struct gbm_bo *
gbm_bo_create(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format, uint32_t flags);
struct gbm_bo *
gbm_bo_create_with_modifiers(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count);
struct gbm_bo *
gbm_bo_create_with_modifiers2(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count,
uint32_t flags);
#define GBM_BO_IMPORT_WL_BUFFER 0x5501
#define GBM_BO_IMPORT_EGL_IMAGE 0x5502
#define GBM_BO_IMPORT_FD 0x5503
#define GBM_BO_IMPORT_FD_MODIFIER 0x5504
struct gbm_import_fd_data {
int fd;
uint32_t width;
uint32_t height;
uint32_t stride;
uint32_t format;
};
#define GBM_MAX_PLANES 4
struct gbm_import_fd_modifier_data {
uint32_t width;
uint32_t height;
uint32_t format;
uint32_t num_fds;
int fds[GBM_MAX_PLANES];
int strides[GBM_MAX_PLANES];
int offsets[GBM_MAX_PLANES];
uint64_t modifier;
};
struct gbm_bo *
gbm_bo_import(struct gbm_device *gbm, uint32_t type,
void *buffer, uint32_t flags);
/**
* Flags to indicate the type of mapping for the buffer - these are
* passed into gbm_bo_map(). The caller must set the union of all the
* flags that are appropriate.
*
* These flags are independent of the GBM_BO_USE_* creation flags. However,
* mapping the buffer may require copying to/from a staging buffer.
*
* See also: pipe_map_flags
*/
enum gbm_bo_transfer_flags {
/**
* Buffer contents read back (or accessed directly) at transfer
* create time.
*/
GBM_BO_TRANSFER_READ = (1 << 0),
/**
* Buffer contents will be written back at unmap time
* (or modified as a result of being accessed directly).
*/
GBM_BO_TRANSFER_WRITE = (1 << 1),
/**
* Read/modify/write
*/
GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
};
void *
gbm_bo_map(struct gbm_bo *bo,
uint32_t x, uint32_t y, uint32_t width, uint32_t height,
uint32_t flags, uint32_t *stride, void **map_data);
void
gbm_bo_unmap(struct gbm_bo *bo, void *map_data);
uint32_t
gbm_bo_get_width(struct gbm_bo *bo);
uint32_t
gbm_bo_get_height(struct gbm_bo *bo);
uint32_t
gbm_bo_get_stride(struct gbm_bo *bo);
uint32_t
gbm_bo_get_stride_for_plane(struct gbm_bo *bo, int plane);
uint32_t
gbm_bo_get_format(struct gbm_bo *bo);
uint32_t
gbm_bo_get_bpp(struct gbm_bo *bo);
uint32_t
gbm_bo_get_offset(struct gbm_bo *bo, int plane);
struct gbm_device *
gbm_bo_get_device(struct gbm_bo *bo);
union gbm_bo_handle
gbm_bo_get_handle(struct gbm_bo *bo);
int
gbm_bo_get_fd(struct gbm_bo *bo);
uint64_t
gbm_bo_get_modifier(struct gbm_bo *bo);
int
gbm_bo_get_plane_count(struct gbm_bo *bo);
union gbm_bo_handle
gbm_bo_get_handle_for_plane(struct gbm_bo *bo, int plane);
int
gbm_bo_get_fd_for_plane(struct gbm_bo *bo, int plane);
int
gbm_bo_write(struct gbm_bo *bo, const void *buf, size_t count);
void
gbm_bo_set_user_data(struct gbm_bo *bo, void *data,
void (*destroy_user_data)(struct gbm_bo *, void *));
void *
gbm_bo_get_user_data(struct gbm_bo *bo);
void
gbm_bo_destroy(struct gbm_bo *bo);
struct gbm_surface *
gbm_surface_create(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format, uint32_t flags);
struct gbm_surface *
gbm_surface_create_with_modifiers(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count);
struct gbm_surface *
gbm_surface_create_with_modifiers2(struct gbm_device *gbm,
uint32_t width, uint32_t height,
uint32_t format,
const uint64_t *modifiers,
const unsigned int count,
uint32_t flags);
struct gbm_bo *
gbm_surface_lock_front_buffer(struct gbm_surface *surface);
void
gbm_surface_release_buffer(struct gbm_surface *surface, struct gbm_bo *bo);
int
gbm_surface_has_free_buffers(struct gbm_surface *surface);
void
gbm_surface_destroy(struct gbm_surface *surface);
char *
gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc *desc);
#ifdef __cplusplus
}
#endif
#endif
+11
View File
@@ -0,0 +1,11 @@
# glvnd vendor ABI headers
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
headers intact.
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
smaller dependency than a build-time download of a source tree.
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
+141
View File
@@ -0,0 +1,141 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#include <GL/gl.h>
#if !defined(__GL_DISPATCH_ABI_H)
#define __GL_DISPATCH_ABI_H
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup gldispatchabi GL dispatching ABI
*
* This is not a complete ABI, but rather a fragment common to the libEGL and
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
* these client ABIs.
*/
/*!
* Thread-local implementation used by libglvnd. This is passed into the patch
* function callback via the type parameter.
*
* For most architectures, the vendor library can ignore this parameter, since
* it will always be the same value. It's used for systems like ARM, where the
* stubs might be use the ARM or Thumb instruction sets.
*
* The stub type does not make any distinction between TLS and TSD stubs. The
* entire purpose of entrypoint rewriting is to skip the dispatch table in
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
*/
enum {
/*!
* Indicates that the stubs aren't defined in assembly. For example, if the
* dispatch stubs are written in C. Vendor libraries generally won't see
* this value.
*/
__GLDISPATCH_STUB_UNKNOWN,
/*!
* Used for stubs on x86 systems.
*/
__GLDISPATCH_STUB_X86,
/*!
* Used for stubs on x86-64 systems.
*/
__GLDISPATCH_STUB_X86_64,
/*!
* Used for stubs on ARMv7, using the Thumb instruction set.
*/
__GLDISPATCH_STUB_ARMV7_THUMB,
/*!
* Used for stubs on ARMv7, using the normal ARM instruction set.
*/
__GLDISPATCH_STUB_ARMV7_ARM,
/*!
* Used for stubs on ARMv8/aarch64.
*/
__GLDISPATCH_STUB_AARCH64,
/*!
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
*/
__GLDISPATCH_STUB_X32,
/*!
* Used for stubs on PPC64 systems.
*/
__GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
*/
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on LOONGARCH/loongarch64.
*/
__GLDISPATCH_STUB_LOONGARCH64,
};
/*!
* A callback function called by the vendor library to fetch the address of an
* entrypoint.
*
* The function returns two pointers, one writable and one executable. The two
* pointers may or may not be the same virtual address, but they will both be
* mappings of the same physical memory.
*
* The vendor library should write its entrypoint to the address returned by
* \p writePtr, but should use the address from \p execPtr for things like
* calculating PC-relative offsets.
*
* Note that if this function fails, then the vendor library can still try to
* patch other entrypoints.
*
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
* be zero, even if the stub uses the thumb instruction set. The vendor library
* should use the \c type parameter of \c initiatePatch to determine which
* instruction set to use.
*
* \param funcName The function name.
* \param[out] writePtr The pointer that the vendor library can write to.
* \param[out] execPtr The pointer to the executable code.
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
*/
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
void **writePtr, const void **execPtr);
#if defined(__cplusplus)
}
#endif
#endif // __GL_DISPATCH_ABI_H
+464
View File
@@ -0,0 +1,464 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#if !defined(__LIB_EGL_ABI_H)
#define __LIB_EGL_ABI_H
#include <stdint.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include "glvnd/GLdispatchABI.h"
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup eglvendorabi EGL Vendor ABI
*
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
*
* Each vendor is associated with three distinct dispatch table types:
*
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
* provided by the vendor at load time during the initial handshake.
* - dynamic EGL dispatch table: this is a structure allocated by the API
* library at runtime used to manage EGL extension functions which are not
* present in the static table.
* - core GL dispatch table: this is a structure maintained by the API library
* which contains both GL core (static) and GL extension (dynamic) functions.
*
* @{
*/
/*
* Rendering API handling:
*
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
* other API is added, then the major version number will be incremented.
*
* When the application calls eglBindAPI, libEGL will forward the call to every
* vendor library. In addition, a vendor library can query the current API from
* libEGL using the getCurrentApi callback.
*
* Vendor libraries are not required to support both GL and GLES, but they must
* be able to deal with either one as the current rendering API. If a vendor
* doesn't support the current API, then it should return an error from
* eglCreateContext.
*/
/*!
* Current version of the ABI.
*
* This version number contains a major number in the high-order 16 bits, and
* a minor version number in the low-order 16 bits.
*
* The major version number is incremented when an interface change will break
* backwards compatibility with existing vendor libraries. The minor version
* number is incremented when there's a change but existing vendor libraries
* will still work.
*/
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
{
return version >> 16;
}
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
{
return version & 0xFFFF;
}
/*!
* This opaque structure stores function pointers for EGL extension functions.
* It is allocated at runtime by the API library. Vendor-provided dispatch
* functions retrieve and operate on this structure using the API below.
*/
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
/****************************************************************************
* API library exports *
****************************************************************************/
typedef struct __EGLapiExportsRec {
/************************************************************************
* The following routines are used by vendor-implemented EGL dispatch
* functions to lookup and call into the right vendor.
************************************************************************/
/*!
* This function must be called at the start of every EGL dispatch stub. It
* performs any necessary per-call bookkeeping.
*/
void (* threadInit) (void);
EGLenum (* getCurrentApi)(void);
/*!
* This function retrieves the appropriate current dynamic dispatch table,
* if a GL context is current. Otherwise, this returns NULL.
*/
__EGLvendorInfo *(*getCurrentVendor)(void);
/*!
* This retrieves the current context for this thread.
*/
EGLContext (*getCurrentContext)(void);
/*!
* Returns the current display for this thread.
*/
EGLDisplay (*getCurrentDisplay)(void);
/*!
* Returns the current drawable for this thread.
*/
EGLSurface (*getCurrentSurface)(EGLint readDraw);
/*!
* This function retrieves an entry point from the dynamic dispatch table
* given an index into the table.
*/
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
/*!
* Sets the last error for the current thread. The error will be returned
* the next time the app calls eglGetError().
*
* This function will override a call to \c setLastVendor, and vice-versa.
*/
void (* setEGLError) (EGLint errorCode);
/*!
* Notifies libEGL about the vendor library that an EGL function is
* dispatched to. This is used to look up the last EGL error code from a
* vendor.
*
* A vendor library may generate an EGL error asynchronously, if it
* offloads some EGL functions onto a worker thread. In that case, calling
* \c setEGLError to set the last error would not work.
*
* When an EGL dispatch stubs finds the vendor library, it must call this
* function before it dispatches the function call.
*
* This function will override a call to \c setEGLError, and vice-versa.
*
* \param vendor The vendor library of the current EGL function call.
* \return EGL_TRUE on success, or EGL_FALSE on failure.
*/
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
/************************************************************************
* These routines are used by vendor dispatch functions to look up
* and add mappings between various objects and vendors.
************************************************************************/
// As far as I can tell, all EGL functions that take a context or drawable
// also take an EGLDisplay. So, we don't need any extra functions for
// mapping a context/surface/whatever to a EGLDisplay or vendor.
/*!
* Returns the EGL vendor for an EGLDisplay handle.
*/
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
/*!
* Returns the EGL vendor for an EGLDeviceEXT handle.
*/
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
/**
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
* any function that returns an EGLDeviceEXT handle should call this.
*
* Supported since ABI version 1.2.
*/
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
} __EGLapiExports;
/*****************************************************************************
* API library imports *
*****************************************************************************/
/*!
* The enum values accepted by \c __EGLapiImports::getVendorString.
*/
enum
{
/*!
* The set of platform extensions that the vendor supports.
*
* Platform extensions typically just add enums to the existing
* eglGetPlatformDisplay function, so we don't need any additional logic
* or special handling from libEGL itself.
*
* As a result, vendor libraries can add these extensions without having
* to modify libEGL to match.
*/
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
};
/*!
* This structure stores required and optional vendor library callbacks.
*/
typedef struct __EGLapiImportsRec {
/**
* Creates an EGLDisplay. This function is used to handle both
* \c eglGetDisplay and \c eglGetPlatformDisplay.
*
* For some platform types, libEGL may be able to identify a specific
* vendor to use.
*
* If libEGL can't figure out which vendor to use on its own, then it will
* go through the list of available vendor libraries and call this function
* until one succeeds.
*
* If the application calls eglGetPlatformDisplay, then the parameters are
* passed through from the application.
*
* If the application calls eglGetDisplay, and the native display handle is
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
* type, and will pass that platform to the vendor library.
*
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
* libEGL will call into each vendor library with \p platform set to
* \c EGL_NONE. The vendor library can then select a default display to
* return.
*
* In all cases, if the vendor library can't find a matching EGLDisplay,
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
* through the vendor's \c eglGetError function.
*/
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
const EGLAttrib *attrib_list);
/*!
* Checks if the vendor library supports a given client API (that is, the
* API value passed to \c eglBindAPI).
*/
EGLBoolean (* getSupportsAPI) (EGLenum api);
/*!
* (OPTIONAL) Returns a string from the vendor library. This is used for
* anything that isn't available from eglQueryString.
*/
const char * (* getVendorString) (int name);
/*
* NOTE: libEGL will load all of the vendor libraries up front, so it
* handles extension functions slightly differently than EGL.
*/
/*!
* This retrieves the pointer to the real EGL or core GL function.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function.
*/
void * (* getProcAddress) (const char *procName);
/*!
* This retrieves vendor-neutral functions which use the
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
*
* A vendor library must provide a dispatch function for all EGL display
* extension functions that it supports.
*
* Client extension functions cannot be dispatched based on an EGDisplay,
* so they must be handled in libEGL itself.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function or \p procName is not a EGL display function.
*/
void * (*getDispatchAddress) (const char *procName);
/*!
* This notifies the vendor library which dispatch table index is
* assigned to a particular EGL extension function.
*/
void (*setDispatchIndex) (const char *procName, int index);
/*
* The vendor library may use the isPatchSupported, initiatePatch,
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
* entrypoints at make current time, provided no other contexts are current
* and the TLS model supports this functionality. This is a performance
* optimization that may not be available at runtime; the vendor library
* must not depend on this functionality for correctness.
*
* To use this optimization, the vendor library must provide at least the
* isPatchSupported and initiatePatch entrypoints.
*
* Note that these functions are identical to the ones used in GLX.
*/
/*!
* (OPTIONAL) Checks to see if the vendor library supports patching the
* given stub type and size.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*/
GLboolean (* isPatchSupported)(int type, int stubSize);
/*!
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
* top-level entrypoints.
*
* The vendor library should use the \p lookupStubOffset callback to find
* the addresses of each entrypoint.
*
* This function may be called more than once to patch multiple sets of
* entrypoints. For example, depending on how they're built, libOpenGL.so
* or libGL.so may have their own entrypoints that are separate functions
* from the ones in libGLdispatch.
*
* Note that during this call is the only time that the entrypoints can be
* modified. After the call to \c initiatePatch returns, the vendor library
* should treat the entrypoints as read-only.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*
* \return GL_TRUE if the vendor library supports patching with this type
* and size.
*/
GLboolean (*initiatePatch)(int type,
int stubSize,
DispatchPatchLookupStubOffset lookupStubOffset);
/*!
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
* longer owns the top-level entrypoints.
*
* Libglvnd will take care of the restoring the entrypoints back to their
* original state. The vendor library must not try to modify them.
*/
void (*releasePatch)(void);
/*!
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
* This callback allows vendor libraries to perform any per-thread
* initialization.
*
* This is basically a workaround for broken applications. A lot of apps
* will make one or more invalid GLX/EGL calls on a thread (often including
* a MakeCurrent with invalid parameters), and then will try to call an
* OpenGL function.
*
* A non-libglvnd-based driver would be able to initialize any thread state
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
* past libGLX.
*
* This function is optional. If it's \c NULL, then libGLdispatch will
* simply ignore it.
*
* \note This function may be called concurrently from multiple threads.
*/
void (*patchThreadAttach)(void);
/*!
* (OPTIONAL) Tries to determine the platform type for a native display.
*
* If the vendor library provides this function, then libglvnd will call it
* to determine which platform to use for a native display handle in
* eglGetDisplay.
*
* If no vendor library identifies the platform, then libglvnd will fall
* back to its own platform detection logic.
*
* Libglvnd can call this function for any native display handle except
* \c EGL_DEFAULT_DISPLAY.
*
* No matter what the value of \p native_display, the vendor library must
* not crash, and must not return a false match. If the vendor library
* can't identify the display, then it must return \c EGL_NONE.
*
* In particular, that means that a vendor library must not return any sort
* of default or fallback platform.
*
* \param native_display The native display handle passed to eglGetDisplay.
* \return Either a platform type enum or EGL_NONE.
*/
EGLenum (* findNativeDisplayPlatform) (void *native_display);
} __EGLapiImports;
/*****************************************************************************/
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* Vendor libraries must export a function called __egl_Main() with the
* following prototype.
*
* This function also performs a handshake based on the ABI version number.
* Vendor libraries can optionally use the version number to support older
* versions of the ABI.
*
* \param[in] version The ABI version. The upper 16 bits contains the major version
* number, and the lower 16 bits contains the minor version number.
*
* \param[in] exports The table of functions provided by libEGL. This pointer will
* remain valid for as long as the vendor is loaded.
*
* \param[in] vendor The opaque pointer used to identify this vendor library. This
* may be used in future versions to provide additional per-vendor information.
*
* \param[out] imports The function table that the vendor library should fill
* in. The vendor library must assign every non-optional function in the
* struct.
*
* \return True on success. If the vendor library does not support the
* requested ABI version or if some other error occurs, then it should return
* False.
*/
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* @}
*/
#if defined(__cplusplus)
}
#endif
#endif /* __LIB_EGL_ABI_H */
+104
View File
@@ -403,7 +403,10 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
@@ -434,10 +437,14 @@ set(SOURCE_FILES
MobileGL/MG_Backend/Init.cpp
MobileGL/MG_Backend/BackendObject.cpp
MobileGL/MG_Backend/HostFrameBridge.cpp
MobileGL/MG_Backend/GbmFrameChannel.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/Utils.cpp
MobileGL/MG_Backend/DirectGLES/Managers.cpp
MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
@@ -640,6 +647,7 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/FatalFunnel.cpp
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
MobileGL/MG_Remote/Client/ClientSession.cpp
MobileGL/MG_Remote/Client/DmaBufImport.cpp
MobileGL/MG_Remote/Client/EmitTables.cpp
MobileGL/MG_Remote/Client/CapsMirror.cpp
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
@@ -778,6 +786,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# target, and above all no flatc in the build graph. protocol_generated.h
# is committed and regenerated by scripts/gen_protocol.py.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
endif()
add_library(${CMAKE_PROJECT_NAME} SHARED
@@ -815,6 +826,17 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
${MOBILEGL_LINK_LIBRARIES}
)
# A LINUX BUILD COMPILES THE DirectVulkan BACKEND BUT HAS NO VULKAN LOADER TO LINK AGAINST, so the
# resulting shared library carries undefined vk* symbols - and that is not theoretical: glvnd dlopens
# this file as the EGL vendor with RTLD_NOW, so the load fails outright with "undefined symbol:
# vkGetQueryPoolResults" (measured on the phone's container). Android links the loader already; on
# Linux the caller passes the runtime's own libvulkan.so.1, and leaving the variable empty keeps the
# previous behaviour for every build that has no Vulkan runtime to point at.
set(MOBILEGL_VULKAN_LOADER "" CACHE FILEPATH "Vulkan loader (libvulkan.so.1) to link into the GL library")
if (UNIX AND NOT ANDROID AND MOBILEGL_VULKAN_LOADER)
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${MOBILEGL_VULKAN_LOADER})
endif()
target_compile_definitions(${CMAKE_PROJECT_NAME}
PUBLIC
${MOBILEGL_COMPILE_DEF}
@@ -881,6 +903,68 @@ if(NOT ANDROID)
)
endif()
# ---------------------------------------------------------------------------
# MobileGLGbm: the GBM drop-in the container's compositor loads.
#
# kwin - the Wayland compositor in the container half of the split - allocates the
# buffers it renders into and scans out through libgbm, and the dma-buf fds it gets
# back are what the Anland display producer ships to the phone. Mesa's libgbm cannot
# serve that: its buffers belong to the host GPU, and the phone would receive a copy
# through a path that was never designed to carry one. This target is the library
# that is loaded in its place, under the name a loader looks for.
#
# It is deliberately NOT part of libMobileGL.so. The compositor needs one file named
# libgbm.so that it loads instead of Mesa's, and folding this code into the GL
# library would put a second GL frontend, a second Vulkan instance and a second
# glslang in the compositor's address space to serve an API that shares none of them.
#
# The include paths below are borrowed rather than linked. Includes.h - which this
# library includes for the MobileGL types and the MGLOG macros - pulls in the GL,
# EGL, Vulkan, glslang, SPIRV-Cross and xxHash HEADERS transitively, and headers are
# all this library wants from them: linking any of those static libraries into a
# libgbm would embed a copy of each in a process that already has one.
#
# -Bsymbolic-functions, for the same reason the main target carries it: the point of
# this library is to REPLACE a libgbm that may already be loaded in the process, so
# every internal reference has to bind to this file's own definitions rather than to
# whatever else the process has under the same names.
# ---------------------------------------------------------------------------
add_library(MobileGLGbm SHARED
MobileGL/MG_Gbm/GbmDevice.cpp
# The display host's socket protocol, kept in its own translation unit so that the
# allocation paths never have to know how a frame arrives - only that one did.
MobileGL/MG_Gbm/GbmHostChannel.cpp
MobileGL/MG_Gbm/GbmApi.cpp
# MGLOG_I/W/E expand to a call to MobileGL::MG_Util::Debug::Log, and this is the
# one translation unit that defines it. It is compiled in rather than linked from
# libMobileGL.so because this library has to load into a compositor that has no
# libMobileGL in it at all - and because a libgbm with an undefined symbol is a
# library that fails on the first diagnostic rather than at a link step somebody
# could have read.
MobileGL/MG_Util/Debug/Log.cpp
)
# The vendored gbm.h first: this library implements THAT declaration set, and a
# system <gbm.h> from another Mesa version on the include path ahead of it would
# change the ABI under the implementations in GbmApi.cpp.
target_include_directories(MobileGLGbm PRIVATE
${CMAKE_SOURCE_DIR}/3rdparty/gbm/include
${MOBILEGL_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/3rdparty/glslang
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Cross
${CMAKE_SOURCE_DIR}/3rdparty/xxHash
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
)
set_target_properties(MobileGLGbm PROPERTIES
OUTPUT_NAME gbm
POSITION_INDEPENDENT_CODE ON
)
if (UNIX AND NOT APPLE)
target_link_options(MobileGLGbm PRIVATE "LINKER:-Bsymbolic-functions")
endif()
if (TRACY_ENABLE)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
@@ -888,6 +972,19 @@ if (TRACY_ENABLE)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
endif ()
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
#
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
# With default visibility every reference inside this library resolves against the whole process,
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
# the two visibilities do not mix; this flag is that promise made by the linker instead.
if (UNIX AND NOT APPLE)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
endif()
if (ANDROID)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
android
@@ -1156,3 +1253,10 @@ if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
endif()
endif()
endif()
# The GBM library carries the tree's logger, which reaches Android's own log. The main target gets
# these libraries in its own Android block; a library built from the same sources needs them for the
# same reason, and without them it is the LINK that fails rather than anything in the code.
if (ANDROID)
target_link_libraries(MobileGLGbm PRIVATE log android)
endif()
+10
View File
@@ -499,6 +499,7 @@ namespace MobileGL::MG_Config {
enum class IpcSurface : Uint8 {
Offscreen = 0, // a window surface is the client's own window, as before (the default)
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
Host = 2, // a window surface draws into frames the DISPLAY HOST owns; the host presents
};
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
@@ -729,6 +730,15 @@ namespace MobileGL::MG_Config {
inline Bool ServerOwnedWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
}
// P12: the other on-screen shape, and the mirror image of the one above. The display host
// (the Android side that owns the screen) allocates the frames, offers each one over the
// host frame bridge, and puts it on the glass when this side says it is drawn - so a
// client that asks for this has no window of its own either, and the server takes the
// frames rather than making a window.
inline Bool HostFrameWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Host;
}
#else
// The whole point: in a build without MG_Remote this folds at compile time, so
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
+11 -7
View File
@@ -494,8 +494,8 @@ namespace MobileGL::MG_ConfigLoader {
}
}
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
// the semantics). An unknown value is named and read as the default, like every knob here.
// P12 (on-screen): MOBILEGL_IPC_SURFACE = offscreen | server | host (Config.h has the
// semantics). An unknown value is named and read as the default, like every knob here.
{
String surface;
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
@@ -505,9 +505,11 @@ namespace MobileGL::MG_ConfigLoader {
ipc.Surface = MG_Config::IpcSurface::Offscreen;
} else if (surface == "server") {
ipc.Surface = MG_Config::IpcSurface::Server;
} else if (surface == "host") {
ipc.Surface = MG_Config::IpcSurface::Host;
} else {
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
"offscreen|server, using offscreen",
"offscreen|server|host, using offscreen",
surface.c_str());
ipc.Surface = MG_Config::IpcSurface::Offscreen;
}
@@ -515,12 +517,14 @@ namespace MobileGL::MG_ConfigLoader {
// parses this before RunSession sets its own transport, and has no client half anyway.)
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
if (ipc.Surface != MG_Config::IpcSurface::Offscreen && !serverProcess &&
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
// Said, not silently honoured: there is no remote server here to own a window.
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
"and this run has none. Window surfaces stay the client's own");
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=%s is IGNORED - it asks for an on-screen "
"surface (the server's own window, or the frames the display host owns) and "
"this run has no remote server to provide one: MOBILEGL_TRANSPORT is not "
"spawn. Window surfaces stay the client's own",
ipc.Surface == MG_Config::IpcSurface::Server ? "server" : "host");
}
}
+9 -2
View File
@@ -8,6 +8,8 @@
#pragma once
#include <cstdio>
// ============== Platform-specific definitions and macros ============== //
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
@@ -34,6 +36,11 @@
#ifdef _WIN32
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
#else
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
// points with. The one place where that is not enough - a vendor library handing glvnd its own
// entry points, where a reference resolving against the process instead of this library would be a
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
// here, because a second visibility on the same declaration does not compile.
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
#endif
@@ -105,8 +112,8 @@
#define MOBILEGL_ASSERT(condition, ...) \
do { \
if (!(condition)) { \
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
fprintf(stderr, "Assertion failed" __VA_OPT__(": ") "%s", "" __VA_ARGS__); \
fprintf(stderr, " at %s:%d (%s)\n", __FILE__, __LINE__, __func__); \
TRAP; \
} \
} while (0)
+86 -5
View File
@@ -172,11 +172,15 @@ namespace MobileGL::MG_Backend {
return false;
}
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
return false;
}
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
// refused a caller for a reason that is not there: the surface and context tables are keyed
// by the display each was created on, every operation carries its own display handle, and
// this field has no reader anywhere - it was the whole of the rule.
//
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
// probing another, so the second eglInitialize is the one that matters, and a refusal there
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
// and the real display having failed, with nothing in between that says which was refused.
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
@@ -209,6 +213,28 @@ namespace MobileGL::MG_Backend {
return true;
}
// The frame itself is the backend's business (InitHostFrameSurface); what happens here is
// the registration every surface kind shares, so the swap path and the state mirrors see
// a host-framed surface exactly like any other.
Bool BackendObject::CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized) {
MGLOG_E("CreateEGLHostFrameSurface failed: EGL display is not initialized");
return false;
}
if (surface == EGL_NO_SURFACE) {
MGLOG_E("CreateEGLHostFrameSurface failed: invalid EGLSurface");
return false;
}
auto& state = m_eglSurfaces[surface];
state = EGLSurfaceState{
.Kind = SurfaceKind::HostFrame,
.Width = static_cast<EGLint>(std::max<Uint32>(width, 1)),
.Height = static_cast<EGLint>(std::max<Uint32>(height, 1)),
};
return ActivateEGLSurface(surface);
}
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
@@ -290,6 +316,11 @@ namespace MobileGL::MG_Backend {
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
return false;
}
} else if (surfaceState->Kind == SurfaceKind::HostFrame) {
if (!InitHostFrameSurface(surfaceState->Width, surfaceState->Height)) {
MGLOG_E("ActivateEGLSurface failed: backend InitHostFrameSurface failed");
return false;
}
} else {
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
return false;
@@ -315,6 +346,29 @@ namespace MobileGL::MG_Backend {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
// EGL_KHR_surfaceless_context. A context whose draw and read surfaces are both absent has
// no framebuffer of its own to be bound to: the caller draws into framebuffers it binds
// itself, and this side only has to have its own GL context current on this thread for the
// records that follow. Everything below this point describes a surface, so none of it
// applies - and what is recorded is exactly what was asked for, so a later swap on a
// surface that does not exist is never made to look like one that does.
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
if (!m_backendCapabilitiesInitialized) {
if (!InitCapabilities()) {
MGLOG_E("MakeEGLCurrent failed: InitCapabilities failed");
return false;
}
m_backendCapabilitiesInitialized = true;
}
ReleaseEGLCurrentThread(threadKey);
m_eglCurrentThreads[threadKey] = EGLCurrentState{
.Display = dpy,
.DrawSurface = EGL_NO_SURFACE,
.ReadSurface = EGL_NO_SURFACE,
.Context = ctx,
};
return true;
}
if (!m_eglSurfaceInitialized) {
if (draw != read || !ActivateEGLSurface(draw)) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
@@ -387,6 +441,12 @@ namespace MobileGL::MG_Backend {
return false;
}
// A host-framed surface has no swapchain: presenting it means telling the host the frame
// is drawn, because the host is what puts it on the glass.
if (m_eglSurfaceKind == SurfaceKind::HostFrame) {
return PresentHostFrame();
}
const auto& backendFunctions = GetBackendFunctions();
if (!backendFunctions.Present) {
MGLOG_E("SwapEGLBuffers failed: backend Present function is null");
@@ -404,6 +464,27 @@ namespace MobileGL::MG_Backend {
}
}
// A backend that has no host-framed surfaces says so by name: the server may ask for one
// (MOBILEGL_IPC_SURFACE=host) against a build that cannot serve it, and that has to be a
// refusal with a reason rather than a surface that draws nowhere.
Bool BackendObject::InitHostFrameSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
MGLOG_E("InitHostFrameSurface: this backend has no host-framed surfaces");
return false;
}
Bool BackendObject::PresentHostFrame() {
MGLOG_E("PresentHostFrame: this backend has no host-framed surfaces");
return false;
}
Bool BackendObject::PresentHostFrameIfCurrent() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglSurfaceInitialized || m_eglSurfaceKind != SurfaceKind::HostFrame) return true;
return PresentHostFrame();
}
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
if (surface == EGL_NO_SURFACE) {
return false;
+17 -1
View File
@@ -595,6 +595,18 @@ namespace MobileGL {
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
// The host owns the screen: this side never gets a window of the client's to draw
// into, so the surface it draws into is a frame the host allocated and offered over
// the host frame bridge. Each swap tells the host the frame is done and takes the
// next; presentation is the host's, not a swapchain's.
virtual Bool CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool InitHostFrameSurface(EGLint width, EGLint height);
virtual Bool PresentHostFrame();
// The present RECORD's half of a host-framed swap. A split client's swap reaches the
// server as a present record (ServerVerbSink::OnPresent), never as SwapEGLBuffers, so
// without this the host was never told a frame was drawn. True when the current
// surface is not host-framed: there is nothing to present then.
Bool PresentHostFrameIfCurrent();
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// Forwards the app-requested eglSwapInterval to the backend's native
@@ -616,7 +628,11 @@ namespace MobileGL {
enum class SurfaceKind {
None,
Window,
Pbuffer
Pbuffer,
// A surface whose drawing target is a frame the display host owns. There is no
// swapchain behind it: the host puts the frame on the glass when this side says
// it is drawn, which is why presenting one is its own call and not a Present.
HostFrame
};
struct EGLCurrentState {
@@ -6,6 +6,12 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// The role's own backend is resolved the way Utils.cpp (C6 / ID-52) resolves it; that needs this
// header, and it has to be included at file scope - a header included inside this file's namespace
// would nest its own namespace under it.
#include <MG_Remote/Server/ServerLoop.h>
#endif
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
@@ -1028,6 +1034,241 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
}
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
namespace {
// The host may still be starting when the client asks for its surface: it is a separate
// app that has to come up and allocate its pool. Waiting is better than refusing a
// surface a client is entitled to, and the wait is answered by name if it expires.
constexpr Int kHostFrameWaitMs = 30000;
}
Bool TakeHostFrameForExternalTexture(Uint width, Uint height) {
// THIS ROLE'S OWN BACKEND, resolved exactly the way ActiveBackendFormatCaps (Utils.cpp,
// C6 / ID-52) resolves it: under a live split session the SERVER's private backend owns the
// context on the apply thread, and pActiveBackendObject is the CLIENT's mirror there.
MG_Backend::BackendObject* backend = nullptr;
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
backend = MG_Remote::Server::ServerLoopInstance().Backend();
} else {
backend = MG_Backend::pActiveBackendObject.get();
}
if (backend == nullptr) return false;
return backend->InitHostFrameSurface(static_cast<EGLint>(width), static_cast<EGLint>(height));
}
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
// The EGL context comes from the same place a pbuffer session's does. What changes is not
// how this side draws but what it draws INTO: a frame the display host allocated, bound as
// this context's framebuffer. The pbuffer stays as the draw target for the moment before
// the first frame arrives, which is the only moment it is used.
if (!DirectGLES::InitPbufferSurface(width, height)) {
MGLOG_E("InitHostFrameSurface: the context the host frames are drawn in did not come up");
return false;
}
if (!m_hostFrameBridgeOpen) {
String why;
if (!m_hostFrameBridge.Open(MG_Backend::kHostFrameSocket, kHostFrameWaitMs, why)) {
MGLOG_E("InitHostFrameSurface: no display host: %s", why.c_str());
return false;
}
m_hostFrameBridgeOpen = true;
}
return TakeHostFrame();
}
Bool BackendObject_DirectGLES::TakeHostFrame() {
String why;
struct AHardwareBuffer* frame = nullptr;
MG_Backend::HostFrameOffer offer{};
if (!m_hostFrameBridge.Acquire(&frame, offer, why)) {
MGLOG_E("TakeHostFrame: %s", why.c_str());
return false;
}
HostFrameTarget target;
if (!HostFrameTargetCreate(frame, target, why)) {
MGLOG_E("TakeHostFrame: the host frame %ux%u did not become a draw target: %s", offer.Width,
offer.Height, why.c_str());
AHardwareBuffer_release(frame);
return false;
}
DestroyHostFrame();
m_hostFrameTarget = target;
m_hostFrameBuffer = frame;
m_hostFrameOffer = offer;
// The canvas is made at the first frame's size and kept; a failure leaves the frame itself
// as the draw target, which is how this path behaved before the canvas existed.
if (!EnsureHostFrameCanvas(target.Width, target.Height))
MGLOG_E_ONCE("TakeHostFrame: no canvas in front of the host frames; drawing into the frame itself");
const HostFrameTarget& drawTarget = HostFrameDrawTarget();
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
// resolve to this target, and that resolution is what this global is.
g_hostFrameFramebufferId = drawTarget.Framebuffer;
// Published together with the framebuffer, because they describe the same target: the STORAGE
// an attachment is bound to when the client's texture has none of its own, and the SIZE that
// decides whether binding it is right at all.
g_hostFrameRenderbuffer = drawTarget.Renderbuffer;
g_hostFrameWidth = drawTarget.Width;
g_hostFrameHeight = drawTarget.Height;
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(drawTarget);
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
// over there renders into THESE frames - it runs this project's own GBM, whose
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
// every frame taken here is useless to it until its descriptor has crossed. Open() binds
// the listening socket a container dials, and the IsUp() test keeps the ordinary frame
// down to an integer compare: the socket outlives every container and every connection,
// while OfferFrame accepts whoever dialled since the last frame and opens the socket
// itself when the first frame arrives before any container has.
//
// NEITHER ANSWER IS EXAMINED, and that is the channel's contract rather than an
// oversight: no container running, a container that has died, or one that has taken the
// descriptor and stopped reading must all leave this frame drawn and presented exactly as
// it would have been without a channel - the descriptor is a copy of the pixels' route,
// not a step in producing them.
if (!m_gbmFrameChannel.IsUp()) m_gbmFrameChannel.Open();
(void)m_gbmFrameChannel.OfferFrame(frame, offer);
MGLOG_I("host frame %u is the draw target: %ux%u", offer.Index, target.Width, target.Height);
return true;
}
void BackendObject_DirectGLES::DestroyHostFrame() {
// The canvas outlives the frame, so with one the globals keep naming it: the attachments
// made over it stay valid between this frame being handed back and the next one arriving.
g_hostFrameFramebufferId = m_hostFrameCanvas.Framebuffer;
g_hostFrameRenderbuffer = m_hostFrameCanvas.Renderbuffer;
g_hostFrameWidth = m_hostFrameCanvas.Width;
g_hostFrameHeight = m_hostFrameCanvas.Height;
HostFrameTargetDestroy(m_hostFrameTarget);
if (m_hostFrameBuffer != nullptr) {
AHardwareBuffer_release(m_hostFrameBuffer);
m_hostFrameBuffer = nullptr;
}
// AND NOTHING IS SAID TO THE CHANNEL HERE, which is the fix rather than an omission. This
// runs once per presented frame: it used to Close() the channel (unlinking the container's
// socket node and dropping its connection sixty times a second - measured: connect()
// answered EAGAIN(11) on every attempt) and it used to tell the channel to forget the frame,
// which left the ONE window a compositor's first allocations arrive in - between this frame
// being presented and the host handing the next one over, i.e. inside Complete() above -
// with nothing to answer a dialer with. A compositor allocates its swapchain ONCE and keeps
// those buffers for the life of the process, so a dialer that finds nothing here renders
// its whole life into buffers nobody displays. The frame the channel remembers is the last
// one the host handed over: the next offer replaces it and Close() releases it.
}
Bool BackendObject_DirectGLES::PresentHostFrame() {
// When this returns, the pixels are complete: glFinish is the fence the host is told
// about, and the host's own readback - which Complete reads - is what proves the drawing
// landed where the screen will look.
BlitHostFrameCanvas();
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
// server's apply loop passes through the host channel - the swap it runs lands here for a
// host-framed surface - and a release is the container's compositor saying it is done
// reading a frame this side handed over. Poll() is a bare recvmsg on a non-blocking
// socket: with no container connected it returns on its first test, and a container that
// is between frames answers EAGAIN, so a pass costs nothing when there is nothing to
// read. Nothing here is waited for and nothing is required to arrive; the count of
// frames offered and not released is a number the channel keeps for the log, never a
// frame the render path waits on.
m_gbmFrameChannel.Poll();
String why;
if (!m_hostFrameBridge.Complete(m_hostFrameOffer, /*imported=*/true, /*drawn=*/true, /*fenceOk=*/true, why)) {
MGLOG_E("PresentHostFrame: %s", why.c_str());
return false;
}
// The answer has been read, so the host has already put this frame on the glass: it is
// this side's to drop, and the next one becomes the drawing target.
DestroyHostFrame();
return TakeHostFrame();
}
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint width, Uint height) {
if (m_hostFrameCanvas.Framebuffer != 0) return true;
if (width == 0 || height == 0 || !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glBlitFramebuffer ||
!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glCheckFramebufferStatus)
return false;
// RAW binds, as HostFrameTargetCreate makes them: the caller invalidates the engine's
// framebuffer shadow and binds through it right after.
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
// The frames are R8G8B8A8_UNORM (the display host allocates them so), and so is this.
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
GLuint framebuffer = 0;
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E("host frame canvas %ux%u: framebuffer status 0x%04x", width, height, status);
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
return false;
}
m_hostFrameCanvas.Renderbuffer = renderbuffer;
m_hostFrameCanvas.Framebuffer = framebuffer;
m_hostFrameCanvas.Width = width;
m_hostFrameCanvas.Height = height;
MGLOG_I("host frame canvas: %ux%u as framebuffer %u (renderbuffer %u), copied into each frame at present",
width, height, framebuffer, renderbuffer);
return true;
}
void BackendObject_DirectGLES::BlitHostFrameCanvas() {
if (m_hostFrameCanvas.Framebuffer == 0 || m_hostFrameTarget.Framebuffer == 0) return;
// Whatever the compositor had bound is put back as it was, raw for raw, so the engine's
// shadow (which never saw this) is still right afterwards. The scissor test and
// rasterizer discard both apply to a blit, so they are lifted for it.
GLint readBinding = 0;
GLint drawBinding = 0;
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readBinding);
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawBinding);
const GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
const GLboolean discard = g_GLESFuncs.glIsEnabled(GL_RASTERIZER_DISCARD);
if (scissor) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
if (discard) g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD);
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_hostFrameCanvas.Framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_hostFrameTarget.Framebuffer);
const Bool sameSize = m_hostFrameCanvas.Width == m_hostFrameTarget.Width &&
m_hostFrameCanvas.Height == m_hostFrameTarget.Height;
g_GLESFuncs.glBlitFramebuffer(0, 0, static_cast<GLint>(m_hostFrameCanvas.Width),
static_cast<GLint>(m_hostFrameCanvas.Height), 0, 0,
static_cast<GLint>(m_hostFrameTarget.Width),
static_cast<GLint>(m_hostFrameTarget.Height), GL_COLOR_BUFFER_BIT,
sameSize ? GL_NEAREST : GL_LINEAR);
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readBinding));
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawBinding));
if (scissor) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
if (discard) g_GLESFuncs.glEnable(GL_RASTERIZER_DISCARD);
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
// The other arm's half: on a build with no host frames there is no frame to take, and the
// attach site's ordinary path (a storage-less external texture, exactly as before) is the honest
// behaviour there rather than a link error.
Bool TakeHostFrameForExternalTexture(Uint, Uint) { return false; }
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
(void)width;
(void)height;
MGLOG_E("InitHostFrameSurface: host frames are a disaggregated Android path");
return false;
}
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
void BackendObject_DirectGLES::DestroyHostFrame() {}
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint, Uint) { return false; }
void BackendObject_DirectGLES::BlitHostFrameCanvas() {}
Bool BackendObject_DirectGLES::PresentHostFrame() {
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
return false;
}
#endif
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
return DirectGLES::InitPbufferSurface(width, height);
}
@@ -1052,13 +1293,42 @@ namespace MobileGL::MG_Backend::DirectGLES {
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
return false;
}
if (!m_eglSurfaceInitialized) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
return false;
// EGL_KHR_surfaceless_context, WHICH IS THE SHAPE A COMPOSITOR ASKS FOR. Its context has no
// draw surface of its own - every frame goes into a framebuffer it binds itself, and under
// this project that framebuffer is one of the display host's frames - so the surface checks
// below describe a surface that legitimately does not exist. What the server still needs is
// its own GL context current on this thread for the records that follow, and that is what
// the native bind underneath them does.
const Bool surfaceless = draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT;
if (surfaceless && !m_eglSurfaceInitialized) {
// A DRAWABLE OF THIS SIDE'S OWN, WHICH IS WHAT A SURFACELESS CLIENT LEAVES TO US. The
// backend's native context has to be current on a surface of some kind before a single
// record of the client's can run - that is what the Init*Surface calls establish - and a
// client that makes its context current with no surface at all supplies none of its own.
// A pbuffer is the one that costs nothing to keep: it is never scanned out and never
// read, because every frame the client draws goes into a framebuffer it binds itself.
// The client is NOT told it has a surface - m_eglSurface stays EGL_NO_SURFACE - so a
// swap on one is still the refusal it was.
if (!InitPbufferSurface(1, 1)) {
MGLOG_E("MakeEGLCurrent failed: the backend's own drawable for a surfaceless context could not "
"be created");
return false;
}
m_eglSurface = EGL_NO_SURFACE;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = SurfaceKind::Pbuffer;
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
return false;
if (!surfaceless) {
if (!m_eglSurfaceInitialized) {
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
return false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
return false;
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -1086,9 +1356,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple
// (the ID-67 control's different tuple stays at 1 native bind where 2 are required).
const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
m_eglSurfaceInitialized && m_eglSurface == draw &&
DirectGLES::IsBackendContextCurrentOnThisThread() &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
(surfaceless || (m_eglSurfaceInitialized && m_eglSurface == draw &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx)));
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
NoteNativeContextGone();
return false;
@@ -1107,6 +1377,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
return false;
}
// A HOST-FRAMED SURFACE HAS TO BE PUT BACK AFTER EVERY MAKE-CURRENT. The native call
// binds the surface's own default framebuffer - the pbuffer this path makes its context
// with - and that is not where these pixels go. Both the driver and the engine's binding
// shadow have to be told again, or the next default-framebuffer bind is deduped away and
// every draw lands in a pbuffer nobody sees.
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
FramebufferImpl::InvalidateFramebufferBindingCache();
HostFrameTargetBind(HostFrameDrawTarget());
}
return true;
}
@@ -1263,7 +1542,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
E_GL_ARB_get_program_binary,
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
// missing checkSupported() answered false, EglContext::create() returned nullptr,
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
// rendering context" and quit before drawing a frame.
//
// The three are true statements about this backend now rather than a test unlock: every
// procedure they define is implemented in MG_Impl/GLImpl/Exporting/Definitions.cpp over
// the same shader and program objects the core entry points drive - the 39 of
// ARB_shader_objects, ARB_vertex_shader's three attrib-location entry points, and none
// at all for ARB_fragment_shader ("New Procedures and Functions: None" - it is the
// capability half, and the reason kwin's conjunction names it).
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
// NPOT textures: core since GL 2.0, and every host this backend can run on provides
// them. Full non-power-of-two support - mipmapping and every wrap mode, not ES 2.0's
// CLAMP_TO_EDGE-only exception - is required from ES 3.0 on, and the entries this list
// already advertises without any gate (compute shaders, shader storage buffers,
// indirect draws) need an ES 3.1 host. Nothing in the texture path rejects or pads a
// non-power-of-two size - storage and sampling are forwarded to that host - so the
// string describes what an application actually gets.
E_GL_ARB_texture_non_power_of_two,
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
// and this name is what makes them reachable - libepoxy in kwin resolves
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
// The render-server half of the import (fd over the GBM socket, then the server-side
// EGLImage) is the next change; until it lands an imported texture has no storage behind
// it, which is logged once at the entry point rather than left to look like a black frame.
E_GL_OES_EGL_image};
// Minecraft 26.3 checks this prerequisite before it even considers
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
// folds the version and pointer checks into SupportsDrawIndirect.
@@ -8,6 +8,9 @@
#pragma once
#include <Includes.h>
#include "HostFrameTarget.h"
#include "MG_Backend/HostFrameBridge.h"
#include "MG_Backend/GbmFrameChannel.h"
#include "../BackendObject.h"
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
@@ -56,8 +59,46 @@ namespace MobileGL::MG_Backend::DirectGLES {
private:
void UpdateDynamicBackendParameters();
Bool InitPbufferSurface(EGLint width, EGLint height) override;
// A host-framed surface draws into a frame the display host owns; presenting one
// tells the host the frame is drawn, because the host is what puts it on the glass.
// NOT CreateEGLHostFrameSurface: the base class registers the surface and activates it,
// and activation is what reaches InitHostFrameSurface below. There is nothing about the
// EGL side of a host-framed surface this backend needs to do its own way.
Bool InitHostFrameSurface(EGLint width, EGLint height) override;
Bool PresentHostFrame() override;
// Takes the next frame from the host and makes it this context's framebuffer.
Bool TakeHostFrame();
void DestroyHostFrame();
// The canvas the compositor draws into, made once at the first frame's size.
Bool EnsureHostFrameCanvas(Uint width, Uint height);
// Copies the canvas into the frame about to be presented.
void BlitHostFrameCanvas();
// What draws resolve to: the canvas when there is one, the frame itself otherwise.
const HostFrameTarget& HostFrameDrawTarget() const {
return m_hostFrameCanvas.Framebuffer != 0 ? m_hostFrameCanvas : m_hostFrameTarget;
}
void OnEGLSurfaceReleased(EGLSurface surface) override;
// MOBILEGL_IPC_SURFACE=host: the frames the display host owns, one at a time, which
// is what the host offers - it hands one over and waits for the answer before it
// presents it and offers the next.
MG_Backend::HostFrameBridge m_hostFrameBridge;
// The frames the bridge just took, on to the container: the Wayland compositor over there
// renders into this same memory, and the only route to it that end has is the dma-buf
// descriptor this channel offers per frame and reads a release back from.
MG_Backend::GbmFrameChannel m_gbmFrameChannel;
HostFrameTarget m_hostFrameTarget;
// A STABLE TARGET IN FRONT OF THE FRAMES. Every present hands the frame back and takes
// a different one, and its renderbuffer is deleted with it - but a compositor attaches
// its framebuffers ONCE (kwin: one per daemon buffer, for the life of the process), so
// attachments bound to a frame's own renderbuffer named a deleted object from the second
// frame on. The attachments name this instead, and each present copies it into the
// frame. No EGLImage behind it (Image stays null).
HostFrameTarget m_hostFrameCanvas;
struct AHardwareBuffer* m_hostFrameBuffer = nullptr;
MG_Backend::HostFrameOffer m_hostFrameOffer{};
Bool m_hostFrameBridgeOpen = false;
Bool m_initialized = false;
MG_External::EGLFunctionsTable m_EGLFunctions;
MG_External::GLESFunctionsTable m_GLESFunctions;
@@ -87,6 +87,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
MG_External::EGLFunctionsTable g_EGLFuncs;
Uint g_hostFrameFramebufferId = 0;
Uint g_hostFrameRenderbuffer = 0;
Uint g_hostFrameWidth = 0;
Uint g_hostFrameHeight = 0;
MG_External::GLESFunctionsTable g_GLESFuncs;
MG_External::GLESCapabilities g_GLESCapabilities;
@@ -6515,7 +6519,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Through the shadow: a raw bind here would leave the shadow claiming
// the previous user FBO, false-skipping its next re-bind.
FramebufferImpl::BindFramebufferId(
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
}
return;
}
@@ -6584,7 +6588,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Through the shadow: a raw bind here would leave the shadow claiming
// the previous user FBO, false-skipping its next re-bind.
FramebufferImpl::BindFramebufferId(
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
}
}
@@ -6604,7 +6608,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferImpl::g_integerColorDrawBufferMask = 0;
}
FramebufferImpl::BindFramebufferId(
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
return;
}
@@ -6645,7 +6649,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
FramebufferImpl::g_integerColorDrawBufferMask = 0;
}
FramebufferImpl::BindFramebufferId(
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, 0);
target == FramebufferTarget::Draw ? GL_DRAW_FRAMEBUFFER : GL_READ_FRAMEBUFFER, g_hostFrameFramebufferId);
return;
}
@@ -249,6 +249,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidateSyncedRenderState();
} // namespace RenderStateImpl
// MOBILEGL_IPC_SURFACE=host: what the client's DEFAULT framebuffer means while a host-framed
// surface is current. Zero everywhere else, because the driver's own 0 IS the EGL surface
// then - but on a host-framed surface the pixels belong to a frame the display host owns,
// and drawing into the driver's 0 would draw into a pbuffer nobody ever sees.
extern Uint g_hostFrameFramebufferId;
// WHAT THE HOST FRAME'S STORAGE IS, for the one caller that has to bind it in place of a
// storage-less attachment: an imported dma-buf is the display host's frame (same memory, same
// size), and the frame's storage is a RENDERBUFFER (HostFrameTarget.cpp binds it with
// glFramebufferRenderbuffer). A texture argument cannot name it, which is why these exist and
// why g_hostFrameFramebufferId alone was not enough. Zero whenever no frame is held.
extern Uint g_hostFrameRenderbuffer;
extern Uint g_hostFrameWidth;
extern Uint g_hostFrameHeight;
// TAKE THE FRAME WHEN NOTHING ELSE WILL. On the anland route the container's compositor never
// creates a window surface, so ActivateEGLSurface never runs and InitHostFrameSurface is never
// reached: the FIRST proof that the display host's frame is wanted is an ATTACHMENT of an
// imported external texture (Managers.cpp's 0x8D65 arm), and that is where the frame has to be
// taken. The host's own offer carries the width/height, so the numbers passed here are only
// what the bridge logs. Answers false (and changes nothing) whenever no backend is up.
Bool TakeHostFrameForExternalTexture(Uint width, Uint height);
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
@@ -0,0 +1,208 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "HostFrameTarget.h"
#include "DirectGLES.h"
#include "Managers.h"
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <cstring>
#include <format>
namespace MobileGL::MG_Backend::DirectGLES {
namespace {
// The four entry points this is spelled in are optional everywhere: the loader's own
// tables do not carry them (a missing EGL function there is MGLOG_F), so the extension
// strings decide first and the pointers are taken only once they have.
using GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
using CreateImageFn = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
using DestroyImageFn = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
using ImageTargetRenderbufferFn = void (*)(GLenum, GLeglImageOES);
Bool HasExtensionWord(const char* list, const char* word) {
if (list == nullptr) return false;
const SizeT n = std::strlen(word);
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
const Bool startOk = at == list || at[-1] == ' ';
const Bool endOk = !at[n] || at[n] == ' ';
if (startOk && endOk) return true;
}
return false;
}
struct EntryPoints {
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
CreateImageFn CreateImage = nullptr;
DestroyImageFn DestroyImage = nullptr;
ImageTargetRenderbufferFn TargetRenderbuffer = nullptr;
};
// Resolved once per process and cached together with the refusal: a driver that lacks one
// of these does not grow it later, and the answer names what is missing.
const EntryPoints& Resolved(String& why) {
static EntryPoints points;
static Bool resolved = false;
static String refusal;
if (!resolved) {
resolved = true;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
!g_GLESFuncs.glGetString) {
refusal = "the loader has no eglGetProcAddress/eglQueryString/eglGetCurrentDisplay/glGetString";
} else {
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
refusal = "EGL_ANDROID_get_native_client_buffer is not advertised";
} else if (!HasExtensionWord(egl, "EGL_KHR_image_base")) {
refusal = "EGL_KHR_image_base is not advertised";
} else if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
refusal = "GL_OES_EGL_image is not advertised";
} else {
points.GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
points.CreateImage =
reinterpret_cast<CreateImageFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
points.DestroyImage =
reinterpret_cast<DestroyImageFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
points.TargetRenderbuffer = reinterpret_cast<ImageTargetRenderbufferFn>(
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
if (points.GetNativeClientBuffer == nullptr || points.CreateImage == nullptr ||
points.DestroyImage == nullptr || points.TargetRenderbuffer == nullptr) {
refusal = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / eglCreateImageKHR / "
"eglDestroyImageKHR / glEGLImageTargetRenderbufferStorageOES";
points = EntryPoints{};
}
}
}
}
why = refusal;
return points;
}
void DrainGLErrors() {
if (!g_GLESFuncs.glGetError) return;
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
}
}
}
Bool HostFrameTargetSupported(String& why) {
const EntryPoints& points = Resolved(why);
return points.GetNativeClientBuffer != nullptr;
}
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why) {
target = HostFrameTarget{};
if (buffer == nullptr) {
why = "no buffer";
return false;
}
const EntryPoints& points = Resolved(why);
if (points.GetNativeClientBuffer == nullptr) return false;
if (!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glBindRenderbuffer || !g_GLESFuncs.glGenFramebuffers ||
!g_GLESFuncs.glBindFramebuffer || !g_GLESFuncs.glFramebufferRenderbuffer ||
!g_GLESFuncs.glCheckFramebufferStatus) {
why = "the loader has no glGenRenderbuffers/glBindRenderbuffer/glGenFramebuffers/glBindFramebuffer/"
"glFramebufferRenderbuffer/glCheckFramebufferStatus";
return false;
}
AHardwareBuffer_Desc desc{};
AHardwareBuffer_describe(buffer, &desc);
DrainGLErrors();
const EGLClientBuffer clientBuffer = points.GetNativeClientBuffer(buffer);
if (clientBuffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return false;
}
const EGLint imageAttribs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
const EGLImageKHR image =
points.CreateImage(g_EGLFuncs.eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
clientBuffer, imageAttribs);
if (image == EGL_NO_IMAGE_KHR) {
why = "eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) refused the host frame";
return false;
}
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
points.TargetRenderbuffer(GL_RENDERBUFFER, reinterpret_cast<GLeglImageOES>(image));
const GLenum renderbufferError = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
if (renderbufferError != GL_NO_ERROR) {
why = std::format("glEGLImageTargetRenderbufferStorageOES -> 0x{:04x}", renderbufferError);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
return false;
}
GLuint framebuffer = 0;
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
why = std::format("the host frame as a color attachment -> framebuffer status 0x{:04x}", status);
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
return false;
}
target.Image = image;
target.Renderbuffer = renderbuffer;
target.Framebuffer = framebuffer;
target.Width = desc.width;
target.Height = desc.height;
MGLOG_I("host frame target: %ux%d as framebuffer %u (stride %u, format 0x%x)", desc.width, desc.height,
framebuffer, desc.stride, desc.format);
return true;
}
void HostFrameTargetBind(const HostFrameTarget& target) {
if (target.Framebuffer == 0) return;
// THROUGH THE ENGINE'S SHADOW, never raw: a raw bind would leave the shadow claiming
// the previous framebuffer and false-skip the next re-bind (FramebufferImpl, Managers.h).
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, target.Framebuffer);
}
void HostFrameTargetDestroy(HostFrameTarget& target) {
if (target.Framebuffer != 0 && g_GLESFuncs.glDeleteFramebuffers)
g_GLESFuncs.glDeleteFramebuffers(1, &target.Framebuffer);
if (target.Renderbuffer != 0 && g_GLESFuncs.glDeleteRenderbuffers)
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
if (target.Image != nullptr) {
String why;
const EntryPoints& points = Resolved(why);
if (points.DestroyImage != nullptr && g_EGLFuncs.eglGetCurrentDisplay)
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), static_cast<EGLImageKHR>(target.Image));
}
target = HostFrameTarget{};
}
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
namespace MobileGL::MG_Backend::DirectGLES {
Bool HostFrameTargetSupported(String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
Bool HostFrameTargetCreate(const struct AHardwareBuffer*, HostFrameTarget&, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
void HostFrameTargetBind(const HostFrameTarget&) {}
void HostFrameTargetDestroy(HostFrameTarget& target) { target = HostFrameTarget{}; }
}
#endif
@@ -0,0 +1,52 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
struct AHardwareBuffer;
namespace MobileGL::MG_Backend::DirectGLES {
// THE FRAME THE DISPLAY HOST IS ABOUT TO SCAN OUT, AS A DRAW TARGET.
//
// The display host (the Android side that owns the screen) allocates its frames itself and
// offers each one over a socket as an AHardwareBuffer. That is not a convenience: measured
// on the device, AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this side
// allocated and refuses one the display queue handed over (gralloc BAD_VALUE), so the frames
// a render server can draw into are the ones the host made for it.
//
// What this makes of such a frame: an EGLImage the driver accepts as a renderbuffer, and a
// framebuffer that is the drawing target while that frame is the current one. None of
// Android's window machinery is involved - no ANativeWindow, no SurfaceFlinger - which is
// the point: the host puts the frame on the glass when this side says it is drawn.
struct HostFrameTarget {
void* Image = nullptr; // EGLImageKHR; opaque here so this header stays free of EGL types
Uint Renderbuffer = 0;
Uint Framebuffer = 0;
Uint Width = 0;
Uint Height = 0;
};
// Whether the driver advertises what this is spelled in. False is answered with the missing
// name in `why`, never an abort: a server that cannot take host frames has to be able to
// say so.
Bool HostFrameTargetSupported(String& why);
// Imports `buffer` as a render target. On failure `target` is left empty and `why` says
// which call refused; no GL name is leaked.
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why);
// Binds the target as the current draw framebuffer, called when its frame becomes current.
void HostFrameTargetBind(const HostFrameTarget& target);
// Drops the GL names and the EGLImage. The AHardwareBuffer belongs to the bridge, which
// releases it; this never touches it.
void HostFrameTargetDestroy(HostFrameTarget& target);
}
+90 -7
View File
@@ -3183,6 +3183,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray;
case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle;
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer;
case MG_Pipe::MGPipeResourceTarget::TexExternalOES: return TextureTarget::TextureExternalOES;
default: return TextureTarget::Unknown;
}
}
@@ -10151,7 +10152,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with "
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize,
m_backendTextureId, backendId, buffer ? buffer->GetSize() : (SizeT)0,
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
// A texture that names a window of the buffer needs the range form; the
// whole-buffer forms report offset 0 and the buffer's current size, which
@@ -11282,9 +11283,45 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
// THE TEXTURE'S OWN TARGET, not the attach target: an external texture is attached
// through the GL_TEXTURE_2D upload target (TextureEnumConverter maps 0x8D65 there so the
// attach validates), so glTextureTarget is never 0x8D65 here and the arms below were
// dead - kwin's framebuffers got a storage-less texture and every draw into them failed
// with GL_INVALID_FRAMEBUFFER_OPERATION.
const Bool externalTexture =
glTextureTarget == 0x8D65 || textureObject->GetTarget() == TextureTarget::TextureExternalOES;
if (externalTexture && g_hostFrameRenderbuffer == 0) {
// NOBODY ELSE TAKES THE FRAME ON THIS ROUTE (DirectGLES.h): the compositor
// here never creates a window surface, so this attachment is the first place
// the display host's frame is known to be wanted. A failed take leaves the
// ordinary arm below exactly as it was.
// The numbers size only the fallback pbuffer that is the draw target for the
// moment before the first frame arrives; the frame's real extent comes from the
// display host's own offer. They are the panel's, because that is the surface
// this route exists to fill.
(void)TakeHostFrameForExternalTexture(1440, 3200);
}
if (externalTexture && g_hostFrameRenderbuffer != 0) {
// THE IMPORTED dma-buf IS THE DISPLAY HOST'S FRAME. Measured, and the whole reason
// this branch exists: kwin_wayland in the container imports the display daemon's
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937
// fourcc 0x34324241") and builds a framebuffer over that texture - and a texture
// that arrived this way has NO storage on this side, so the attachment was bound
// as an empty texture and kwin refused its own framebuffer ("framebuffer for
// dmabuf 0 is not complete"). The buffer it imports IS the frame the display
// host handed this process (same memory, same size), and that frame's storage is
// a RENDERBUFFER - a texture argument cannot name it - so the attachment is bound
// to the frame itself. The gate is the measured shape of that case: target
// GL_TEXTURE_EXTERNAL_OES (0x8d65), which nothing else in this stack binds, and a
// frame currently held. Every other attachment - a normal GL_TEXTURE_2D, or any
// moment with no frame - takes the ordinary path below, unchanged.
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
g_hostFrameRenderbuffer);
} else {
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(attachmentObject.GetTextureLevel()));
}
}
} else if (attachmentObject.IsRenderbuffer()) {
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
@@ -11600,6 +11637,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
// nothing here is the whole of the agreement. (The legacy arm does the same.)
return true;
}
// AN IMPORTED dma-buf AT A COLOUR POINT IS THE DISPLAY HOST'S FRAME. The client marks the
// texture it bound an imported EGLImage to as GL_TEXTURE_EXTERNAL_OES (TextureObject2D's
// target), the server never received that dma-buf's storage, and the compositor's
// framebuffer over it was incomplete (measured: status 0x8cd7, every draw
// GL_INVALID_FRAMEBUFFER_OPERATION). It is bound to the host frame canvas instead - the
// storage the display host's frames are copied out of at present - before any twin or
// storage sync, which have nothing to offer a texture with no storage.
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture &&
surface.TextureTarget ==
static_cast<decltype(surface.TextureTarget)>(TextureTarget::TextureExternalOES)) {
if (g_hostFrameRenderbuffer == 0) (void)TakeHostFrameForExternalTexture(1440, 3200);
if (g_hostFrameRenderbuffer != 0) {
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
g_hostFrameRenderbuffer);
return true;
}
}
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
// The record's handle is validated BEFORE the frontend is consulted (N-3): it is
// the untrusted half, the frontend cross-check below is the corroboration, and
@@ -11683,9 +11737,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(surface.Level));
if (glTextureTarget == 0x8D65 && g_hostFrameRenderbuffer == 0) {
// NOBODY ELSE TAKES THE FRAME ON THIS ROUTE (DirectGLES.h): the compositor
// here never creates a window surface, so this attachment is the first place
// the display host's frame is known to be wanted. A failed take leaves the
// ordinary arm below exactly as it was.
// The numbers size only the fallback pbuffer that is the draw target for the
// moment before the first frame arrives; the frame's real extent comes from the
// display host's own offer. They are the panel's, because that is the surface
// this route exists to fill.
(void)TakeHostFrameForExternalTexture(1440, 3200);
}
if (glTextureTarget == 0x8D65 && g_hostFrameRenderbuffer != 0) {
// THE IMPORTED dma-buf IS THE DISPLAY HOST'S FRAME. Measured, and the whole reason
// this branch exists: kwin_wayland in the container imports the display daemon's
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937
// fourcc 0x34324241") and builds a framebuffer over that texture - and a texture
// that arrived this way has NO storage on this side, so the attachment was bound
// as an empty texture and kwin refused its own framebuffer ("framebuffer for
// dmabuf 0 is not complete"). The buffer it imports IS the frame the display
// host handed this process (same memory, same size), and that frame's storage is
// a RENDERBUFFER - a texture argument cannot name it - so the attachment is bound
// to the frame itself. The gate is the measured shape of that case: target
// GL_TEXTURE_EXTERNAL_OES (0x8d65), which nothing else in this stack binds, and a
// frame currently held. Every other attachment - a normal GL_TEXTURE_2D, or any
// moment with no frame - takes the ordinary path below, unchanged.
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
g_hostFrameRenderbuffer);
} else {
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
backendTextureObject->GetBackendTextureId(),
static_cast<GLint>(surface.Level));
}
}
return true;
}
@@ -1879,6 +1879,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
// An external texture's storage is an imported EGLImage; to the driver it is 2D.
case TextureTarget::TextureExternalOES:
return TextureTarget::Texture2D;
default:
return target;
}
@@ -659,7 +659,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
// extension explicitly permits. It is also the only thing that
// exposes glProgramParameteri before GL 4.1.
E_GL_ARB_get_program_binary};
E_GL_ARB_get_program_binary,
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
// missing checkSupported() answered false, EglContext::create() returned nullptr,
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
// rendering context" and quit before drawing a frame.
//
// The three are true statements about this backend too, and the entry points behind
// them are the same ones the DirectGLES half of this list is advertised with: the
// frontend implements all 39 ARB_shader_objects procedures, ARB_vertex_shader's three
// attrib-location entry points, and nothing for ARB_fragment_shader, whose "New
// Procedures and Functions" section reads "None". The shader and program objects live in
// MG_Impl/MG_State, above the backend that draws with them, so a name that is true on
// one backend is true on the other.
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
// NPOT textures: Vulkan puts no power-of-two restriction on an image, its mip chain or
// its samplers, so this is the strongest form of the promise the string makes. Kept
// identical to the DirectGLES group so the two backends do not disagree about what
// MobileGL is.
E_GL_ARB_texture_non_power_of_two,
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
// and this name is what makes them reachable - libepoxy in kwin resolves
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
// The render-server half of the import (fd over the GBM socket, then the server-side
// EGLImage) is the next change; until it lands an imported texture has no storage behind
// it, which is logged once at the entry point rather than left to look like a black frame.
E_GL_OES_EGL_image};
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
+737
View File
@@ -0,0 +1,737 @@
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "GbmFrameChannel.h"
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <cerrno>
#include <cstring>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>
// WHAT THIS FILE IS WRITTEN AGAINST, AND WHAT HAS NOT BEEN RUN.
//
// The other end exists and is tested on this machine: MobileGL/MG_Gbm/GbmHostChannel.cpp dials
// this socket with SOCK_SEQPACKET, reads one message per frame, computes that message's expected
// length from the handle's own two counts, and refuses any other length. Every byte below is
// written to that parser. There is no phone attached to the machine this was written on, so the
// file is compile-verified for aarch64 Android and nothing more: the first run on a device is what
// will say whether the relay in RelayHandle() reads back the handle's bytes exactly as
// AHardwareBuffer_sendHandleToUnixSocket writes them, which is why that path is written to
// survive both layouts the platform's serializer could have (see the note there).
namespace MobileGL::MG_Backend {
namespace {
// THE LAYOUT IS A PROTOCOL, SO ITS SIZES ARE ASSERTED AND NOT ASSUMED. The container
// derives the length of a frame message from these three sizes plus the handle's counts,
// so a field added here, or the packing attribute dropped, would not be a compile error
// over there - it would be every frame skipped with a message about its length.
static_assert(sizeof(GbmFrameOffer) == 10 * sizeof(Uint32), "the offer is ten 32 bit fields on the wire");
static_assert(sizeof(GbmFrameRelease) == 3 * sizeof(Uint32), "the release is three 32 bit fields on the wire");
static_assert(sizeof(GbmNativeHandleHeader) == 3 * sizeof(Int32),
"a native handle begins with a version and two counts, on every Android ABI");
// The container reads one message per frame into a 4 KiB buffer and skips a message that
// does not fit in it (GbmHostMessageMax, GbmHostChannel.cpp), so a message larger than this
// is refused here rather than built and dropped there.
constexpr SizeT kMessageMax = 4096;
// The descriptors one handle may carry, which is the container's own cap (GbmHostMaxFds).
// Every AHardwareBuffer handle this project has seen carries one - the dma-buf - and both
// ends refuse a frame whose handle names more than they can take rather than forward a
// truncated list into a buffer of unknown memory.
constexpr Int kMaxHandleFds = 8;
// How many release messages one call reads before it returns to the frame. A cap and not a
// loop to exhaustion: the messages left behind stay in the socket, whose buffer the kernel
// bounds, and the next frame drains them.
constexpr Int kDrainMax = 64;
// HOW MANY DIALERS THE KERNEL MAY HOLD FOR THIS LISTENER. One container is the protocol,
// but the queue is not the connection: every compositor that was restarted dials once more,
// and a listener that only ever has one slot leaves its queue full for the next one. With
// listen(fd, 1) that is exactly what was measured - connect() from the container answered
// EAGAIN(11) on four attempts out of four after two restarts, and no dialer reached this
// side again for as long as the server lived.
constexpr Int kDialerBacklog = 16;
// The acceptor's poll timeout. It is a wake-up and not a schedule, so it only has to be
// short enough that a container dialling into a socket nothing else touches is taken
// promptly; a dialer that arrives while the thread is waiting wakes it at once.
constexpr Int kAcceptPollMs = 100;
// How long the relay read waits for bytes the send one statement earlier has already put in
// the socket. It exists only so that an invariant broken somewhere else cannot park the
// apply thread; on a local socketpair the wait is zero.
constexpr Int kRelayWaitMs = 2;
// close() may overwrite the errno a caller is in the middle of reporting, and every caller
// here is reporting a failure errno describes.
void CloseQuietly(Int fd) {
if (fd < 0) return;
const Int savedErrno = errno;
::close(fd);
errno = savedErrno;
}
}
GbmFrameChannel::~GbmFrameChannel() { Close(); }
Bool GbmFrameChannel::Open() {
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_listen >= 0) return true;
const char* configured = ::getenv(kGbmFrameSocketEnv);
const char* path = (configured != nullptr && configured[0] != 0) ? configured : kGbmFrameSocketPath;
constexpr SizeT pathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
if (std::strlen(path) >= pathMax) {
MGLOG_E_ONCE("gbm frame channel: the container socket path %s does not fit in sun_path (%zu bytes); no "
"container can be served",
path, pathMax);
return false;
}
// SOCK_SEQPACKET because that is what the container dials with (GbmHostChannel::EnsureOpen
// asks for exactly this), and a stream listener answers it EPROTOTYPE - the container
// prints that as "a socket of a different type". SOCK_NONBLOCK because the accept below
// runs on the apply thread and may not wait for a container that is not there; SOCK_CLOEXEC
// because every process this server starts would otherwise inherit a socket that receives
// the phone's frames.
const Int fd = ::socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) {
MGLOG_E_ONCE("gbm frame channel: socket(AF_UNIX, SOCK_SEQPACKET): %s; the frames are drawn and presented "
"without a container",
std::strerror(errno));
return false;
}
// A node left behind by a server that died is not a reason to refuse a container: bind
// would answer EADDRINUSE for a socket nobody is listening on.
::unlink(path);
struct sockaddr_un address = {};
address.sun_family = AF_UNIX;
// The terminator travels with the name: sun_path is a C string for a pathname socket, and a
// name without one is a name the kernel reads past the end of.
std::memcpy(address.sun_path, path, std::strlen(path) + 1);
if (::bind(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0) {
MGLOG_E_ONCE("gbm frame channel: bind(%s): %s; the frames are drawn and presented without a container",
path, std::strerror(errno));
CloseQuietly(fd);
return false;
}
// connect() to a pathname socket needs write permission on the node, and the container is
// not this process's user, so the umask's usual 0755 would leave it refused for a reason it
// could not see. 0666 is what makes the node reachable by the other half of a split run; a
// chmod that fails is not fatal, because a peer running as this uid still dials it.
if (::chmod(path, 0666) != 0) {
MGLOG_W_ONCE("gbm frame channel: chmod(%s, 0666): %s; a container running as another user cannot dial "
"this socket",
path, std::strerror(errno));
}
if (::listen(fd, kDialerBacklog) != 0) {
MGLOG_E_ONCE("gbm frame channel: listen(%s): %s; the frames are drawn and presented without a container",
path, std::strerror(errno));
::unlink(path);
CloseQuietly(fd);
return false;
}
m_listen = fd;
m_path = path;
// THE ACCEPTOR IS STARTED WITH THE SOCKET AND NOT WITH THE FIRST FRAME. A compositor dials
// at its device creation and keeps the connection, so the dial a frame would have answered
// has already happened by the time that frame is offered: an accept on the offer path is an
// accept that runs after the thing it exists for. The descriptor is copied into the thread
// as an argument - it is the one piece of this object the thread reads without the lock,
// which is what lets Close() join it before closing that descriptor.
m_accepting.store(true, std::memory_order_release);
try {
m_acceptor = std::thread(&GbmFrameChannel::AcceptLoop, this, fd);
} catch (...) {
// A thread that cannot be made is a phone that is out of them, not a reason to refuse
// the frames the container can still be given one at a time.
m_accepting.store(false, std::memory_order_release);
MGLOG_W_ONCE("gbm frame channel: the acceptor thread could not be started; a container is only taken in "
"when a frame is offered, and one that dials between frames waits for the next one");
}
MGLOG_I("gbm frame channel: listening on %s for one container (SOCK_SEQPACKET, one message per frame)", path);
return true;
}
Bool GbmFrameChannel::EnsureRelay() {
if (m_relay[0] >= 0 && m_relay[1] >= 0) return true;
// Both ends are dropped before a new pair is made: half a socketpair is a socket with bytes
// in it that nothing will ever read, and the next handle read from it would be the tail of
// the last one.
DropRelay();
Int pair[2] = {-1, -1};
if (::socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, pair) != 0) {
MGLOG_E_ONCE("gbm frame channel: socketpair for the handle relay: %s; no frame can be offered until one "
"can be made",
std::strerror(errno));
return false;
}
m_relay[0] = pair[0];
m_relay[1] = pair[1];
return true;
}
void GbmFrameChannel::DropRelay() {
CloseQuietly(m_relay[0]);
CloseQuietly(m_relay[1]);
m_relay[0] = -1;
m_relay[1] = -1;
}
Bool GbmFrameChannel::RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
Int* fds, Int& fdCount) {
payloadBytes = 0;
fdCount = 0;
if (!EnsureRelay()) return false;
// WHY A FRAME'S HANDLE GOES THROUGH A SOCKETPAIR OF THIS CHANNEL'S OWN.
//
// The container takes ONE message per frame - the offer, then the handle's counts, then the
// arrays those counts describe, with the descriptors in that same message's SCM_RIGHTS
// control data - and it refuses a message of any other length. Two sends cannot be one
// message on a message-oriented socket, so AHardwareBuffer_sendHandleToUnixSocket, which
// sends a handle as a message of its own, is pointed at a socketpair this channel owns, and
// what comes out of the other end is both halves of what the container wants: the handle's
// own bytes, read here, and its descriptors, as SCM_RIGHTS.
//
// The alternative does not exist: the NDK's <android/hardware_buffer.h> declares no way to
// ask a buffer for its descriptors. AHardwareBuffer_getNativeHandle is a platform API and
// is absent from the NDK's copy of that header (checked against NDK 27.3.13750724 in this
// tree's build-termux toolchain), so this is the one route from an AHardwareBuffer to a
// dma-buf descriptor that an app can take.
const Int code = AHardwareBuffer_sendHandleToUnixSocket(buffer, m_relay[0]);
if (code != 0) {
MGLOG_W_ONCE("gbm frame channel: the frame's handle did not cross the relay socket (rc %d, errno %d); the "
"frame is not offered to the container",
code, errno);
// The relay is dropped rather than kept: a send that failed part way through would
// leave bytes that the next handle's read would take for its own header.
DropRelay();
return false;
}
struct iovec vector = {};
vector.iov_base = payload;
vector.iov_len = capacity;
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
struct msghdr message = {};
message.msg_iov = &vector;
message.msg_iovlen = 1;
message.msg_control = control;
message.msg_controllen = sizeof(control);
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC so a descriptor
// read out of the relay does not survive into a child of this process, which would then hold
// the phone's frame until it died.
ssize_t got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
if (got < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
struct pollfd waiting = {};
waiting.fd = m_relay[1];
waiting.events = POLLIN;
if (::poll(&waiting, 1, kRelayWaitMs) > 0) {
// The kernel rewrites these on every call, so a retry starts from the same state as
// the first attempt.
message.msg_controllen = sizeof(control);
message.msg_flags = 0;
got = ::recvmsg(m_relay[1], &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
}
}
if (got < 0) {
MGLOG_W_ONCE("gbm frame channel: a relayed handle could not be read back (%d: %s); the frame is not "
"offered to the container",
errno, std::strerror(errno));
DropRelay();
return false;
}
if (got == 0) {
// Zero bytes on a connected socket is the end of it. Only this process holds either end
// of this socketpair, so this is a bug being reported rather than a peer going away.
MGLOG_W_ONCE("gbm frame channel: the handle relay is closed; the frame is not offered to the container");
DropRelay();
return false;
}
// The descriptors come out of the control data before anything is decided about the
// message, because they belong to this process from the moment they were received: a path
// that returns without closing them leaks a descriptor per frame.
Int collected = 0;
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
header = CMSG_NXTHDR(&message, header)) {
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS) continue;
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
const Int count = (Int)(bytes / sizeof(Int));
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
for (Int i = 0; i < count; ++i) {
if (collected < kMaxHandleFds) {
fds[collected++] = descriptors[i];
} else {
// More descriptors than a frame can carry: they are this process's now, so they
// are closed here rather than leaked.
CloseQuietly(descriptors[i]);
}
}
}
if ((message.msg_flags & MSG_CTRUNC) != 0) {
MGLOG_W_ONCE("gbm frame channel: a frame's control data did not fit in %zu bytes; the frame is not "
"offered because its descriptor list is incomplete",
sizeof(control));
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
DropRelay();
return false;
}
if ((message.msg_flags & MSG_TRUNC) != 0 || (SizeT)got < sizeof(GbmNativeHandleHeader)) {
// A message longer than a handle can be, or shorter than the three counts every handle
// begins with. Either way it cannot describe a frame, and a relay that has been read
// past the end of one message must not carry the next frame's header: it is recreated.
MGLOG_W_ONCE("gbm frame channel: a relayed handle is %zd bytes and %s; the frame is not offered", got,
((message.msg_flags & MSG_TRUNC) != 0) ? "longer than a handle can be"
: "too short to hold its own counts");
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
DropRelay();
return false;
}
// THE BYTES THAT COME BACK ARE THE HANDLE'S INTEGER ARRAY, NOT A handle AND ITS COUNTS.
// Measured on the phone: parsing the front of this payload as {version, numFds, numInts}
// produced a NumFds taken from the buffer's own integer data, and every frame was refused
// with "its handle carries more descriptors than either end takes" - which is what the
// container saw as a connection that was accepted and then closed without a frame. The two
// counts are known HERE and are not in those bytes: the descriptors came out of the control
// data (collected of them) and the payload is their integer array. So the header is BUILT
// rather than copied, and what leaves this process is a real native_handle_t layout -
// version, numFds, numInts, then the ints - which is exactly what the container's parser
// reads (GbmInternal.h's AHBNativeHandleHeader).
if (payloadBytes + sizeof(GbmNativeHandleHeader) > capacity) {
MGLOG_W_ONCE("gbm frame channel: a handle's integer array does not fit in a frame message with its "
"header; the frame is not offered to the container");
for (Int i = 0; i < collected; ++i) CloseQuietly(fds[i]);
DropRelay();
return false;
}
std::memmove(payload + sizeof(GbmNativeHandleHeader), payload, payloadBytes);
GbmNativeHandleHeader header = {};
header.Version = (Int32)sizeof(GbmNativeHandleHeader);
header.NumFds = (Int32)collected;
header.NumInts = (Int32)(payloadBytes / sizeof(Int32));
std::memcpy(payload, &header, sizeof(header));
payloadBytes += sizeof(header);
fdCount = collected;
return true;
}
// The acceptor. It touches the listener - its own copy of the descriptor, which nothing else
// closes while this loop runs - and, once per dialer, the channel under its lock.
void GbmFrameChannel::AcceptLoop(Int listen) {
while (m_accepting.load(std::memory_order_acquire)) {
struct pollfd waiting = {};
waiting.fd = listen;
waiting.events = POLLIN;
const Int ready = ::poll(&waiting, 1, kAcceptPollMs);
if (ready < 0) {
if (errno == EINTR) continue;
MGLOG_W_ONCE("gbm frame channel: poll on the listening socket failed (%d: %s); no container can be "
"taken in until the socket is opened again",
errno, std::strerror(errno));
return;
}
if (ready == 0) continue;
// accept4, so the connection is close-on-exec and non-blocking from the moment it
// exists: a container that stops reading must not be able to park a send on either
// thread.
const Int fd = ::accept4(listen, nullptr, nullptr, SOCK_CLOEXEC | SOCK_NONBLOCK);
if (fd < 0) {
if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || errno == ECONNABORTED) continue;
MGLOG_W_ONCE("gbm frame channel: accept failed (%d: %s); no container can be taken in until the "
"socket is opened again",
errno, std::strerror(errno));
return;
}
const std::lock_guard<std::mutex> lock(m_mutex);
AdoptLocked(fd);
}
}
void GbmFrameChannel::AdoptLocked(Int fd) {
if (ClientAliveLocked()) {
// ONE CLIENT, which is what the protocol is written for, and a dialer that arrives while
// a live one is connected is closed rather than left connected - it learns it is not the
// one being served instead of holding a connection whose frames nobody reads.
CloseQuietly(fd);
MGLOG_W_ONCE("gbm frame channel: a second peer dialled %s while a container is connected; it is closed "
"and the frames stay with the connected one",
m_path.c_str());
return;
}
m_client = fd;
MGLOG_I("gbm frame channel: a container is connected on %s; every frame the host offers is sent to it as "
"the dma-buf descriptor behind it",
m_path.c_str());
// AND THE FRAME IT DIALED FOR GOES OUT FROM HERE. A compositor creates its GBM device before
// it asks for a surface, so the frame that is the draw target right now was offered before
// this connection existed; a container that is not told about it allocates every buffer of
// its new swapchain locally, and keeps them for the life of the process.
ReofferFrameLocked();
}
Bool GbmFrameChannel::ClientAliveLocked() {
if (m_client < 0) return false;
Uint8 peek = 0;
// MSG_PEEK, because a release waiting in the socket belongs to the render path: this test
// only asks whether the peer is still there, and a release it consumed would be a frame the
// channel believed was still outstanding.
const ssize_t got = ::recv(m_client, &peek, sizeof(peek), MSG_DONTWAIT | MSG_PEEK);
if (got > 0) return true;
if (got == 0) {
// Zero bytes on a connected socket is the end of it, whether the container exited or
// the kernel gave up on it. A compositor that crashed and is being restarted is
// exactly this, and its successor is the dialer standing right here.
MarkDown("the container closed the connection", 0);
return false;
}
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) return true;
MarkDown("recv", errno);
return false;
}
void GbmFrameChannel::MarkDown(const char* what, Int error) {
if (m_client < 0) return;
CloseQuietly(m_client);
m_client = -1;
if (error != 0) {
MGLOG_W("gbm frame channel: the container connection is down: %s failed (%d: %s); the frames are still "
"drawn and presented, and the container stops receiving them until it dials again",
what, error, std::strerror(error));
} else {
MGLOG_W("gbm frame channel: the container connection is down: %s; the frames are still drawn and "
"presented, and the container stops receiving them until it dials again",
what);
}
}
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer) {
if (buffer == nullptr) return false;
// Open() does nothing once the socket exists, so this is an integer test in the common case.
Open();
const std::lock_guard<std::mutex> lock(m_mutex);
// The releases the container has sent since the last frame, which is also how a container
// that died is noticed. Whoever replaced it was taken by the acceptor's own thread the
// moment it dialled: that thread is what makes a container's connect() succeed at all.
PollLocked();
// AND THE FRAME IS REMEMBERED BEFORE IT IS SENT. A container that dials while this call is
// running is one the acceptor hands the frame it finds here to, so the two threads must not
// be able to disagree about which frame is the one being drawn into.
const Uint32 seq = m_nextSeq;
RememberFrameLocked(buffer, offer, seq);
if (m_client < 0) return false;
return SendFrameLocked(buffer, offer, seq, /*counting=*/true);
}
void GbmFrameChannel::RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq) {
if (m_frame == buffer) {
// The host redrawing into the buffer it already handed over: the same frame under a new
// sequence number. Acquiring it again would be a reference this channel takes and never
// gives back.
m_frameOffer = offer;
m_frameSeq = seq;
return;
}
// The reference is this channel's own, and it is what makes the frame a container that dials
// late is owed safe to hold: the backend drops its handle the moment the frame is presented,
// and a frame owed to a dialer arriving in the same millisecond may not be memory that has
// already gone back to the host's pool.
AHardwareBuffer_acquire(buffer);
ReleaseFrameLocked();
m_frame = buffer;
m_frameOffer = offer;
m_frameSeq = seq;
}
void GbmFrameChannel::ReleaseFrameLocked() {
if (m_frame == nullptr) return;
AHardwareBuffer_release(m_frame);
m_frame = nullptr;
m_frameOffer = {};
m_frameSeq = 0;
}
void GbmFrameChannel::DropFrame() {
const std::lock_guard<std::mutex> lock(m_mutex);
ReleaseFrameLocked();
}
void GbmFrameChannel::ReofferFrameLocked() {
if (m_frame == nullptr) return;
// The same frame, under the sequence number it was offered with: it IS the frame that is the
// draw target now, and a second number for it would be this channel claiming the host had
// handed over a frame it never handed over.
MGLOG_I("gbm frame channel: a container dialled after frame %u was offered; it is sent that frame, which is "
"still the draw target",
m_frameSeq);
SendFrameLocked(m_frame, m_frameOffer, m_frameSeq, /*counting=*/false);
}
Bool GbmFrameChannel::SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq,
Bool counting) {
if (m_client < 0) return false;
Uint8 payload[kMessageMax];
SizeT payloadBytes = 0;
Int fds[kMaxHandleFds];
for (Int i = 0; i < kMaxHandleFds; ++i) fds[i] = -1;
Int fdCount = 0;
if (!RelayHandle(buffer, payload, sizeof(payload), payloadBytes, fds, fdCount)) return false;
GbmNativeHandleHeader handle = {};
std::memcpy(&handle, payload, sizeof(handle));
const SizeT tail = ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
const SizeT messageBytes = sizeof(GbmFrameOffer) + sizeof(GbmNativeHandleHeader) + tail;
// EVERY REASON THIS CHANNEL REFUSES A FRAME, IN ONE PLACE. Each of them is a frame the
// container would refuse anyway - it checks the same counts and the same geometry - and a
// frame that is refused here costs nothing: no message, no sequence number, and no entry in
// the count of frames waiting to be released.
const char* refusal = nullptr;
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0) {
refusal = "the host described it with a width, height or stride of zero, and a buffer with no size is "
"not one a compositor can render into";
} else if (handle.NumFds < 1) {
refusal = "its handle carries no descriptor, and a frame without a dma-buf is not one the container "
"can render into";
} else if (handle.NumFds > kMaxHandleFds) {
refusal = "its handle carries more descriptors than either end takes";
} else if (handle.NumInts < 0) {
refusal = "its handle claims a negative number of integers";
} else if ((SizeT)handle.NumInts > kMessageMax / sizeof(Int32)) {
// This is what keeps the message's length arithmetic from wrapping on a 32 bit ABI: a
// handle whose integer array alone is longer than a whole message cannot be carried by
// this protocol whatever the rest of it says, and the check below would otherwise be
// reading a number that had already turned into a small and plausible one.
refusal = "its handle names more integers than a whole message could carry";
} else if (fdCount < handle.NumFds) {
refusal = "fewer descriptors arrived with it than its handle names";
} else if (messageBytes > kMessageMax) {
refusal = "the message it makes is longer than the container reads";
}
Bool offered = false;
if (refusal != nullptr) {
// ONCE, not per frame: a host that describes every frame the same broken way must not
// turn this into a line per frame on the apply thread.
MGLOG_W_ONCE("gbm frame channel: frame %u (%ux%u) is not offered to the container: %s", offer.Index,
offer.Width, offer.Height, refusal);
} else {
// One message, built whole: the offer, then the handle's own three counts, then the two
// arrays the counts describe. What came back from the relay is copied in as far as it
// goes and the remainder is left zero, because the container checks the LENGTH it
// derives from those counts and never reads the arrays themselves - it takes the
// descriptors from the control data and hands the first one to GBM. So a handle whose
// bytes are shorter than its own counts imply is padded rather than refused, and one
// longer than they imply is cut to what the counts say; either way the length the
// container computes is the length it receives.
Uint8 message[kMessageMax] = {};
GbmFrameOffer out = {};
out.Magic = kGbmFrameOfferMagic;
out.Version = kGbmFrameVersion;
out.Seq = seq;
out.Index = offer.Index;
out.Width = offer.Width;
out.Height = offer.Height;
out.StrideBytes = offer.StrideBytes;
out.Format = offer.Format;
out.UsageLo = offer.UsageLo;
out.UsageHi = offer.UsageHi;
std::memcpy(message, &out, sizeof(out));
const SizeT handleBytes = (payloadBytes < sizeof(GbmNativeHandleHeader) + tail)
? payloadBytes
: (sizeof(GbmNativeHandleHeader) + tail);
std::memcpy(message + sizeof(out), payload, handleBytes);
struct iovec vector = {};
vector.iov_base = message;
vector.iov_len = messageBytes;
Uint8 control[CMSG_SPACE(sizeof(Int) * (SizeT)kMaxHandleFds)] = {};
struct msghdr outgoing = {};
outgoing.msg_iov = &vector;
outgoing.msg_iovlen = 1;
outgoing.msg_control = control;
outgoing.msg_controllen = CMSG_SPACE(sizeof(Int) * (SizeT)handle.NumFds);
struct cmsghdr* rights = CMSG_FIRSTHDR(&outgoing);
rights->cmsg_level = SOL_SOCKET;
rights->cmsg_type = SCM_RIGHTS;
rights->cmsg_len = CMSG_LEN(sizeof(Int) * (SizeT)handle.NumFds);
std::memcpy(CMSG_DATA(rights), fds, sizeof(Int) * (SizeT)handle.NumFds);
// MSG_DONTWAIT for the promise at the top of this file, and MSG_NOSIGNAL because a
// server killed by SIGPIPE when the container goes away takes the frames it was drawing
// with it. A full socket, a reset peer and a broken pipe all say the same thing here:
// this connection can no longer be trusted with a frame, so it is closed and the next
// container to dial is served.
const ssize_t sent = ::sendmsg(m_client, &outgoing, MSG_DONTWAIT | MSG_NOSIGNAL);
if (sent != (ssize_t)messageBytes) {
MarkDown("sendmsg", (sent < 0) ? errno : 0);
} else {
// A frame offered for the first time is the next sequence number and one more frame
// outstanding. The same frame sent again to a container that has just dialled is
// neither: the host handed over nothing new, and a count of frames waiting to be
// released may not be moved by a message that describes one already in it.
if (counting) {
m_nextSeq = seq + 1;
++m_offered;
++m_outstanding;
}
offered = true;
// The first offer is the one worth a line, and it carries the host's own numbers:
// the format in particular is the field the two ends could disagree about without
// either failing, so the value the host actually sends is put where a run on a
// device can be read afterwards.
MGLOG_I_ONCE("gbm frame channel: the first frame is offered to the container (seq %u index %u %ux%u "
"stride %u format 0x%08x)",
out.Seq, out.Index, out.Width, out.Height, out.StrideBytes, out.Format);
MGLOG_D("gbm frame channel: frame seq=%u index=%u offered (%zu bytes, %d descriptor(s), %llu "
"outstanding)",
out.Seq, out.Index, messageBytes, handle.NumFds, (unsigned long long)m_outstanding);
}
}
// The kernel took its own copies of these when the message was sent, and one that was not
// sent is not the container's to hold: every path closes what the relay handed over.
for (Int i = 0; i < fdCount; ++i) CloseQuietly(fds[i]);
return offered;
}
void GbmFrameChannel::Poll() {
const std::lock_guard<std::mutex> lock(m_mutex);
PollLocked();
}
void GbmFrameChannel::PollLocked() {
if (m_client < 0) return;
for (Int drained = 0; drained < kDrainMax; ++drained) {
GbmFrameRelease release = {};
struct iovec vector = {};
vector.iov_base = &release;
vector.iov_len = sizeof(release);
struct msghdr message = {};
message.msg_iov = &vector;
message.msg_iovlen = 1;
// recvmsg and not recv, for MSG_TRUNC: a message longer than a release can only be a
// peer speaking something else, and a release read out of the first twelve bytes of it
// would name a frame nobody released.
const ssize_t got = ::recvmsg(m_client, &message, MSG_DONTWAIT);
if (got < 0) {
if (errno == EINTR) continue;
// Nothing said, which is what a container that is between frames looks like.
if (errno == EAGAIN || errno == EWOULDBLOCK) return;
MarkDown("recvmsg", errno);
return;
}
if (got == 0) {
// Zero bytes on a connected socket is the end of it, whether the container exited or
// the kernel gave up on it. Its replacement is taken by the next frame.
MarkDown("the container closed the connection", 0);
return;
}
if ((message.msg_flags & MSG_TRUNC) != 0 || got != (ssize_t)sizeof(release)) {
// The message is skipped, not the connection: on a message-oriented socket a
// message too long for the buffer is consumed whole, so the next one is still a
// message rather than the tail of this one.
MGLOG_W_ONCE("gbm frame channel: the container sent a message that is not a %zu byte release; it is "
"skipped",
sizeof(release));
continue;
}
if (release.Magic != kGbmFrameReleaseMagic) {
MGLOG_W_ONCE("gbm frame channel: a message from the container begins with 0x%08x rather than 'MGGR'; "
"it is skipped",
release.Magic);
continue;
}
if (release.Version != kGbmFrameVersion) {
// Refused rather than acted on: the version is what says how the rest of the message
// is laid out, and a release read with the wrong layout names a frame the container
// did not mean.
MGLOG_E_ONCE("gbm frame channel: the container speaks protocol version %u where this server speaks "
"version %u; its releases are refused until the two agree",
release.Version, (Uint32)kGbmFrameVersion);
continue;
}
++m_released;
if (m_outstanding > 0) --m_outstanding;
// The first release is the only one worth a line: it is the proof that the container
// took the descriptor and its compositor retired the frame, where every later one says
// the same thing sixty times a second.
MGLOG_I_ONCE("gbm frame channel: the container is releasing frames (first: seq %u)", release.Seq);
}
// Anything past this cap stays in the socket, whose buffer the kernel bounds, and goes out
// with the next frame: a peer that says nothing but releases may not spend the apply thread
// here.
}
void GbmFrameChannel::Close() {
// THE ACCEPTOR IS STOPPED AND JOINED FIRST, and outside the lock. It may be inside that
// lock when this is called - a teardown that waited for it while holding it would wait for
// ever - and it polls the listening descriptor, which may not be closed under it: the next
// open() in this process hands out the descriptor number a moment later, and the thread
// would then be polling somebody else's socket.
m_accepting.store(false, std::memory_order_release);
if (m_acceptor.joinable()) m_acceptor.join();
const std::lock_guard<std::mutex> lock(m_mutex);
// The frame is this channel's reference and is released with it: the backend has already
// dropped its own by the time a teardown here is a teardown of the frame as well.
ReleaseFrameLocked();
// The connection is closed quietly rather than marked down: this is a teardown, not a
// failure, and a warning about it would be noise in a log read for the failures.
CloseQuietly(m_client);
m_client = -1;
DropRelay();
if (m_listen >= 0) {
// The node is this channel's to remove only while it still IS this channel's socket: a
// server that was restarted has bound the same path with a socket of its own, and a
// teardown that unlinked it would take the successor's node away with it. st_dev and
// st_ino against the listening descriptor is the cheapest way to ask, and a check that
// fails only leaves a name behind that the next Open() unlinks anyway.
struct stat node = {};
struct stat socket = {};
if (!m_path.empty() && ::stat(m_path.c_str(), &node) == 0 && ::fstat(m_listen, &socket) == 0 &&
node.st_dev == socket.st_dev && node.st_ino == socket.st_ino) {
::unlink(m_path.c_str());
}
CloseQuietly(m_listen);
m_listen = -1;
}
m_path.clear();
// The counters are history and stay: a caller that logs Outstanding() after a teardown is
// asking what happened, and IsUp() is what answers what is connected now.
}
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
namespace MobileGL::MG_Backend {
// A build that is not the Android render server has no AHardwareBuffer to offer and no
// container beside it. The class exists so that callers do not have to be compiled
// differently, and every entry point answers the way an absent container does.
GbmFrameChannel::~GbmFrameChannel() = default;
Bool GbmFrameChannel::Open() { return false; }
Bool GbmFrameChannel::OfferFrame(struct AHardwareBuffer*, const HostFrameOffer&) { return false; }
void GbmFrameChannel::Poll() {}
void GbmFrameChannel::Close() {}
}
#endif
// End of Source File Header
+281
View File
@@ -0,0 +1,281 @@
// MobileGL - MobileGL/MG_Backend/GbmFrameChannel.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include "HostFrameBridge.h"
struct AHardwareBuffer;
namespace MobileGL::MG_Backend {
// THE FRAME THE HOST HANDED OVER, HANDED ON TO THE CONTAINER AS A dma-buf.
//
// The display host owns a pool of AHardwareBuffers: it allocates them, this side imports the
// one it offers as a draw target (HostFrameBridge, DirectGLES::HostFrameTarget), and the host
// puts it on the glass when this side answers that it is drawn. The Wayland compositor in the
// Linux container has to render into THOSE SAME frames - it runs this project's own GBM, and a
// gbm_bo_create there answers with the phone's frame instead of a dma-heap allocation of its
// own - so every frame this side takes has to reach the container as that frame's dma-buf
// descriptor. That, and the release that comes back, is all this channel carries.
//
// The container's end is MobileGL/MG_Gbm/GbmHostChannel.cpp. It reads exactly the bytes
// declared below and answers with a release once its compositor no longer reads the frame.
// The three shapes ARE that file's GbmFrameOffer/GbmFrameRelease and GbmInternal.h's
// AHBNativeHandleHeader, field for field and packing included. They are restated here rather
// than shared because the two ends are built into different libraries from different build
// configurations: MG_Backend is the Android render server and never sees libgbm's headers,
// while MG_Gbm is built for the container and never sees Android's. A change on either side
// is a protocol change, and the magic is what makes a mismatched pair fail loudly instead of
// being read as plausible numbers.
//
// ONE MESSAGE PER FRAME, on SOCK_SEQPACKET, and that is part of the protocol rather than an
// implementation detail: an offer and its handle only mean anything together, and the message
// boundary is what lets the reader take the two as one thing. The container derives the
// length of the message it expects from the handle's own two counts and refuses any other
// length, so this end sends the offer immediately followed by the handle's counts and its two
// arrays, with the descriptors in the SCM_RIGHTS control data of that same sendmsg.
//
// The channel is OPTIONAL, and that is the rule the implementation is built around: a
// container that is not running, has died, or has stopped reading may cost the render path a
// syscall or two, and may never block it, fail it, or grow without bound. Every entry point
// below is non-blocking and answers silence with silence.
inline constexpr Uint32 kGbmFrameOfferMagic = 0x4647474Du; // 'M','G','G','F' in memory order
inline constexpr Uint32 kGbmFrameReleaseMagic = 0x5247474Du; // 'M','G','G','R' in memory order
inline constexpr Uint32 kGbmFrameVersion = 1u;
// WHERE THE CONTAINER DIALS. /data/local/tmp is the one directory both halves of a split run
// can be pointed at on a device, and the default lives on this end because this end is the
// server. The container's compiled-in default is /run/mobilegl-gbm.sock - the same protocol
// with the path each side can reach unaided - so a deployment that puts the two in different
// mount namespaces sets MOBILEGL_GBM_SOCKET on both; it is read here and in
// GbmHostChannelCreate, from the same variable, for that reason.
inline constexpr const char* kGbmFrameSocketPath = "/data/local/tmp/mobilegl-gbm.sock";
inline constexpr const char* kGbmFrameSocketEnv = "MOBILEGL_GBM_SOCKET";
// One frame the host offered, described for the container. Width, Height, StrideBytes and
// Format are the host's own numbers for the frame (HostFrameOffer), carried through
// unchanged: they describe the memory the descriptor points at, and this end is not the place
// where a number the other two ends agreed on gets rewritten.
struct GbmFrameOffer {
Uint32 Magic;
Uint32 Version;
Uint32 Seq; // this channel's, one per frame offered
Uint32 Index; // the host's own buffer index, echoed so the phone can match a frame
Uint32 Width;
Uint32 Height;
Uint32 StrideBytes;
Uint32 Format;
Uint32 UsageLo;
Uint32 UsageHi;
} __attribute__((packed));
// What the container says once it is done with a frame. It may arrive late, it may never
// arrive, and both are cheap here: the channel keeps a count and no per-frame record (see
// Outstanding below), so nothing is leaked and nothing grows when one is missed.
struct GbmFrameRelease {
Uint32 Magic;
Uint32 Version;
Uint32 Seq;
} __attribute__((packed));
// THE OTHER DIRECTION: A dma-buf THE CONTAINER WANTS THIS SIDE TO BACK A TEXTURE WITH.
//
// The compositor's scene reaches the display daemon's dma-bufs through glEGLImageTargetTexture2DOES
// (MG_Impl/GLImpl/Exporting/Definitions.cpp, MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp both
// name this half as the next change). eglCreateImageKHR has already held a dup() of every plane's
// descriptor since the image was made; what was missing is the render server's half - telling THIS
// side which texture that dma-buf backs, over the one channel that can carry a descriptor across the
// container boundary. This message is that half: the client names the texture target and the
// texture's own name, describes the buffer exactly as drmModeAddFB2 would, and the plane descriptors
// ride in the SCM_RIGHTS control data of the same sendmsg. One message per import, as SEQPACKET
// requires, so a descriptor and its description are never read apart.
//
// The reply is what the client waits for before a framebuffer is allowed to be complete over the
// texture: Ok=0 means this side could not make the import real, and the client keeps treating the
// texture as having no storage rather than letting it render into nothing.
inline constexpr Uint32 kGbmImportRequestMagic = 0x4947474Du; // 'M','G','G','I' in memory order
inline constexpr Uint32 kGbmImportReplyMagic = 0x52474749u; // 'I','G','G','R' in memory order
inline constexpr Uint32 kGbmImportVersion = 1u;
inline constexpr Uint32 kGbmImportMaxPlanes = 4u;
struct GbmImportRequest {
Uint32 Magic;
Uint32 Version;
Uint32 Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
Uint32 TexName; // the CLIENT's texture name; this side resolves its twin
Uint32 Width;
Uint32 Height;
Uint32 FourCC;
Uint32 PlaneCount; // 1..kGbmImportMaxPlanes; the descriptors are the message's control data
Int32 Stride[kGbmImportMaxPlanes];
Int32 Offset[kGbmImportMaxPlanes];
Uint64 Modifier[kGbmImportMaxPlanes];
Uint32 HasModifier[kGbmImportMaxPlanes];
} __attribute__((packed));
struct GbmImportReply {
Uint32 Magic;
Uint32 Version;
Uint32 Ok;
Uint32 Reserved;
} __attribute__((packed));
// An AHardwareBuffer native handle as it appears on the wire: the platform's native_handle_t
// (version, then the two counts, then the descriptor array followed by the integer array),
// written by AHardwareBuffer_sendHandleToUnixSocket and read by the container's
// AHBNativeHandleHeader. The counts are 32 bit on every Android ABI, which is why they are
// spelled out here rather than taken from a platform header this library cannot include.
struct GbmNativeHandleHeader {
Int32 Version;
Int32 NumFds;
Int32 NumInts;
} __attribute__((packed));
// ONE CONTAINER, ONE LISTENING SOCKET, TWO THREADS: the render path's, and an acceptor's.
//
// A compositor dials ONCE - when it creates its GBM device - and keeps that connection for the
// life of the process, while this side offers a frame per drawn frame. An accept that only ran
// inside OfferFrame therefore ran after the dial it was supposed to answer, and a dialer that
// nobody took kept the listener's queue full for every later one. That is measured on the
// phone rather than argued: with accept() only on the offer path, connect() from the container
// answered EAGAIN(11) on four attempts out of four while the node sat there listening.
//
// So the accept has a thread of its own, started by Open() and stopped by Close(), and it
// adopts what it takes on the spot - including sending it the frame the host last handed over,
// because a compositor's first allocation is the one its swapchain is built from. The
// connected socket, the frame that is remembered, the relay and the counters are shared between
// the two threads and guarded by the one mutex below; both entry points are non-blocking, so
// neither thread can park the other for longer than a syscall.
class GbmFrameChannel {
public:
GbmFrameChannel() = default;
GbmFrameChannel(const GbmFrameChannel&) = delete;
GbmFrameChannel& operator=(const GbmFrameChannel&) = delete;
~GbmFrameChannel();
// Creates the listening socket if it is not there yet and starts the acceptor, and does
// nothing otherwise. It never waits for a container: the socket is bound, a backlog is
// given to listen(), and the acceptor takes whoever dials from then on - whether or not a
// frame is being drawn. A caller that wants the container to find the socket before the
// first frame - the container dials once at its own device creation and retries about once
// a second - can call this when the host-framed surface is initialized, beside
// HostFrameBridge::Open.
Bool Open();
// Offers one frame the host just handed over, as the dma-buf descriptor behind it.
// False means the frame did not go out, for any of the reasons this channel treats as
// ordinary: no container is connected, the container just died, or the frame could not be
// described. None of them is the caller's problem and none of them is fatal - the frame
// is drawn and presented exactly as it would have been without a channel at all.
Bool OfferFrame(struct AHardwareBuffer* buffer, const HostFrameOffer& offer);
// Reads whatever the container has said since the last call and marks the channel down if
// the connection has ended. OfferFrame already does this once per frame, so a caller only
// needs it if it wants releases drained on a path that has no new frame in it.
void Poll();
// The frame the host handed over is gone - it was presented, or it was replaced - and this
// channel may no longer hold it or offer it to anybody. THE CHANNEL ITSELF STAYS OPEN, and
// that is the point of the call: the listening socket, its node and the container's
// connection outlive every frame, and a channel that was closed and reopened per frame took
// the node away sixty times a second - a container dialling at the wrong moment got ENOENT
// for a socket that was there a millisecond earlier and would be there a millisecond later.
void DropFrame();
// True while a container is connected and the frames are being offered to it.
Bool IsUp() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_client >= 0;
}
// How many frames have been offered and not released, as a count of messages and not a
// record of them. It is here to be logged, and it is deliberately the only per-frame state
// the channel keeps: a release that never arrives costs a number that is too high, and
// nothing else, which is what makes a container that stops reading harmless.
Uint64 Outstanding() const {
const std::lock_guard<std::mutex> lock(m_mutex);
return m_outstanding;
}
// Closes the connection, the listening socket and the node it created. Idempotent, safe
// from a destructor, and safe to call while a container is connected: the container sees
// the connection end and goes back to allocating its frames locally, which is the state it
// starts in.
void Close();
private:
// The socketpair a frame's handle is handed to and read back from. Recreated on demand,
// because the one call the NDK offers for a buffer's descriptors sends them as a message
// of its own and the container takes one message per frame - see the note in the .cpp.
Bool EnsureRelay();
void DropRelay();
Bool RelayHandle(struct AHardwareBuffer* buffer, Uint8* payload, SizeT capacity, SizeT& payloadBytes,
Int* fds, Int& fdCount);
// Takes the one container the protocol allows, and closes anyone else who dials while a
// LIVE one is connected rather than leaving them to believe they are being served. A
// connection whose peer has exited is not a live one: it is closed here and its dialer's
// replacement is taken, which is what a compositor that crashed and was restarted is.
void AdoptLocked(Int fd);
// True while the connected container is still there. recvmsg with MSG_PEEK is what asks:
// zero bytes is the peer's close, EAGAIN is a peer that is between frames, and a release
// waiting in the socket is a peer that is alive - peeked at rather than read, because the
// render path is the one that spends releases.
Bool ClientAliveLocked();
// Takes the one container the protocol allows out of whoever dialled.
void AcceptLoop(Int listen);
// Sends the frame that is the draw target to a container that has just connected.
void ReofferFrameLocked();
// Remembers the frame the host last handed over, holding a reference to it so that a
// container which dials after the offer can still be given it. The previous one is
// released here; nothing else in the channel holds a frame.
void RememberFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq);
void ReleaseFrameLocked();
// The offer itself: the handle relayed out of the buffer, the message built, one sendmsg.
// A frame that has already been offered once is sent again with the sequence number it was
// offered under and does not move the counters, because it is the same frame and not a new
// one.
Bool SendFrameLocked(struct AHardwareBuffer* buffer, const HostFrameOffer& offer, Uint32 seq, Bool counting);
// The body of Poll(), for a caller that is already holding the lock.
void PollLocked();
// Closes the connection and says why, once. The channel stays open for the next container.
void MarkDown(const char* what, Int error);
Int m_listen = -1;
Int m_client = -1;
Int m_relay[2] = {-1, -1};
String m_path;
// Bumped only for a frame that actually went out, so a sequence number is never reused and
// a release can never be read as belonging to a frame of an earlier connection.
Uint32 m_nextSeq = 0;
Uint64 m_offered = 0;
Uint64 m_released = 0;
Uint64 m_outstanding = 0;
// The frame the host handed over most recently, held by this channel's own reference for as
// long as it is the draw target: it is what a container that dials late is sent.
struct AHardwareBuffer* m_frame = nullptr;
HostFrameOffer m_frameOffer = {};
Uint32 m_frameSeq = 0;
// The acceptor, and the flag that ends it. The listening descriptor is handed to the thread
// as its own copy, so it is the only thing in here that thread reads without the lock - and
// Close() joins the thread before it closes that descriptor.
std::thread m_acceptor;
std::atomic<Bool> m_accepting{false};
// Guards every member above, from both threads. Mutable because the two const questions a
// caller asks about the channel - is a container connected, how many frames are outstanding
// - are answers that have to be read under it.
mutable std::mutex m_mutex;
};
}
+189
View File
@@ -0,0 +1,189 @@
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "HostFrameBridge.h"
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#include <cerrno>
#include <cstring>
#include <poll.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
namespace MobileGL::MG_Backend {
namespace {
Bool ReadExactly(Int fd, void* out, SizeT n) {
Uint8* at = static_cast<Uint8*>(out);
while (n > 0) {
const ssize_t got = ::read(fd, at, n);
if (got <= 0) {
if (got < 0 && errno == EINTR) continue;
return false;
}
at += got;
n -= static_cast<SizeT>(got);
}
return true;
}
Bool WriteExactly(Int fd, const void* in, SizeT n) {
const Uint8* at = static_cast<const Uint8*>(in);
while (n > 0) {
const ssize_t put = ::write(fd, at, n);
if (put <= 0) {
if (put < 0 && errno == EINTR) continue;
return false;
}
at += put;
n -= static_cast<SizeT>(put);
}
return true;
}
}
HostFrameBridge::~HostFrameBridge() { Close(); }
Bool HostFrameBridge::Open(const char* path, Int acceptTimeoutMs, String& why) {
Close();
const char* name = (path != nullptr && path[0] != 0) ? path : kHostFrameSocket;
m_listen = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (m_listen < 0) {
why = std::format("socket(AF_UNIX): {}", std::strerror(errno));
return false;
}
sockaddr_un address{};
address.sun_family = AF_UNIX;
if (std::strlen(name) >= sizeof(address.sun_path)) {
why = std::format("the socket name is longer than the kernel takes: {}", name);
Close();
return false;
}
// "@name" is the abstract namespace: sun_path[0] is NUL and the name follows it.
// Nothing is created on disk, so nothing is left behind and no file permission stands
// between two apps that may not write each other's directories.
if (name[0] == '@') {
address.sun_path[0] = 0;
std::strncpy(address.sun_path + 1, name + 1, sizeof(address.sun_path) - 2);
} else {
std::strncpy(address.sun_path, name, sizeof(address.sun_path) - 1);
// A leftover socket file from a server that died is not a reason to refuse the host.
::unlink(name);
}
if (::bind(m_listen, reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0) {
why = std::format("bind({}): {}", name, std::strerror(errno));
Close();
return false;
}
if (::listen(m_listen, 1) != 0) {
why = std::format("listen({}): {}", name, std::strerror(errno));
Close();
return false;
}
pollfd waiting{};
waiting.fd = m_listen;
waiting.events = POLLIN;
const Int ready = ::poll(&waiting, 1, acceptTimeoutMs);
if (ready <= 0) {
why = (ready == 0) ? std::format("no display host dialled {} within {} ms", name, acceptTimeoutMs)
: std::format("poll({}): {}", name, std::strerror(errno));
Close();
return false;
}
m_fd = ::accept(m_listen, nullptr, nullptr);
if (m_fd < 0) {
why = std::format("accept({}): {}", name, std::strerror(errno));
Close();
return false;
}
MGLOG_I("host frame bridge: a display host is on %s", name);
return true;
}
Bool HostFrameBridge::Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why) {
*buffer = nullptr;
offer = HostFrameOffer{};
if (m_fd < 0) {
why = "the bridge is not open";
return false;
}
AHardwareBuffer* received = nullptr;
if (AHardwareBuffer_recvHandleFromUnixSocket(m_fd, &received) != 0 || received == nullptr) {
why = "the host stopped offering frames, or the handle did not cross";
return false;
}
if (!ReadExactly(m_fd, &offer, sizeof(offer)) || offer.Magic != kHostFrameMagic) {
// The handle is already this side's by the time this fails, so it goes back before
// the failure does: a reference held past the connection starves the host's pool.
AHardwareBuffer_release(received);
why = std::format("the frame's description did not follow its handle (magic 0x{:08x})", offer.Magic);
return false;
}
*buffer = received;
return true;
}
Bool HostFrameBridge::Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why) {
if (m_fd < 0) {
why = "the bridge is not open";
return false;
}
HostFrameAck ack{};
ack.Magic = kHostFrameMagic;
ack.Version = kHostFrameVersion;
ack.Index = offer.Index;
ack.Imported = imported ? 1u : 0u;
ack.Drawn = drawn ? 1u : 0u;
ack.FenceOk = fenceOk ? 1u : 0u;
if (!WriteExactly(m_fd, &ack, sizeof(ack))) {
why = std::format("the answer to frame {} did not go out: {}", offer.Index, std::strerror(errno));
return false;
}
// The host answers with its own view of the pixels. It is what closes the round trip, so
// it is read here and kept rather than left in the socket for the next frame to trip over.
m_lastSeen = HostFrameSeen{};
if (!ReadExactly(m_fd, &m_lastSeen, sizeof(m_lastSeen)) || m_lastSeen.Magic != kHostFrameMagic) {
why = std::format("frame {} was answered but the host's readback did not arrive", offer.Index);
m_lastSeen = HostFrameSeen{};
return false;
}
return true;
}
void HostFrameBridge::Close() {
if (m_fd >= 0) ::close(m_fd);
if (m_listen >= 0) ::close(m_listen);
m_fd = -1;
m_listen = -1;
}
}
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
namespace MobileGL::MG_Backend {
HostFrameBridge::~HostFrameBridge() = default;
Bool HostFrameBridge::Open(const char*, Int, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
Bool HostFrameBridge::Acquire(struct AHardwareBuffer**, HostFrameOffer&, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
Bool HostFrameBridge::Complete(const HostFrameOffer&, Bool, Bool, Bool, String& why) {
why = "host frames are a disaggregated Android path";
return false;
}
void HostFrameBridge::Close() {}
}
#endif
+103
View File
@@ -0,0 +1,103 @@
// MobileGL - MobileGL/MG_Backend/HostFrameBridge.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
struct AHardwareBuffer;
namespace MobileGL::MG_Backend {
// THE DISPLAY HOST'S FRAMES, AS THEY ARRIVE.
//
// The Android side that owns the screen allocates its frames itself and offers each one over
// an AF_UNIX socket: the handle travels ahead of a fixed-size description on the same
// connection, and the host then waits for this side to say the frame is drawn before it puts
// it on the glass. Why the host allocates rather than this side asking for the display's own
// buffers is measured, not chosen: AHardwareBuffer_createFromHandle accepts an
// AHardwareBuffer this process allocated and refuses one the display queue handed over
// (gralloc BAD_VALUE), so the only frames a render server can draw into are the ones the host
// made for it.
//
// The three message shapes below ARE the host's (its ahb_bridge.h), field for field and
// packing included. They are restated here rather than shared because the two ends are built
// by different projects; a change on either side is a protocol change, and the magic is what
// makes a mismatched pair fail loudly instead of being read as plausible numbers.
inline constexpr Uint32 kHostFrameMagic = 0x4D474C41u; // the host spells it "ALGM" in memory
inline constexpr Uint32 kHostFrameVersion = 1u;
// AN ABSTRACT NAME, not a path: the two ends are separate apps, and neither may create a
// socket file in /data/local/tmp (its directory grants others traverse only). An abstract
// name lives in the kernel namespace instead of the filesystem, so there is no file
// permission to get wrong - the same reason this project names its own endpoints @mgl-*.
inline constexpr const char* kHostFrameSocket = "@mobilegl-host-frame";
struct HostFrameOffer {
Uint32 Magic;
Uint32 Version;
Uint32 Index;
Uint32 Width;
Uint32 Height;
Uint32 StrideBytes;
Uint32 Format;
Uint32 UsageLo;
Uint32 UsageHi;
char Note[64];
} __attribute__((packed));
struct HostFrameAck {
Uint32 Magic;
Uint32 Version;
Uint32 Index;
Uint32 Imported;
Uint32 Drawn;
Uint32 FenceOk;
Uint8 Color[4];
char Note[64];
} __attribute__((packed));
struct HostFrameSeen {
Uint32 Magic;
Uint32 Version;
Uint32 Index;
Uint32 Locked;
Uint8 Observed[4];
Uint8 ObservedCorner[4];
char Note[64];
} __attribute__((packed));
// One host connection at a time, which is what the host offers: it dials, hands over a frame,
// waits for the answer, and hands over the next.
class HostFrameBridge {
public:
HostFrameBridge() = default;
HostFrameBridge(const HostFrameBridge&) = delete;
HostFrameBridge& operator=(const HostFrameBridge&) = delete;
~HostFrameBridge();
// Listens on `path` (the well-known name by default) and waits for the host. A host that
// never dials is answered with false and a reason, never an abort.
Bool Open(const char* path, Int acceptTimeoutMs, String& why);
// Takes the next frame. Ownership of the returned AHardwareBuffer is the caller's, who
// must hand it back through ReleaseOwned() once the frame is off the glass.
Bool Acquire(struct AHardwareBuffer** buffer, HostFrameOffer& offer, String& why);
// Tells the host what became of the frame it offered, and reads back what the host's own
// view of the pixels holds. `drawn` is what the host waits for before it presents.
Bool Complete(const HostFrameOffer& offer, Bool imported, Bool drawn, Bool fenceOk, String& why);
// The readback the host returned for the last completed frame, as the host reported it.
const HostFrameSeen& LastSeen() const { return m_lastSeen; }
void Close();
private:
Int m_fd = -1;
Int m_listen = -1;
HostFrameSeen m_lastSeen{};
};
}
+4
View File
@@ -14,6 +14,7 @@
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/PipeApply.h>
#include <MG_Pipe/PipeMutation.h> // MGPipeForgetAllPublications, at the end of ShutdownSplitRoles
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/FatalFunnel.h> // InstallPipeSessionFailHook, installed at step 0 below
#include <MG_Remote/Server/ServerLoop.h>
@@ -398,6 +399,9 @@ namespace MobileGL::MG_Backend {
// and every later split bring-up in the process fails at CreateBackend's m_backend!=null
// guard.
MG_Remote::Server::ServerLoopInstance().Stop();
// The server's twins died with the session; the frontend objects that are destroyed after
// this (the GL context's) have nothing left on the other side to delete.
MG_Pipe::MGPipeForgetAllPublications();
}
#endif
+663
View File
@@ -0,0 +1,663 @@
// MobileGL - MobileGL/MG_Gbm/GbmApi.cpp
// SPDX-License-Identifier: LGPL-3.0-only
//
// The extern "C" surface: every entry point gbm.h declares, and nothing else.
//
// This file is a translation layer, and it is kept that way on purpose. Each entry
// point does three things - check the pointers it was handed, take the device's one
// mutex for the whole call, and hand the work to GbmDevice.cpp - so the concurrency
// rule ("one mutex per device, taken once at the API boundary, never inside the
// allocation code") is visible in one place instead of being spread over the
// backends. A missing lock is a bug that only shows up in the compositor, so the
// boilerplate is deliberately identical in every function.
//
// Two conventions are observable from outside and are stated here because they are
// the ones a caller can get wrong:
//
// * gbm_bo_get_fd and gbm_bo_get_fd_for_plane return a DUP the caller owns. The
// caller closes what it was given; the buffer's own descriptor is closed once,
// when the buffer is destroyed, and never by a caller's close.
// * Nothing here throws and nothing here aborts. A failure is reported the way
// GBM reports failures: null or -1 with errno set, so that the strerror(errno)
// a caller prints describes the failure that actually happened.
#include "GbmInternal.h"
#include <cerrno>
using namespace MobileGL;
using namespace MobileGL::MG_Gbm;
namespace
{
// The one mutex that guards a record, reached through the device that owns it.
// Null means the pointer was not one this library handed out, and every entry
// point turns that into its own failure answer rather than dereferencing it.
//
// A buffer or surface with no owner cannot exist while the library is used
// correctly - the device destroys everything it still holds before it goes away
// - so null here is a defence against a stale pointer rather than a state the
// code produces.
std::mutex* MutexOf(struct gbm_device* device) noexcept
{
return device != nullptr ? &device->Impl.Mutex : nullptr;
}
std::mutex* MutexOf(struct gbm_bo* bo) noexcept
{
return bo != nullptr ? MutexOf(bo->Impl.Owner) : nullptr;
}
std::mutex* MutexOf(struct gbm_surface* surface) noexcept
{
return surface != nullptr ? MutexOf(surface->Impl.Owner) : nullptr;
}
} // namespace
// ---------------------------------------------------------------------------
// Device
// ---------------------------------------------------------------------------
extern "C" struct gbm_device* gbm_create_device(int fd)
{
// No lock to take: the device being built is not reachable by any other thread
// until this call returns it.
return GbmDeviceCreate(fd);
}
extern "C" void gbm_device_destroy(struct gbm_device* gbm)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
return;
}
// The lock is taken and deliberately never released: the mutex lives inside the
// record this call deletes, and unlocking a destroyed mutex is worse than
// leaving a dead one locked. Any other thread touching this device after its
// owner declared it dead is already outside what GBM can be asked to survive.
mutex->lock();
GbmDeviceDestroy(*gbm);
}
extern "C" int gbm_device_get_fd(struct gbm_device* gbm)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return -1;
}
std::lock_guard<std::mutex> guard(*mutex);
// The device's own dup of the fd it was created with, or -1 when it was created
// without one and no render node could be opened. That is not an error state:
// the self-allocated backend never talks to the node, so a device can be fully
// usable and still have no descriptor to report.
return gbm->Impl.DeviceFd;
}
extern "C" const char* gbm_device_get_backend_name(struct gbm_device* gbm)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
// The name says which memory this device hands out ("mobilegl-dma-heap" or
// "mobilegl-memfd"), because the callers that read it - a compositor logging its
// backend, a peer test recording what it got - are asking exactly that. The
// string lives in the device and outlives every call that can read it.
return gbm->Impl.BackendName.c_str();
}
extern "C" int gbm_device_is_format_supported(struct gbm_device* gbm, uint32_t format, uint32_t flags)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
// Every buffer this device produces is linear, mappable and shareable, so the
// usage flags do not narrow the answer to any of the usual combinations -
// including GBM_BO_USE_SCANOUT, which a linear dma-buf from the system heap
// satisfies. The one request the backend cannot honour is protected memory.
if ((flags & GBM_BO_USE_PROTECTED) != 0)
{
return 0;
}
return GbmFormatIsSupported(format) ? 1 : 0;
}
extern "C" int gbm_device_get_format_modifier_plane_count(struct gbm_device* gbm, uint32_t format, uint64_t modifier)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
// Only linear memory exists here, so any other modifier names a buffer this
// device will never produce and the answer is zero planes. INVALID is answered
// as well, because it means "no modifier was declared", which is the same
// layout question asked without an answer.
if (modifier != DrmFormatModLinear && modifier != DrmFormatModInvalid)
{
return 0;
}
GbmPlaneLayout planes[GbmMaxPlanes] = {};
Int planeCount = 0;
SizeT totalBytes = 0;
if (!GbmDescribeFormat(format, 1, 1, planes, planeCount, totalBytes))
{
return 0;
}
return planeCount;
}
// ---------------------------------------------------------------------------
// Buffer objects
// ---------------------------------------------------------------------------
extern "C" struct gbm_bo* gbm_bo_create(struct gbm_device* gbm, uint32_t width, uint32_t height, uint32_t format,
uint32_t flags)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
// No modifier list: the buffer is linear and gbm_bo_get_modifier says so.
return GbmBufferCreate(*gbm, width, height, format, flags, nullptr, 0);
}
extern "C" struct gbm_bo* gbm_bo_create_with_modifiers(struct gbm_device* gbm, uint32_t width, uint32_t height,
uint32_t format, const uint64_t* modifiers,
const unsigned int count)
{
// The modifier-less variant is the one with no flags parameter, so it is the
// flags-0 case of the other, and saying that here keeps one implementation.
return gbm_bo_create_with_modifiers2(gbm, width, height, format, modifiers, count, 0);
}
extern "C" struct gbm_bo* gbm_bo_create_with_modifiers2(struct gbm_device* gbm, uint32_t width, uint32_t height,
uint32_t format, const uint64_t* modifiers,
const unsigned int count, uint32_t flags)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
return GbmBufferCreate(*gbm, width, height, format, flags, modifiers, (Uint32)count);
}
extern "C" struct gbm_bo* gbm_bo_import(struct gbm_device* gbm, uint32_t type, void* buffer, uint32_t flags)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
return GbmBufferImport(*gbm, type, buffer, flags);
}
extern "C" void* gbm_bo_map(struct gbm_bo* bo, uint32_t x, uint32_t y, uint32_t width, uint32_t height,
uint32_t flags, uint32_t* stride, void** map_data)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
// The rectangle's size is not used to size the mapping: the whole buffer is
// mapped once, at its own length, and the rectangle is applied as an offset into
// it. That is what lets two overlapping maps share one address, and it is why a
// buffer's mapping stays valid for a caller that only ever asks for a corner.
(void)width;
(void)height;
(void)flags;
std::lock_guard<std::mutex> guard(*mutex);
Uint32 mapStride = 0;
void* address = GbmBufferMap(*bo, x, y, mapStride);
if (address == nullptr)
{
return nullptr;
}
if (stride != nullptr)
{
*stride = mapStride;
}
if (map_data != nullptr)
{
// The same address the function returns. Callers pass back either one to
// gbm_bo_unmap, and both have to name this mapping.
*map_data = address;
}
return address;
}
extern "C" void gbm_bo_unmap(struct gbm_bo* bo, void* map_data)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
return;
}
std::lock_guard<std::mutex> guard(*mutex);
GbmBufferUnmap(*bo, map_data);
}
extern "C" uint32_t gbm_bo_get_width(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.Width;
}
extern "C" uint32_t gbm_bo_get_height(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.Height;
}
extern "C" uint32_t gbm_bo_get_stride(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.Strides[0];
}
extern "C" uint32_t gbm_bo_get_stride_for_plane(struct gbm_bo* bo, int plane)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
// A plane this buffer does not have is answered with zero, which is the only
// value a caller can tell apart from a real stride.
if (plane < 0 || plane >= bo->Impl.PlaneCount)
{
errno = EINVAL;
return 0;
}
return bo->Impl.Strides[plane];
}
extern "C" uint32_t gbm_bo_get_format(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.Format;
}
extern "C" uint32_t gbm_bo_get_bpp(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
// Bits per pixel: a 32 bit format answers 32, which is what a caller computing a
// row length in bits or a row length in bytes from the same number expects.
return bo->Impl.BytesPerPixel * 8;
}
extern "C" uint32_t gbm_bo_get_offset(struct gbm_bo* bo, int plane)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
if (plane < 0 || plane >= bo->Impl.PlaneCount)
{
errno = EINVAL;
return 0;
}
return bo->Impl.Offsets[plane];
}
extern "C" struct gbm_device* gbm_bo_get_device(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
return bo->Impl.Owner;
}
extern "C" union gbm_bo_handle gbm_bo_get_handle(struct gbm_bo* bo)
{
union gbm_bo_handle handle;
handle.ptr = nullptr;
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
handle.s32 = -1;
return handle;
}
std::lock_guard<std::mutex> guard(*mutex);
// The kernel-level name of a dma-buf in this process is its file descriptor. A
// GEM handle would be a name that exists only relative to a DRM node, and this
// library deliberately does not require one, so the fd is the only answer here
// that is true for every buffer it hands out. Callers that need a name for a
// device should ask for the fd and pass that.
handle.s32 = bo->Impl.Fds[0];
return handle;
}
extern "C" int gbm_bo_get_fd(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return -1;
}
std::lock_guard<std::mutex> guard(*mutex);
// A dup, close-on-exec, owned by the caller. Closing it cannot affect the buffer
// or any other caller's copy.
return GbmBufferDupFd(*bo, 0);
}
extern "C" uint64_t gbm_bo_get_modifier(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return DrmFormatModInvalid;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.Modifier;
}
extern "C" int gbm_bo_get_plane_count(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.PlaneCount;
}
extern "C" union gbm_bo_handle gbm_bo_get_handle_for_plane(struct gbm_bo* bo, int plane)
{
union gbm_bo_handle handle;
handle.ptr = nullptr;
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
handle.s32 = -1;
return handle;
}
std::lock_guard<std::mutex> guard(*mutex);
if (plane < 0 || plane >= bo->Impl.PlaneCount)
{
errno = EINVAL;
handle.s32 = -1;
return handle;
}
// Same reasoning as gbm_bo_get_handle: the fd is the buffer's kernel name. Every
// plane of a self-allocated buffer lives in the one dma-buf, so all of them
// answer with that descriptor; the plane's position inside it is
// gbm_bo_get_offset.
const Int fd = bo->Impl.Fds[plane] >= 0 ? bo->Impl.Fds[plane] : bo->Impl.Fds[0];
handle.s32 = fd;
return handle;
}
extern "C" int gbm_bo_get_fd_for_plane(struct gbm_bo* bo, int plane)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return -1;
}
std::lock_guard<std::mutex> guard(*mutex);
return GbmBufferDupFd(*bo, plane);
}
extern "C" int gbm_bo_write(struct gbm_bo* bo, const void* buf, size_t count)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr || buf == nullptr)
{
errno = EINVAL;
return -1;
}
std::lock_guard<std::mutex> guard(*mutex);
if (count == 0)
{
return 0;
}
if (count > bo->Impl.AllocationSize)
{
// Refused rather than truncated: a caller that writes 4 MiB into a 1 MiB
// buffer and is told "0 bytes written, success" would read back memory it
// never wrote and call the result correct.
errno = EINVAL;
return -1;
}
// The write goes through the same counted mapping the rest of the API uses, so a
// caller that already holds the buffer mapped does not get a second mapping (or
// lose its own) behind its back.
Uint32 stride = 0;
void* address = GbmBufferMap(*bo, 0, 0, stride);
if (address == nullptr)
{
return -1;
}
memcpy(address, buf, count);
GbmBufferUnmap(*bo, address);
return 0;
}
extern "C" void gbm_bo_set_user_data(struct gbm_bo* bo, void* data,
void (*destroy_user_data)(struct gbm_bo*, void*))
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
return;
}
std::lock_guard<std::mutex> guard(*mutex);
// Assigning over existing data does not call the old callback: the record calls
// it exactly once, when the buffer goes away, which is the one moment a caller
// can still recognise the pointer it registered.
bo->Impl.UserData = data;
bo->Impl.UserDataDestroy = destroy_user_data;
}
extern "C" void* gbm_bo_get_user_data(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
return bo->Impl.UserData;
}
extern "C" void gbm_bo_destroy(struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(bo);
if (mutex == nullptr)
{
return;
}
std::lock_guard<std::mutex> guard(*mutex);
// Drops this caller's reference. A buffer a surface still holds stays alive -
// that is what the surface's own reference is for - so destroying a buffer that
// is currently presented is not a use-after-free waiting to happen.
GbmBufferRelease(*bo->Impl.Owner, *bo);
}
// ---------------------------------------------------------------------------
// Surfaces
// ---------------------------------------------------------------------------
extern "C" struct gbm_surface* gbm_surface_create(struct gbm_device* gbm, uint32_t width, uint32_t height,
uint32_t format, uint32_t flags)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
return GbmSurfaceCreate(*gbm, width, height, format, flags, nullptr, 0);
}
extern "C" struct gbm_surface* gbm_surface_create_with_modifiers(struct gbm_device* gbm, uint32_t width,
uint32_t height, uint32_t format,
const uint64_t* modifiers,
const unsigned int count)
{
// As with the buffer pair: the modifier-less form is the flags-0 case.
return gbm_surface_create_with_modifiers2(gbm, width, height, format, modifiers, count, 0);
}
extern "C" struct gbm_surface* gbm_surface_create_with_modifiers2(struct gbm_device* gbm, uint32_t width,
uint32_t height, uint32_t format,
const uint64_t* modifiers,
const unsigned int count, uint32_t flags)
{
std::mutex* mutex = MutexOf(gbm);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
return GbmSurfaceCreate(*gbm, width, height, format, flags, modifiers, (Uint32)count);
}
extern "C" struct gbm_bo* gbm_surface_lock_front_buffer(struct gbm_surface* surface)
{
std::mutex* mutex = MutexOf(surface);
if (mutex == nullptr)
{
errno = EINVAL;
return nullptr;
}
std::lock_guard<std::mutex> guard(*mutex);
// Null with errno = EBUSY when the compositor still holds every buffer: that is a
// state the caller has to handle, and pretending otherwise by handing out a
// buffer that is still being scanned out would be a torn frame rather than an
// error.
return GbmSurfaceLockFront(*surface);
}
extern "C" void gbm_surface_release_buffer(struct gbm_surface* surface, struct gbm_bo* bo)
{
std::mutex* mutex = MutexOf(surface);
if (mutex == nullptr || bo == nullptr)
{
return;
}
std::lock_guard<std::mutex> guard(*mutex);
GbmSurfaceReleaseBuffer(*surface, *bo);
}
extern "C" int gbm_surface_has_free_buffers(struct gbm_surface* surface)
{
std::mutex* mutex = MutexOf(surface);
if (mutex == nullptr)
{
errno = EINVAL;
return 0;
}
std::lock_guard<std::mutex> guard(*mutex);
// Truthful by construction: the swapchain exists from the moment the surface
// does, so this answers about buffers that were really allocated rather than
// about how many could still be.
return GbmSurfaceHasFreeBuffers(*surface) ? 1 : 0;
}
extern "C" void gbm_surface_destroy(struct gbm_surface* surface)
{
std::mutex* mutex = MutexOf(surface);
if (mutex == nullptr)
{
return;
}
std::lock_guard<std::mutex> guard(*mutex);
GbmSurfaceDestroy(*surface);
}
// ---------------------------------------------------------------------------
// Format names
// ---------------------------------------------------------------------------
extern "C" char* gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc* desc)
{
if (desc == nullptr)
{
errno = EINVAL;
return nullptr;
}
// The name of a fourcc is its four characters, most significant byte last, and
// that is all there is to it: a caller compares these strings against "AR24" and
// friends, and the big-endian bit is part of the value rather than something to
// spell out here.
desc->name[0] = (char)(gbm_format & 0xffu);
desc->name[1] = (char)((gbm_format >> 8) & 0xffu);
desc->name[2] = (char)((gbm_format >> 16) & 0xffu);
desc->name[3] = (char)((gbm_format >> 24) & 0xffu);
desc->name[4] = '\0';
return desc->name;
}
// End of Source File Header
File diff suppressed because it is too large Load Diff
+835
View File
@@ -0,0 +1,835 @@
// MobileGL - MobileGL/MG_Gbm/GbmHostChannel.cpp
// SPDX-License-Identifier: LGPL-3.0-only
//
// The host-frame backend: the socket the container receives the phone's frames on.
//
// The container half of MobileGL runs a Wayland compositor, and the frames that
// compositor renders into have to be the frames the phone already owns - the phone
// allocates them, its GPU draws into them, and its display scans them out. A buffer
// allocated from the container's own dma-heap would be a copy that has to travel back
// to the phone before anything can be shown, so when the display host is reachable this
// backend hands the compositor the phone's own buffers instead, and the fd the
// compositor passes on is the fd of a frame the display path already knows.
//
// What that costs is the one thing GBM normally promises: the geometry. A frame's
// width, height, stride and format are the display's and not the caller's, so a
// gbm_bo_create that takes an offered frame answers with the offer's geometry, and the
// caller reads it back with gbm_bo_get_width and its neighbours. That is this backend's
// documented behaviour rather than a surprise: the host decides what a frame is.
//
// Three rules keep this backend from ever being worse than not having it.
//
// * Nothing on the allocation path blocks. The socket is non-blocking and connect()
// to a pathname socket either succeeds or fails immediately, so a call that cannot
// be served from the host falls through to a local allocation inside the same call
// instead of waiting for a phone that may never answer. The one wait in this file is
// in device creation, after a display host has already accepted the connection, and
// it exists because of a measured race: the compositor builds its swapchain within
// microseconds of creating the device, while the offer for the first frame needs the
// round trip of the connection plus whatever the phone does before it sends. Without
// that wait the swapchain - the buffers that are shown for the life of the process -
// is allocated locally and the host's frames arrive for nobody to take.
// * A channel that dies is closed and marked down, and the next frame is allocated
// locally again. This library is loaded by a compositor that has to keep painting,
// so losing the display host may cost the frames their path to the screen but it
// may not cost the session.
// * A frame is released exactly once, when the record holding it is destroyed, and
// only into the connection that offered it. Releasing on gbm_bo_destroy would be a
// lie - the surface may still be scanning that frame out - and releasing an old
// frame into a new connection would free a slot belonging to another session.
//
// The device mutex is the only lock in here. Both callers of this file - the allocation
// path and the buffer record's destructor - already run under it, so there is no second
// lock to order against and no state of this file's own that another thread can see.
#include "GbmInternal.h"
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
namespace MobileGL::MG_Gbm
{
namespace
{
// One frame per message, and a message is the offer followed by the handle the
// offer describes. 4 KiB is far more than an AHardwareBuffer handle needs (a
// handle is a version, two counts and their two arrays) while still being small
// enough that a message which does not fit is a peer speaking something else -
// which is worth refusing rather than growing a buffer for.
constexpr SizeT GbmHostMessageMax = 4096;
// The descriptors one handle may carry. An AHardwareBuffer handle carries one
// (the dma-buf) in every layout this library has seen and the protocol reads only
// the first, so this sizes the control buffer rather than claiming anything about
// real handles.
constexpr Int GbmHostMaxFds = 8;
// How many messages one gbm_bo_create may look at before it gives up and
// allocates locally. An unusable message is skipped rather than fatal, so this
// bounds the work a peer that sends nothing but unusable messages can cause
// inside a single allocation.
constexpr Int GbmHostTakeAttempts = 16;
// The floor between two connection attempts. A container with no display host is
// the normal case, so a retry may not turn every allocation into a connect plus a
// log line; one attempt per second is often enough that a phone which comes up is
// picked up within a frame or two of the compositor's next allocation.
constexpr std::chrono::milliseconds GbmHostRetryInterval{1000};
// How long device creation waits for the display host's first frame once the
// connection is up. A frame the phone offers immediately needs one round trip on
// a unix socket, so this is orders of magnitude more than that and it is only
// ever spent by a device that has a host which then says nothing; a compile or a
// test run without a display host never reaches it, because the connect fails
// first.
constexpr Int GbmHostFirstOfferWaitMs = 250;
// Frame releases the socket would not take. A release is twelve bytes, so a full
// socket buffer is hundreds of frames' worth of them and this queue is only
// reached once the phone has stopped reading; the cap is what stops a peer that
// never reads from growing it without bound.
constexpr SizeT GbmHostMaxPendingReleases = 256;
// sun_path is where a pathname socket's name lives, and a name that does not fit
// is one the kernel cannot be told about at all.
constexpr SizeT GbmHostPathMax = sizeof(((struct sockaddr_un*)nullptr)->sun_path);
// close() may overwrite errno, and every caller here is in the middle of
// reporting a failure that errno describes. Saving it around the close keeps the
// reported failure the one that happened.
void GbmHostCloseQuietly(Int fd) noexcept
{
if (fd < 0)
{
return;
}
const Int savedErrno = errno;
close(fd);
errno = savedErrno;
}
} // namespace
// One connection to the display host, for the life of one device.
class GbmHostChannel
{
public:
explicit GbmHostChannel(const String& path) noexcept : Path(path) {}
~GbmHostChannel() noexcept;
GbmHostChannel(const GbmHostChannel&) = delete;
GbmHostChannel& operator=(const GbmHostChannel&) = delete;
// Connects when there is no connection and the retry interval has passed. A
// failure is never the caller's problem: the channel stays closed and the
// allocation that asked for a frame falls back to local memory.
void EnsureOpen(::gbm_device& device) noexcept;
// Waits, once, for the connection's first frame to be readable. Called from
// device creation and nowhere else: see the banner for why that one wait exists.
void AwaitFirstOffer(::gbm_device& device) noexcept;
// The next frame the host offered, or false when none is waiting.
Bool TakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept;
// Queues the release for one frame and tries to send what is queued.
void ReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept;
private:
// Turns one message into a frame. Everything it refuses is reported with the
// reason, because a peer this library cannot understand is the one thing that
// would otherwise be invisible from the outside.
Bool ParseOffer(const Uint8* payload, SizeT length, const Int* fds, Int fdCount,
GbmHostFrame& frameOut) noexcept;
// Sends what is queued. False means the connection can no longer be trusted to
// carry a twelve byte message, with errno describing what happened.
Bool FlushReleases() noexcept;
// Closes the socket and drops whatever was queued for it.
void CloseSocket() noexcept;
// Closes the socket and tells the device what that means for its backend name.
void MarkDown(::gbm_device& device, const char* what, Int error) noexcept;
String Path;
Int SocketFd = -1;
// Set by a connection this process has just made, and read once by the allocation that
// follows it: the accept and the frame that answers the dial happen on the other side of
// this socket, so an allocation that reads immediately after connecting reads before the
// answer to its own connection has been written. See TakeFrame.
Bool FreshConnection = false;
// Bumped by every connection, so a frame taken from a connection which has since
// died can be recognised as one that must not be released into its replacement.
Uint64 Generation = 0;
std::chrono::steady_clock::time_point NextRetry{};
// The errno of the last failed attempt, so that a failure which repeats once a
// second is not logged once a second.
Int LastFailureErrno = 0;
std::vector<GbmFrameRelease> PendingReleases;
Bool ReleaseOverflowLogged = false;
};
// ---- the offered-frame record ------------------------------------------
GbmHostFrame::~GbmHostFrame()
{
// The record owns the dup it was built on, which is why it is move-only: a copy
// would close the same descriptor twice.
if (Fd >= 0)
{
close(Fd);
Fd = -1;
}
}
GbmHostFrame::GbmHostFrame(GbmHostFrame&& other) noexcept
{
*this = static_cast<GbmHostFrame&&>(other);
}
GbmHostFrame& GbmHostFrame::operator=(GbmHostFrame&& other) noexcept
{
if (this == &other)
{
return *this;
}
if (Fd >= 0)
{
close(Fd);
}
Seq = other.Seq;
Index = other.Index;
Width = other.Width;
Height = other.Height;
StrideBytes = other.StrideBytes;
Format = other.Format;
BytesPerPixel = other.BytesPerPixel;
Usage = other.Usage;
Generation = other.Generation;
Fd = other.Fd;
Size = other.Size;
// The source must not close what this record now owns.
other.Fd = -1;
return *this;
}
// ---- the connection ----------------------------------------------------
GbmHostChannel::~GbmHostChannel() noexcept
{
// One last try for whatever is queued - the phone may still be reading - and
// then the socket goes away with the process.
if (SocketFd >= 0 && !PendingReleases.empty())
{
FlushReleases();
}
if (!PendingReleases.empty())
{
MGLOG_W("gbm: %zu frame release(s) could not be sent to the display host at %s; the phone reclaims "
"those slots when the session ends",
PendingReleases.size(), Path.c_str());
}
CloseSocket();
}
void GbmHostChannel::EnsureOpen(::gbm_device& device) noexcept
{
if (SocketFd >= 0)
{
return;
}
const std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if (now < NextRetry)
{
return;
}
NextRetry = now + GbmHostRetryInterval;
// SOCK_SEQPACKET because a frame and its descriptors are one message and the
// reader may not reassemble either half; SOCK_NONBLOCK because the caller is a
// compositor's frame loop and may not be parked on a phone; SOCK_CLOEXEC because
// every process this compositor starts would otherwise inherit a socket it can
// read frames from.
const Int fd = socket(AF_UNIX, SOCK_SEQPACKET | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0)
{
MGLOG_E_ONCE("gbm: cannot create the socket to the display host (%d: %s); every frame is allocated "
"locally",
errno, strerror(errno));
return;
}
if (Path.empty() || Path.size() >= GbmHostPathMax)
{
MGLOG_E_ONCE("gbm: the display host socket path \"%s\" does not fit in sun_path (%zu bytes); every "
"frame is allocated locally",
Path.c_str(), GbmHostPathMax);
GbmHostCloseQuietly(fd);
return;
}
struct sockaddr_un address = {};
address.sun_family = AF_UNIX;
// The terminator travels with the name: sun_path is a C string for a pathname
// socket, and a name without one is a name the kernel reads past the end of.
memcpy(address.sun_path, Path.c_str(), Path.size() + 1);
if (connect(fd, reinterpret_cast<struct sockaddr*>(&address), sizeof(address)) != 0)
{
const Int connectErrno = errno;
GbmHostCloseQuietly(fd);
// A failure that repeats once a second is not news: a container with no
// display host fails here on every retry, and a line per second saying so
// would bury the rest of the log. A failure that changed is news, because it
// says the path or the peer changed.
if (connectErrno != LastFailureErrno)
{
LastFailureErrno = connectErrno;
if (connectErrno == EPROTOTYPE)
{
MGLOG_E("gbm: %s is a socket of a different type (%d: %s). The frame protocol is "
"SOCK_SEQPACKET - one message per frame, carrying that frame's descriptors - "
"because a reader cannot tell from a byte stream which frame a descriptor "
"belongs to; frames are allocated locally instead",
Path.c_str(), connectErrno, strerror(connectErrno));
}
else
{
MGLOG_I("gbm: no display host at %s (%d: %s); every frame is allocated locally until one "
"answers",
Path.c_str(), connectErrno, strerror(connectErrno));
}
}
return;
}
SocketFd = fd;
FreshConnection = true;
// A new connection is a new session with its own frames, which is what makes the
// generation a correct answer to "may this release be sent here".
Generation++;
LastFailureErrno = 0;
device.Impl.HostAvailable = true;
device.Impl.BackendName = "mobilegl-host-frame";
MGLOG_I("gbm: connected to the display host at %s (one SOCK_SEQPACKET message per frame, up to %zu "
"bytes); frames offered over it become gbm_bos whose geometry is the offer's",
Path.c_str(), GbmHostMessageMax);
}
void GbmHostChannel::AwaitFirstOffer(::gbm_device& device) noexcept
{
if (SocketFd < 0)
{
return;
}
const std::chrono::steady_clock::time_point deadline =
std::chrono::steady_clock::now() + std::chrono::milliseconds(GbmHostFirstOfferWaitMs);
for (;;)
{
const std::chrono::milliseconds left =
std::chrono::duration_cast<std::chrono::milliseconds>(deadline - std::chrono::steady_clock::now());
if (left.count() <= 0)
{
break;
}
// poll() and not recvmsg(): the frame has to stay in the socket for the
// allocation that is about to ask for it, and a peek at a message carrying
// SCM_RIGHTS is a question with no safe answer.
struct pollfd waiting = {};
waiting.fd = SocketFd;
waiting.events = POLLIN;
const Int ready = poll(&waiting, 1, (Int)left.count());
if (ready > 0)
{
MGLOG_I("gbm: the display host at %s has a frame waiting; the buffers the compositor is about "
"to allocate come from it",
Path.c_str());
return;
}
if (ready == 0)
{
break;
}
if (errno != EINTR)
{
MarkDown(device, "poll", errno);
return;
}
}
MGLOG_W("gbm: the display host at %s accepted the connection but offered no frame within %d ms; the "
"buffers allocated before its first frame are local ones",
Path.c_str(), GbmHostFirstOfferWaitMs);
}
Bool GbmHostChannel::TakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept
{
EnsureOpen(device);
if (SocketFd < 0)
{
return false;
}
// THE FIRST ALLOCATION AFTER A DIAL WAITS FOR THE ANSWER TO IT. The accept and the offer that
// answers the connection this call has just made are written by the phone, a round trip
// behind the connect() that returns here — and what is allocated before that offer arrives
// is a local buffer, which is what a compositor builds its whole swapchain out of and then
// keeps for the life of the process. So a connection made on this path is given the same
// first-offer window device creation gives one, once, and only while it is new.
if (FreshConnection)
{
FreshConnection = false;
AwaitFirstOffer(device);
}
// Releases the socket would not take last time are retried here, because this is
// the one call in the library that happens once per frame and therefore the
// natural place for a queue waiting for room to get its next chance.
if (!PendingReleases.empty() && !FlushReleases())
{
MarkDown(device, "sendmsg", errno);
return false;
}
Uint8 payload[GbmHostMessageMax];
for (Int attempt = 0; attempt < GbmHostTakeAttempts; ++attempt)
{
Int receivedFds[GbmHostMaxFds];
for (Int i = 0; i < GbmHostMaxFds; ++i)
{
receivedFds[i] = -1;
}
struct iovec payloadVector = {};
payloadVector.iov_base = payload;
payloadVector.iov_len = sizeof(payload);
Uint8 control[CMSG_SPACE(sizeof(Int) * GbmHostMaxFds)] = {};
struct msghdr message = {};
message.msg_iov = &payloadVector;
message.msg_iovlen = 1;
message.msg_control = control;
message.msg_controllen = sizeof(control);
// MSG_DONTWAIT for the promise at the top of this file, and MSG_CMSG_CLOEXEC
// so a descriptor the phone sent does not survive into a child of this
// compositor, which would then hold a frame nothing will ever release.
const ssize_t received = recvmsg(SocketFd, &message, MSG_DONTWAIT | MSG_CMSG_CLOEXEC);
if (received < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// Nothing offered yet, which is what an idle display looks like. The
// caller allocates locally and asks again on its next frame.
return false;
}
if (errno == EINTR)
{
continue;
}
MarkDown(device, "recvmsg", errno);
return false;
}
// The descriptors come out of the control data before anything is decided
// about the message, because they belong to this process from the moment it
// was received: a path that returns without closing them leaks a descriptor
// per unusable message.
Int fdCount = 0;
for (struct cmsghdr* header = CMSG_FIRSTHDR(&message); header != nullptr;
header = CMSG_NXTHDR(&message, header))
{
if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS)
{
continue;
}
const SizeT bytes = (SizeT)header->cmsg_len - CMSG_LEN(0);
const Int count = (Int)(bytes / sizeof(Int));
const Int* descriptors = reinterpret_cast<const Int*>(CMSG_DATA(header));
for (Int i = 0; i < count; ++i)
{
if (fdCount < GbmHostMaxFds)
{
receivedFds[fdCount++] = descriptors[i];
}
else
{
// More descriptors than a frame can carry: they are this
// process's now, so they are closed here rather than leaked.
GbmHostCloseQuietly(descriptors[i]);
}
}
}
if ((message.msg_flags & MSG_CTRUNC) != 0)
{
MGLOG_W_ONCE("gbm: an offered frame's control data did not fit in %zu bytes; the frame is "
"skipped because its descriptor list is incomplete",
sizeof(control));
for (Int i = 0; i < fdCount; ++i)
{
GbmHostCloseQuietly(receivedFds[i]);
}
continue;
}
if (received == 0)
{
// Zero bytes on a connected socket is the end of it, whether the phone
// closed cleanly or the kernel gave up on it. Either way the frames have
// to come from somewhere else from here on.
for (Int i = 0; i < fdCount; ++i)
{
GbmHostCloseQuietly(receivedFds[i]);
}
MarkDown(device, "the display host closed the connection", 0);
return false;
}
if ((message.msg_flags & MSG_TRUNC) != 0)
{
// Longer than a frame message can be, so it is not one - and on a
// message-oriented socket the rest is already gone while the next message
// is still a whole message, which is why this skips one message instead
// of ending the channel.
MGLOG_W_ONCE("gbm: the display host sent a message longer than %zu bytes; it is skipped",
GbmHostMessageMax);
for (Int i = 0; i < fdCount; ++i)
{
GbmHostCloseQuietly(receivedFds[i]);
}
continue;
}
const Bool parsed = ParseOffer(payload, (SizeT)received, receivedFds, fdCount, frameOut);
// Whatever the frame did not keep is closed here: the copy of the dma-buf
// descriptor the phone sent (the frame owns its own dup) and the rest of the
// handle, which is a layout this library does not interpret.
for (Int i = 0; i < fdCount; ++i)
{
GbmHostCloseQuietly(receivedFds[i]);
}
if (parsed)
{
return true;
}
}
return false;
}
Bool GbmHostChannel::ParseOffer(const Uint8* payload, SizeT length, const Int* fds, Int fdCount,
GbmHostFrame& frameOut) noexcept
{
constexpr SizeT HeaderBytes = sizeof(GbmFrameOffer) + sizeof(AHBNativeHandleHeader);
if (length < HeaderBytes)
{
MGLOG_W_ONCE("gbm: the display host sent a %zu byte message where a frame message is at least %zu "
"bytes (the offer followed by the handle header); it is skipped",
length, HeaderBytes);
return false;
}
GbmFrameOffer offer = {};
memcpy(&offer, payload, sizeof(offer));
if (offer.Magic != GbmFrameOfferMagic)
{
MGLOG_W_ONCE("gbm: a message from the display host begins with 0x%08x rather than 'MGGF'; it is "
"skipped",
offer.Magic);
return false;
}
if (offer.Version != GbmHostProtocolVersion)
{
// Refused rather than read as if the versions agreed: the version is what
// says how the rest of the message is laid out, and a frame read with the
// wrong layout is memory the compositor would render into.
MGLOG_E_ONCE("gbm: the display host offers frames in protocol version %u where this library speaks "
"version %u; its frames are refused until the two agree",
offer.Version, (Uint32)GbmHostProtocolVersion);
return false;
}
AHBNativeHandleHeader handle = {};
memcpy(&handle, payload + sizeof(offer), sizeof(handle));
if (handle.NumFds < 1 || handle.NumInts < 0)
{
MGLOG_W_ONCE("gbm: an offered frame's handle carries %d descriptor(s) and %d integer(s), where the "
"dma-buf is the first descriptor and there has to be one; the frame is skipped",
handle.NumFds, handle.NumInts);
return false;
}
// The handle describes itself with its two counts, so the message has to be
// exactly as long as those counts make it. Any other length means the two ends
// disagree about the layout, and a frame built from a message whose layout was
// guessed is a buffer of unknown memory.
const SizeT expected = HeaderBytes + ((SizeT)handle.NumFds + (SizeT)handle.NumInts) * sizeof(Int32);
if (length != expected)
{
MGLOG_W_ONCE("gbm: an offered frame's message is %zu bytes where its handle (%d descriptors, %d "
"integers) makes it %zu; the frame is skipped",
length, handle.NumFds, handle.NumInts, expected);
return false;
}
if (fdCount < handle.NumFds)
{
MGLOG_W_ONCE("gbm: an offered frame's handle names %d descriptor(s) and %d arrived with the "
"message; the frame is skipped",
handle.NumFds, fdCount);
return false;
}
if (offer.Width == 0 || offer.Height == 0 || offer.StrideBytes == 0)
{
MGLOG_W_ONCE("gbm: an offered frame is %ux%u with a stride of %u bytes; a buffer with no size is "
"not one this library can hand out",
offer.Width, offer.Height, offer.StrideBytes);
return false;
}
// The frame is built on a dup of the first descriptor rather than on the
// descriptor itself: the copy that arrived belongs to the message, and the frame
// outlives it. Close-on-exec, like every other descriptor this library hands out.
const Int frameFd = fcntl(fds[0], F_DUPFD_CLOEXEC, 0);
if (frameFd < 0)
{
MGLOG_E_ONCE("gbm: cannot dup the descriptor of an offered frame (%d: %s)", errno, strerror(errno));
return false;
}
// How long the frame really is, asked of the descriptor instead of computed from
// the geometry: a gralloc dma-buf is usually larger than stride x height, and a
// mapping has to cover what exists rather than what the arithmetic expects.
SizeT size = 0;
const off_t end = lseek(frameFd, 0, SEEK_END);
if (end > 0)
{
size = (SizeT)end;
}
const SizeT described = (SizeT)offer.StrideBytes * (SizeT)offer.Height;
if (size == 0)
{
// A descriptor that does not answer for its own length - an exporter that
// does not implement llseek - leaves the offer's arithmetic as the only
// length there is.
size = described;
}
if (size < described)
{
MGLOG_W_ONCE("gbm: an offered frame's descriptor holds %zu bytes where %u rows of %u bytes need %zu; "
"the frame is skipped rather than mapped over memory that is not there",
size, offer.Height, offer.StrideBytes, described);
GbmHostCloseQuietly(frameFd);
return false;
}
frameOut.Seq = offer.Seq;
frameOut.Index = offer.Index;
frameOut.Width = offer.Width;
frameOut.Height = offer.Height;
frameOut.StrideBytes = offer.StrideBytes;
frameOut.Format = offer.Format;
frameOut.Usage = ((Uint64)offer.UsageHi << 32) | (Uint64)offer.UsageLo;
frameOut.Generation = Generation;
frameOut.Fd = frameFd;
frameOut.Size = size;
frameOut.BytesPerPixel = GbmFormatBytesPerPixel(offer.Format);
if (frameOut.BytesPerPixel == 0)
{
// A format this library has no table entry for is still a frame the phone
// drew. The offer's stride is then the only description of a pixel that
// exists: a row of Width pixels is StrideBytes long.
frameOut.BytesPerPixel = offer.StrideBytes / offer.Width;
}
MGLOG_I("gbm: frame seq=%u index=%u %ux%u stride=%u format=0x%08x (%u bytes per pixel, %zu bytes "
"measured) taken from the display host",
offer.Seq, offer.Index, offer.Width, offer.Height, offer.StrideBytes, offer.Format,
frameOut.BytesPerPixel, size);
return true;
}
void GbmHostChannel::ReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept
{
if (SocketFd < 0)
{
// A release only means anything on the connection that offered the frame,
// and there is none.
return;
}
if (generation != Generation)
{
// The frame was offered by a connection that has since died and been
// replaced. Sending this into the replacement would name a frame of another
// session, and the phone would recycle a slot this buffer was not made of.
MGLOG_W_ONCE("gbm: a frame offered by a display host connection that has since died is not "
"released into its replacement");
return;
}
if (PendingReleases.size() >= GbmHostMaxPendingReleases)
{
if (!ReleaseOverflowLogged)
{
ReleaseOverflowLogged = true;
MGLOG_W("gbm: %zu frame releases to the display host at %s are already waiting for room in the "
"socket; this one is dropped rather than queued without bound, and the phone reclaims "
"the slot when the session ends",
PendingReleases.size(), Path.c_str());
}
return;
}
GbmFrameRelease release = {};
release.Magic = GbmFrameReleaseMagic;
release.Version = GbmHostProtocolVersion;
release.Seq = seq;
PendingReleases.push_back(release);
if (!FlushReleases())
{
MarkDown(device, "sendmsg", errno);
}
}
Bool GbmHostChannel::FlushReleases() noexcept
{
while (!PendingReleases.empty())
{
struct iovec releaseVector = {};
releaseVector.iov_base = &PendingReleases.front();
releaseVector.iov_len = sizeof(GbmFrameRelease);
struct msghdr message = {};
message.msg_iov = &releaseVector;
message.msg_iovlen = 1;
// MSG_DONTWAIT for the same reason every other call here is non-blocking, and
// MSG_NOSIGNAL because a compositor killed by SIGPIPE when the phone goes
// away is a desktop that disappears with it.
const ssize_t sent = sendmsg(SocketFd, &message, MSG_DONTWAIT | MSG_NOSIGNAL);
if (sent == (ssize_t)sizeof(GbmFrameRelease))
{
PendingReleases.erase(PendingReleases.begin());
continue;
}
if (sent < 0 && errno == EINTR)
{
continue;
}
if (sent < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
{
// A full socket is a phone that has stopped reading rather than a dead
// one: the release stays queued and goes out with a later frame.
return true;
}
// A short send, a reset and a broken pipe all say the same thing: this
// connection can no longer be trusted to have carried a twelve byte message,
// and acting on a maybe is worse than handing the slot back when the session
// ends. errno describes which of them it was.
return false;
}
return true;
}
void GbmHostChannel::CloseSocket() noexcept
{
if (SocketFd >= 0)
{
GbmHostCloseQuietly(SocketFd);
SocketFd = -1;
}
PendingReleases.clear();
}
void GbmHostChannel::MarkDown(::gbm_device& device, const char* what, Int error) noexcept
{
if (!PendingReleases.empty())
{
MGLOG_W("gbm: %zu frame release(s) were still queued for the display host and cannot be sent over a "
"connection that is gone; the phone reclaims those slots when the session ends",
PendingReleases.size());
}
CloseSocket();
if (error != 0)
{
MGLOG_W("gbm: the display host connection at %s is down: %s failed (%d: %s); frames are allocated "
"locally from here on, and a new connection is attempted on a later allocation",
Path.c_str(), what, error, strerror(error));
}
else
{
MGLOG_W("gbm: the display host connection at %s is down: %s; frames are allocated locally from here "
"on, and a new connection is attempted on a later allocation",
Path.c_str(), what);
}
device.Impl.HostAvailable = false;
device.Impl.BackendName = device.Impl.SelfAllocatedBackendName;
}
// ---- the device-facing entry points ------------------------------------
void GbmHostChannelCreate(::gbm_device& device) noexcept
{
const char* configured = getenv("MOBILEGL_GBM_SOCKET");
const String path =
(configured != nullptr && configured[0] != '\0') ? String(configured) : String("/run/mobilegl-gbm.sock");
device.Impl.SocketPath = path;
try
{
GbmHostChannel* channel = new GbmHostChannel(path);
device.Impl.Host = channel;
// The one attempt made at creation. It is allowed to fail: a machine that is
// not the container half of a split run has no display host, and that is a
// device which allocates locally rather than a device that could not exist.
channel->EnsureOpen(device);
// A connection that came up is waited on once, and only here, so that the
// compositor's swapchain is built out of the host's frames rather than out of
// local ones it will keep for the life of the process.
channel->AwaitFirstOffer(device);
}
catch (...)
{
device.Impl.Host = nullptr;
device.Impl.HostAvailable = false;
MGLOG_W("gbm: the display host channel could not be created; every frame is allocated locally");
}
}
void GbmHostChannelDestroy(::gbm_device& device) noexcept
{
GbmHostChannel* channel = device.Impl.Host;
if (channel == nullptr)
{
return;
}
device.Impl.Host = nullptr;
device.Impl.HostAvailable = false;
delete channel;
}
Bool GbmHostChannelTakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept
{
GbmHostChannel* channel = device.Impl.Host;
if (channel == nullptr)
{
return false;
}
return channel->TakeFrame(device, frameOut);
}
void GbmHostChannelReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept
{
GbmHostChannel* channel = device.Impl.Host;
if (channel == nullptr)
{
return;
}
channel->ReleaseFrame(device, seq, generation);
}
} // namespace MobileGL::MG_Gbm
// End of Source File Header
+476
View File
@@ -0,0 +1,476 @@
// MobileGL - MobileGL/MG_Gbm/GbmInternal.h
// SPDX-License-Identifier: LGPL-3.0-only
//
// This header carries the internal model shared by the GBM front-end files: the
// device, buffer-object and surface records, the two allocation backends, and the
// wire protocol used to talk to a phone that owns the real display hardware.
//
// The reason a GBM implementation has to exist in this tree at all is that the
// container half of MobileGL runs a Wayland compositor (kwin). That compositor
// needs libgbm to allocate the buffers it renders into, and the fd of such a
// buffer is handed to the Anland display producer, which ships the frame to the
// phone. If Mesa's libgbm is loaded, the compositor renders into Mesa buffers and
// the phone ends up receiving a copy through a path that was never designed to
// carry them. When this library is loaded instead, the buffers are either frames
// the phone itself offered to us, or buffers allocated from the kernel's dma-heap
// so that they are dma-bufs the display host can consume directly.
//
// The ownership rules below are the whole contract, because every one of them is
// observable from outside this library:
//
// * A buffer record owns its descriptors. gbm_bo_get_fd and
// gbm_bo_get_fd_for_plane hand out a dup, so a caller that closes what it was
// given cannot invalidate the buffer, and the matching close happens exactly
// once, in ~GbmBufferObject.
// * A mapping is one mmap of the buffer's fd, counted rather than duplicated, so
// two overlapping gbm_bo_map calls share one address and the last matching
// gbm_bo_unmap releases it.
// * A surface owns one reference per swapchain buffer, and every
// gbm_surface_lock_front_buffer hands the caller a reference of its own. That
// is what makes gbm_bo_destroy on a locked buffer harmless: it drops the
// caller's reference and leaves the surface's bookkeeping intact.
// * One mutex per device. GBM calls arrive from the compositor's render thread
// and from its main thread, and the state they touch (the buffer table, the
// swapchains, the backends) is small enough that one lock is cheaper than
// several - and, more importantly, one lock cannot be taken in the wrong order.
//
// The helpers declared at the bottom of this file are the entire internal interface
// between GbmDevice.cpp (the records and the self-allocated backend) and GbmApi.cpp
// (the extern "C" surface). Every one of them assumes the device mutex is already
// held: the lock is taken once, in the API layer, so that a compositor call can
// never interleave with another half way through a swapchain update.
#pragma once
#include <Includes.h>
#include <gbm.h>
#include <atomic>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace MobileGL::MG_Gbm
{
// DRM_FORMAT_MOD_LINEAR and DRM_FORMAT_MOD_INVALID. drm_fourcc.h is not part of
// libgbm's interface, and these two values are frozen kernel UAPI, so they are
// spelled out here rather than made a build dependency on libdrm's headers.
static constexpr Uint64 DrmFormatModLinear = 0ULL;
static constexpr Uint64 DrmFormatModInvalid = 0x00ffffffffffffffULL;
// Every buffer this library hands out is described by one of these. HostFrame
// buffers are owned by the phone and only borrowed by us, so they must be
// released back over the channel instead of being freed locally.
enum class GbmBackend
{
None = 0,
HostFrame,
DmaHeap,
MemFd,
};
// The phone offers frames with this packed struct immediately followed by an
// AHardwareBuffer native handle in the payload of one message, with the handle's
// descriptors in the SCM_RIGHTS control data of that same message. The layout is
// fixed by the phone side, so the packing attribute is load bearing and the field
// order may not be rearranged.
//
// The transport below the two structs is SOCK_SEQPACKET, and that is part of the
// protocol rather than an implementation detail: a frame and its descriptors only
// mean anything together, and the message boundary is what lets the reader take
// the two as one thing. A stream socket would let a reader receive half an offer,
// or a descriptor whose position in the byte stream it had to reconstruct, and the
// failure mode of getting that arithmetic wrong is a frame silently made of
// another frame's memory.
//
// Format is a DRM fourcc and not an AHardwareBuffer format, because it is what
// gbm_bo_get_format has to answer with and a compositor compares it against
// DRM_FORMAT_* values. StrideBytes is the byte distance between two rows of plane
// 0, which is the same single-stride description GBM_BO_IMPORT_FD carries - and
// the reason a frame read from this struct is reported as linear.
struct GbmFrameOffer
{
uint32_t Magic; // 'M','G','G','F' little endian on both ends of the wire
uint32_t Version;
uint32_t Seq;
uint32_t Index;
uint32_t Width;
uint32_t Height;
uint32_t StrideBytes;
uint32_t Format;
uint32_t UsageLo;
uint32_t UsageHi;
} __attribute__((packed));
// The container replies with this once it no longer reads the frame, which is
// what lets the phone recycle the slot. Without it the display host would run
// out of buffers after a handful of frames.
struct GbmFrameRelease
{
uint32_t Magic; // 'M','G','G','R'
uint32_t Version;
uint32_t Seq;
} __attribute__((packed));
static constexpr uint32_t GbmFrameOfferMagic = 0x4647474Du; // "MGGF"
static constexpr uint32_t GbmFrameReleaseMagic = 0x5247474Du; // "MGGR"
static constexpr uint32_t GbmHostProtocolVersion = 1;
// THE REVERSE DIRECTION: this end asking the render server to back one of its textures with a
// dma-buf. The compositor's scene reaches the display daemon's buffers through
// glEGLImageTargetTexture2DOES, and the server has to be told which texture that dma-buf backs -
// over this same socket, because it is the one channel that can carry a descriptor across the
// container boundary. Restated here rather than shared with MG_Backend/GbmFrameChannel.h for the
// same reason the two frame shapes are: the ends are built into different libraries from
// different build configurations (that file is the Android render server and never sees libgbm).
// Field for field, packing included; the plane descriptors ride in the SCM_RIGHTS control data of
// the same sendmsg, one message per import.
struct GbmImportRequest
{
uint32_t Magic; // 'M','G','G','I'
uint32_t Version;
uint32_t Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
uint32_t TexName; // THIS side's texture name; the server resolves its twin
uint32_t Width;
uint32_t Height;
uint32_t FourCC;
uint32_t PlaneCount;
int32_t Stride[4];
int32_t Offset[4];
uint64_t Modifier[4];
uint32_t HasModifier[4];
} __attribute__((packed));
struct GbmImportReply
{
uint32_t Magic; // 'I','G','G','R'
uint32_t Version;
uint32_t Ok;
uint32_t Reserved;
} __attribute__((packed));
static constexpr uint32_t GbmImportRequestMagic = 0x4947474Du; // "MGGI"
static constexpr uint32_t GbmImportReplyMagic = 0x52474749u; // "IGGR"
static constexpr uint32_t GbmImportMaxPlanes = 4;
// An AHardwareBuffer native handle as it appears on the wire: three counts
// followed by numFds file descriptors and numInts integers. The counts are
// 32 bit even on 64 bit Android, which is why they are spelled out here
// rather than using the platform's native_handle_t.
struct AHBNativeHandleHeader
{
int32_t Version;
int32_t NumFds;
int32_t NumInts;
} __attribute__((packed));
static constexpr uint32_t GbmMaxPlanes = 4;
// How one plane of a buffer is laid out inside the single allocation that backs
// it. Multi-plane formats keep every plane in one dma-buf and describe the
// separation with Offsets, which is the layout a modifier of
// DRM_FORMAT_MOD_LINEAR can express and the only one this library produces.
struct GbmPlaneLayout
{
Uint32 Stride = 0;
Uint32 Offset = 0;
SizeT Size = 0;
};
// One frame the phone offered, as this process holds it: the offer's own
// description of the memory, plus the descriptor this library duped out of the
// handle the phone sent. It owns that descriptor for as long as it lives, so a
// caller that takes a frame and then fails still closes it, and the descriptor the
// phone sent is closed with the rest of the message's descriptors rather than kept.
struct GbmHostFrame
{
Uint32 Seq = 0;
Uint32 Index = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 StrideBytes = 0;
Uint32 Format = 0;
// Bytes per pixel of plane 0. The offer carries a fourcc, so it comes from the
// format table when there is an entry for it and from the stride otherwise.
Uint32 BytesPerPixel = 0;
Uint64 Usage = 0;
// Which connection offered the frame. A release names a frame, and a frame only
// means anything to the connection that offered it.
Uint64 Generation = 0;
// The duped dma-buf descriptor, and how many bytes it actually backs as asked
// of the descriptor itself rather than computed from the offered geometry.
Int Fd = -1;
SizeT Size = 0;
// Move-only, because two records naming one descriptor would close it twice.
GbmHostFrame() = default;
GbmHostFrame(const GbmHostFrame&) = delete;
GbmHostFrame& operator=(const GbmHostFrame&) = delete;
GbmHostFrame(GbmHostFrame&& other) noexcept;
GbmHostFrame& operator=(GbmHostFrame&& other) noexcept;
~GbmHostFrame();
};
// One imported or locally allocated buffer. The struct is private to this
// library; gbm_bo is the public opaque name and is defined in terms of it.
struct GbmBufferObject
{
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Format = 0;
Uint32 Stride = 0;
Uint64 Modifier = DrmFormatModLinear;
Uint64 Usage = 0;
Int PlaneCount = 1;
// Bytes per pixel of plane 0. Kept beside the layout because gbm_bo_map
// needs it to place a sub-rectangle, and because a format this library can
// allocate but cannot describe in bytes would be a format it must refuse.
Uint32 BytesPerPixel = 0;
// Fds owned by this record. get_fd and get_fd_for_plane hand out dups so
// that a caller closing its fd can never invalidate the buffer. Index 0 is
// the only one a self-allocated buffer has: every plane of a linear image
// lives in that one dma-buf, and Fds[1..] stay -1 for it. An import with
// several descriptors fills several entries, and the destructor closes
// every entry that is not -1.
Int Fds[GbmMaxPlanes] = {-1, -1, -1, -1};
Uint32 Strides[GbmMaxPlanes] = {0, 0, 0, 0};
Uint32 Offsets[GbmMaxPlanes] = {0, 0, 0, 0};
Uint64 Modifiers[GbmMaxPlanes] = {0, 0, 0, 0};
// The number of bytes the descriptors actually back, rounded up to the page
// size because that is what both backends allocate. It is the only length
// that is safe to mmap: a mapping longer than the object faults on the last
// page, and a shorter one would hide the caller's own stride arithmetic.
SizeT AllocationSize = 0;
GbmBackend Backend = GbmBackend::None;
// Set only for host frames: the slot index and sequence number that the
// release message has to mention so the phone can recycle the right slot, and
// the connection the frame arrived on. A frame left over from a connection
// that has since died is never released into the one that replaced it, because
// that would free a slot belonging to another session.
Uint32 HostIndex = 0;
Uint32 HostSeq = 0;
Uint64 HostGeneration = 0;
Bool HostOwned = false;
// How many maps are outstanding. The mapping itself is kept here so that
// unmap can find and tear down the right one.
Int MapRefCount = 0;
void* MapAddress = nullptr;
SizeT MapSize = 0;
// Surfaces keep their buffers alive by count rather than by ownership, so
// the device knows when a bo is truly unreferenced.
Int RefCount = 1;
// gbm_bo_set_user_data's pair. The destroy callback is invoked with the
// gbm_bo that is going away, once, from the destructor below.
void* UserData = nullptr;
void (*UserDataDestroy)(::gbm_bo*, void*) = nullptr;
::gbm_device* Owner = nullptr;
// The record releases its own descriptors and its own mapping, so that no
// failure path anywhere in this library can leak either one - an allocation
// that fails half way through construction still cleans up when the record
// it was building is destroyed.
~GbmBufferObject();
};
// A real swapchain. The compositor asks for a buffer to render into, presents
// it, and later returns it; the three states below are exactly the states the
// GBM API can express.
struct GbmSurface
{
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Format = 0;
Uint32 Flags = 0;
std::vector<::gbm_bo*> Buffers;
// Index of the buffer currently handed to the compositor as the front
// buffer, or -1 when nothing has been locked.
Int LockedIndex = -1;
// Indices of buffers the compositor has released and that may be handed
// out again, plus the one that is locked.
std::vector<Bool> Free;
::gbm_device* Owner = nullptr;
// Dropping the surface's references here is what keeps the creation path
// free of a second failure path: if the third swapchain buffer cannot be
// allocated, unwinding the half-built surface returns the two that exist.
~GbmSurface();
};
// The device ties one backend choice, one host channel and one mutex together.
// The single mutex is deliberate: GBM calls arrive from the compositor's
// render thread and from its main thread, and the state they touch (the bo
// table, the backends) is small enough that one lock is cheaper than several.
struct GbmDevice
{
// The fd this device was created with. It is a private dup taken at
// creation, never the caller's descriptor, so gbm_device_destroy can close
// it without ever closing an fd the caller still has open. -1 when the
// caller passed none and no render node could be opened: the self-allocated
// backend does not need a DRM node, so that is a working device and not an
// error.
Int DeviceFd = -1;
// The dma-heap node this device allocates from, or -1 when none could be
// opened, in which case every allocation falls back to a memfd.
Int HeapFd = -1;
String HeapPath;
// Where the display host is reached, as configured when the device was created.
String SocketPath;
GbmBackend DefaultBackend = GbmBackend::DmaHeap;
// True while the channel below is connected and frames are arriving over it.
// False means every allocation this device answers is answered locally.
Bool HostAvailable = false;
// The name gbm_device_get_backend_name reports. It names what this device
// actually does, because a compositor that logs it is asking exactly that - so
// it changes when the host channel connects and when that connection dies, and
// SelfAllocatedBackendName is what it falls back to when it does.
String BackendName = "mobilegl";
String SelfAllocatedBackendName = "mobilegl";
std::mutex Mutex;
std::unordered_map<::gbm_bo*, GbmBufferObject*> Buffers;
std::unordered_map<::gbm_surface*, GbmSurface*> Surfaces;
// The host channel is a separate translation unit so that the socket protocol
// never leaks into the allocation paths. It lives for as long as the device
// does and is reached only under this device's mutex, which is what lets the
// allocation path ask it for a frame without a second lock to order against.
class GbmHostChannel* Host = nullptr;
// Closes the heap node and the private device dup, so that a device which
// fails to finish constructing still releases the descriptors it opened.
~GbmDevice();
};
// ---- format description ------------------------------------------------
// Fills planes[]/planeCount/totalBytes with the linear layout this library
// would allocate for format at width x height, and returns false for a format
// it cannot lay out. gbm_bo_create refuses a format this refuses, because a
// buffer whose plane layout was guessed is worse than a buffer that was not
// handed out at all.
Bool GbmDescribeFormat(Uint32 format, Uint32 width, Uint32 height, GbmPlaneLayout planes[GbmMaxPlanes],
Int& planeCount, SizeT& totalBytes) noexcept;
// Whether this library can allocate the format at all.
Bool GbmFormatIsSupported(Uint32 format) noexcept;
// Bytes per pixel of plane 0: what gbm_bo_map needs to place a sub-rectangle,
// and half of what gbm_bo_get_bpp reports.
Uint32 GbmFormatBytesPerPixel(Uint32 format) noexcept;
// ---- records and the self-allocated backend ----------------------------
// Builds a device. The only helper here that is called without the mutex,
// because no other thread can see the device it is building.
::gbm_device* GbmDeviceCreate(Int fd) noexcept;
// Tears the device down: every remaining surface, every remaining buffer, the
// heap node, the private device fd, and the record itself.
void GbmDeviceDestroy(::gbm_device& device) noexcept;
// Allocates a buffer from the device's heap node, or from a memfd when there
// is none, and records it in the device's table. Returns null with errno set.
::gbm_bo* GbmBufferCreate(::gbm_device& device, Uint32 width, Uint32 height, Uint32 format, Uint64 usage,
const Uint64* modifiers, Uint32 modifierCount) noexcept;
// Wraps descriptors the caller already owns. The descriptors are duped, so the
// caller keeps ownership of its own; type is GBM_BO_IMPORT_FD or
// GBM_BO_IMPORT_FD_MODIFIER.
::gbm_bo* GbmBufferImport(::gbm_device& device, Uint32 type, const void* buffer, Uint64 usage) noexcept;
// Drops one reference, destroying the buffer when it was the last one.
void GbmBufferRelease(::gbm_device& device, ::gbm_bo& bo) noexcept;
// Destroys a buffer outright, whatever its reference count says. Only the
// device's own teardown uses this: by then no caller's reference is meaningful.
void GbmBufferForceDestroy(::gbm_device& device, ::gbm_bo& bo) noexcept;
// Hands out a dup of the descriptor backing a plane, which the caller then owns.
// A plane this buffer does not have is refused with errno = EINVAL.
Int GbmBufferDupFd(::gbm_bo& bo, Int plane) noexcept;
// Maps the whole buffer once and counts the call; strideOut receives the
// stride of plane 0. The rect (x, y) is applied to the returned address.
void* GbmBufferMap(::gbm_bo& bo, Uint32 x, Uint32 y, Uint32& strideOut) noexcept;
// Drops one mapping; the last one unmaps.
void GbmBufferUnmap(::gbm_bo& bo, void* mapData) noexcept;
// ---- host frames -------------------------------------------------------
// Builds the channel to the display host and makes one attempt to connect it, then
// stores it in the device. A machine that is not the container half of a split run
// has nothing at that path, and that is a device which allocates locally from then
// on - not a device that failed to be created.
void GbmHostChannelCreate(::gbm_device& device) noexcept;
// Closes the channel and releases what it holds. Called from the device's
// destructor, after the buffers that still name offered frames are gone, so that
// their releases can still be sent.
void GbmHostChannelDestroy(::gbm_device& device) noexcept;
// Takes the next frame the host offered, or returns false when none is waiting or
// the channel is down. Never blocks: the socket is non-blocking, so a caller that
// finds nothing allocates locally in the same call, which is what makes this usable
// from a compositor's frame loop.
Bool GbmHostChannelTakeFrame(::gbm_device& device, GbmHostFrame& frameOut) noexcept;
// Tells the host that a frame may be recycled. Called from the buffer record's
// destructor, so a frame goes back exactly when nothing in this process can still
// read it, and only into the connection that offered it.
void GbmHostChannelReleaseFrame(::gbm_device& device, Uint32 seq, Uint64 generation) noexcept;
// ---- swapchains --------------------------------------------------------
// Builds a surface and its swapchain up front, so that
// gbm_surface_has_free_buffers can answer truthfully from the first call.
::gbm_surface* GbmSurfaceCreate(::gbm_device& device, Uint32 width, Uint32 height, Uint32 format, Uint64 usage,
const Uint64* modifiers, Uint32 modifierCount) noexcept;
// Unregisters the surface and destroys it, returning the swapchain buffers it
// still owns to the device.
void GbmSurfaceDestroy(::gbm_surface& surface) noexcept;
// Hands out the next free swapchain buffer with a reference of its own, or
// null with errno = EBUSY when the compositor still holds every one of them.
::gbm_bo* GbmSurfaceLockFront(::gbm_surface& surface) noexcept;
// Takes a locked buffer back, dropping the reference the lock handed out.
void GbmSurfaceReleaseBuffer(::gbm_surface& surface, ::gbm_bo& bo) noexcept;
Bool GbmSurfaceHasFreeBuffers(::gbm_surface& surface) noexcept;
} // namespace MobileGL::MG_Gbm
// The public opaque types are the internal records. Callers only ever see the
// pointers, so this costs nothing and removes a layer of casting.
struct gbm_device
{
MobileGL::MG_Gbm::GbmDevice Impl;
};
struct gbm_bo
{
MobileGL::MG_Gbm::GbmBufferObject Impl;
};
struct gbm_surface
{
MobileGL::MG_Gbm::GbmSurface Impl;
};
// End of Source File Header
+363 -28
View File
@@ -7,6 +7,8 @@
// End of Source File Header
#include "EGLImpl.h"
#include "EGLPlatformExtensions.h"
#include "WaylandWindow.h"
#include "../GetProcAddress.h"
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
@@ -20,6 +22,36 @@
#include <sstream>
#include <type_traits>
// The KHR spellings of the image entry points, exported by Exporting/Definitions.cpp. Declared
// here because the bundled EGL headers declare them only under EGL_EGLEXT_PROTOTYPES, which no
// build of this library defines, and because eglGetProcAddress below answers them by name.
extern "C" EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image);
// The swap-and-sync family the compositor resolves by name while bringing its EGL backend up; defined
// in Exporting/Definitions.cpp and answered from GetProcAddress below (see the definitions).
extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
EGLint n_rects);
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
EGLint n_rects);
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
extern "C" EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
// THE DEVICE-QUERY FAMILY, exported by Exporting/Definitions.cpp and answered by name below: kwin
// reaches eglQueryDisplayAttribEXT through libepoxy's pointer (that is how finding a render device
// starts), and libepoxy asks for it by name. See the definitions for why these had to exist.
extern "C" EGLBoolean eglQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices);
extern "C" EGLBoolean eglQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value);
extern "C" const char* eglQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name);
extern "C" EGLBoolean eglQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value);
extern "C" EGLSurface eglCreatePlatformWindowSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_window,
const EGLAttrib* attrib_list);
extern "C" EGLSurface eglCreatePlatformPixmapSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
const EGLAttrib* attrib_list);
namespace MobileGL::MG_Impl::EGLImpl {
namespace {
using EGLStateContext = MG_State::EGLState::EGLContext;
@@ -116,6 +148,73 @@ namespace MobileGL::MG_Impl::EGLImpl {
return reinterpret_cast<void*>(static_cast<SizeT>(nativeHandle));
}
}
#if MOBILEGL_WAYLAND_WINDOWS
// Every Wayland window surface's presentation, by surface. Guarded by EGLOperationMutex,
// which every entry point that reaches it holds.
UnorderedMap<EGLSurface, UniquePtr<Wayland::WindowSurface>>& WaylandSurfaces() {
static auto* surfaces = new UnorderedMap<EGLSurface, UniquePtr<Wayland::WindowSurface>>();
return *surfaces;
}
// A WINDOW ON A WAYLAND DISPLAY. Whatever MOBILEGL_IPC_SURFACE says, the window is this
// application's own wl_surface, so neither the server's window nor the display host's frame
// is what it shows: the backend draws into a pbuffer of the window's size, and each swap
// reads the frame back and attaches it to the wl_surface (WaylandWindow.h). Answers false
// when the display is not a Wayland one; otherwise *out is the surface, or EGL_NO_SURFACE
// with the error set.
template <typename State>
Bool CreateWaylandWindowSurface(State* state, EGLDisplay dpy, EGLConfig config, void* window,
Bool platformWindow, EGLSurface* out) {
Uint64 nativeDisplay = 0;
EGLenum platform = EGL_NONE;
if (!state->GetDisplayNative(dpy, &nativeDisplay, &platform) ||
!Wayland::IsWaylandDisplay(nativeDisplay, platform)) {
return false;
}
*out = EGL_NO_SURFACE;
if (!state->IsDisplayInitialized(dpy)) {
state->SetError(EGL_NOT_INITIALIZED);
return true;
}
if (!state->ValidateConfigOnDisplay(dpy, config)) {
state->SetError(EGL_BAD_CONFIG);
return true;
}
EGLint width = 0;
EGLint height = 0;
if (!Wayland::WindowSize(window, &width, &height)) {
state->SetError(EGL_BAD_NATIVE_WINDOW);
return true;
}
EGLint alpha = 0;
(void)state->GetConfigAttrib(dpy, config, EGL_ALPHA_SIZE, &alpha);
auto presentation =
Wayland::WindowSurface::Create(reinterpret_cast<void*>(static_cast<uintptr_t>(nativeDisplay)), window,
width, height, alpha > 0);
if (!presentation) {
state->SetError(EGL_BAD_NATIVE_WINDOW);
return true;
}
const EGLSurface surface =
platformWindow ? state->CreatePlatformWindowSurface(dpy, config, window, nullptr)
: state->CreateWindowSurface(
dpy, config, (NativeWindowType)(reinterpret_cast<uintptr_t>(window)), nullptr);
if (surface == EGL_NO_SURFACE) return true;
(void)state->ResizeSurface(dpy, surface, width, height);
auto* backendObject = GetBackendObject(state);
if (!backendObject || !backendObject->CreateEGLPbufferSurface(surface, width, height)) {
MGLOG_E("eglCreateWindowSurface: the backend refused the %dx%d drawable behind a Wayland window",
width, height);
state->DestroySurface(dpy, surface);
state->SetError(EGL_BAD_ALLOC);
return true;
}
WaylandSurfaces()[surface] = std::move(presentation);
*out = surface;
return true;
}
#endif
} // namespace
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
@@ -124,6 +223,16 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
#if MOBILEGL_WAYLAND_WINDOWS
{
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
EGLSurface waylandSurface = EGL_NO_SURFACE;
if (CreateWaylandWindowSurface(state, dpy, config, ToVoidHandle(window), /*platformWindow=*/false,
&waylandSurface)) {
return waylandSurface;
}
}
#endif
if (!state->IsDisplayInitialized(dpy)) {
state->SetError(EGL_NOT_INITIALIZED);
return EGL_NO_SURFACE;
@@ -137,8 +246,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
// the window is the SERVER's: a headless client passes none, so NULL is not
// EGL_BAD_NATIVE_WINDOW here. Whatever the client passed never reaches the wire - the
// remote backend sends WindowKind::ServerOwned with token 0 (BackendObject_Remote).
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
if (IsNullNativeHandle(window) && !serverOwnedWindow) {
// Either on-screen shape leaves this side without a window of its own: =server asks the
// server for its own window, =host draws into the frames the display host offers.
const Bool windowlessOnScreen =
MG_Config::ServerOwnedWindowSurfaces() || MG_Config::HostFrameWindowSurfaces();
if (IsNullNativeHandle(window) && !windowlessOnScreen) {
#else
if (IsNullNativeHandle(window)) {
#endif
@@ -157,15 +269,15 @@ namespace MobileGL::MG_Impl::EGLImpl {
// alike; the same game in a landscape window was right). The native window it handed us
// knows the size the game believes it has: ask for THAT, and the server fixes its window
// buffer to it and aspect-fits the view (MobileGLDisplayActivity), letterboxed.
if (serverOwnedWindow && requestedWidth == 0 && requestedHeight == 0 && !IsNullNativeHandle(window)) {
if (windowlessOnScreen && requestedWidth == 0 && requestedHeight == 0 && !IsNullNativeHandle(window)) {
auto* native = reinterpret_cast<ANativeWindow*>(ToVoidHandle(window));
const int32_t nativeWidth = ANativeWindow_getWidth(native);
const int32_t nativeHeight = ANativeWindow_getHeight(native);
if (nativeWidth > 0 && nativeHeight > 0) {
requestedWidth = static_cast<Uint32>(nativeWidth);
requestedHeight = static_cast<Uint32>(nativeHeight);
MGLOG_I("eglCreateWindowSurface: no EGL_WIDTH/EGL_HEIGHT; the server-owned window is asked for the "
"native window's %dx%d", nativeWidth, nativeHeight);
MGLOG_I("eglCreateWindowSurface: no EGL_WIDTH/EGL_HEIGHT; the on-screen surface is asked for "
"the native window's %dx%d", nativeWidth, nativeHeight);
}
}
#endif
@@ -177,11 +289,20 @@ namespace MobileGL::MG_Impl::EGLImpl {
};
#if MOBILEGL_BUILD_DISAGGREGATED
EGLSurface surface = serverOwnedWindow
? state->CreateServerOwnedWindowSurface(
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false)
: state->CreateWindowSurface(dpy, config, window, attrib_list);
EGLSurface surface = EGL_NO_SURFACE;
if (MG_Config::ServerOwnedWindowSurfaces()) {
// The server's own window: it makes one and draws into it.
surface = state->CreateServerOwnedWindowSurface(
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false);
} else if (MG_Config::HostFrameWindowSurfaces()) {
// The display host's frames, as above: no window of the client's either way.
surface = state->CreateHostFrameWindowSurface(
dpy, config, windowHandle.Handle, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/false);
} else {
surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
}
#else
EGLSurface surface = state->CreateWindowSurface(dpy, config, window, attrib_list);
#endif
@@ -211,6 +332,7 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_FALSE;
}
if (!state->ValidateSurfaceOnDisplay(dpy, draw)) {
MGLOG_E_ONCE("eglSwapBuffers: surface %p is not a surface of display %p (EGL_BAD_SURFACE)", draw, dpy);
state->SetError(EGL_BAD_SURFACE);
return EGL_FALSE;
}
@@ -220,6 +342,11 @@ namespace MobileGL::MG_Impl::EGLImpl {
MGLOG_E_ONCE("activeBackendObject not initialized!");
return EGL_FALSE;
}
#if MOBILEGL_WAYLAND_WINDOWS
// A Wayland window shows what is attached to it: the frame is read back and attached
// BEFORE the swap, while it is still the drawable's content.
if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end()) (void)it->second->Present();
#endif
if (!backendObject->SwapEGLBuffers(dpy, draw)) {
MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw);
state->SetError(EGL_BAD_SURFACE);
@@ -230,7 +357,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLBoolean ChooseConfig(EGLDisplay dpy, const EGLint* attrib_list, EGLConfig* configs, EGLint config_size,
EGLint* num_config) {
auto* state = GetState();
// eglChooseConfig can be an application's first EGL call after eglGetPlatformDisplay + eglInitialize,
// and for a display built from an EGLDeviceEXT nothing has brought MobileGL up yet: GetState()
// then answers NULL and the call lands in EGLContext::ChooseConfig with a null this - measured as
// a crash inside that function (pc/lr both resolved to it) with kwin's own "pEGLContext is null.
// MG_State may not be initialized." as the only warning, and reported one layer up as
// "Could not initialize rendering context".
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_FALSE;
}
@@ -238,11 +371,26 @@ namespace MobileGL::MG_Impl::EGLImpl {
}
EGLContext CreateContext(EGLDisplay dpy, EGLConfig config, EGLContext shareCtx, const EGLint* attrib_list) {
auto* state = GetState();
// WHICH CONTEXT IS BEING ASKED FOR, and what came back. kwin's OpenGL compositing fails with
// "Could not initialize rendering context" while the same path succeeds from a probe against
// this same library (probe10: a 3.3 core context on a GBM surface, makeCurrent answered 1),
// so the difference has to be in this argument list - and an EGL vendor cannot see it from the
// outside. The list is dumped in EGL_NONE-terminated pairs; EGL_NONE is 0x3038, not 0.
MGLOG_I("eglCreateContext(dpy=%p, config=%p, share=%p)", dpy, config, shareCtx);
if (attrib_list != nullptr) {
for (int index = 0; attrib_list[index] != EGL_NONE; index += 2) {
MGLOG_I(" attrib 0x%04x = %d", attrib_list[index], attrib_list[index + 1]);
}
} else {
MGLOG_I(" (no attribute list)");
}
auto* state = GetStateEnsureInitialized();
if (!state) {
return EGL_NO_CONTEXT;
}
return state->CreateContext(dpy, config, shareCtx, attrib_list);
EGLContext context = state->CreateContext(dpy, config, shareCtx, attrib_list);
MGLOG_I("eglCreateContext -> %p", static_cast<void*>(context));
return context;
}
EGLBoolean Initialize(EGLDisplay dpy, EGLint* major, EGLint* minor) {
@@ -282,6 +430,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
return state->ConsumeError();
}
// THE DISPLAY HOST'S FRAME, AS THE COMPOSITOR PRESENTS IT. Set by the surfaceless-bind hook in
// MakeCurrent when this client (the compositor) takes the host frame; read by glFlush, which is
// that compositor's only frame boundary - it never calls eglSwapBuffers, and without a present
// the display host is never told a frame is done, so nothing ever reaches the glass.
EGLDisplay g_hostFramePresentDisplay = nullptr;
EGLSurface g_hostFramePresentSurface = nullptr;
EGLBoolean MakeCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
auto* state = GetState();
@@ -298,6 +453,81 @@ namespace MobileGL::MG_Impl::EGLImpl {
MGLOG_D("eglMakeCurrent begin thread=%s dpy=%p draw=%p read=%p ctx=%p oldDpy=%p oldDraw=%p oldRead=%p oldCtx=%p",
threadId.c_str(), dpy, draw, read, ctx, oldDisplay, oldDraw, oldRead, oldContext);
#if MOBILEGL_BUILD_DISAGGREGATED
// THE DISPLAY HOST'S FRAMES, TO A CLIENT THAT ASKS FOR NO SURFACE.
//
// A compositor on this route never calls eglCreateWindowSurface: it imports the display host's
// dma-bufs and draws into them through framebuffers, binding its contexts with EGL_NO_SURFACE.
// The host frame bridge, though, is opened by exactly one thing - a surface being CREATED and
// activated (ServerLoop's CreateWindowSurface with the host-frame window backend ->
// InitHostFrameSurface) - so with no surface ever created the bridge stayed shut, the display
// host kept offering frames into a name nobody served ("offer buffer 0: bridge fd=-1"), and no
// frame could reach the glass no matter how much was drawn. So: when the session asked for
// host-framed surfaces and a real context is being bound with no surface, make it the surface
// eglCreatePlatformWindowSurface would have made, once, and bind that. The size is the display
// host's own frame size; the client's window value is not on this path at all.
// ONLY THE COMPOSITOR TAKES THE HOST FRAME. A desktop is full of processes that bind a
// context with no surface - konsole, plasmashell, kactivitymanagerd - and measured with
// probe15 as the second client: such a bind from a NON-compositor process made the host-frame
// activation fail (makeCurrent=0) and the process aborted. The compositor is marked by its
// launcher with MOBILEGL_HOST_FRAME_CLIENT=1; everyone else keeps the plain surfaceless bind,
// which is all they ever wanted.
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT &&
MG_Config::HostFrameWindowSurfaces() &&
(std::getenv("MOBILEGL_HOST_FRAME_CLIENT") != nullptr &&
std::string("1") == std::getenv("MOBILEGL_HOST_FRAME_CLIENT"))) {
static Bool hostFrameSurfaceTaken = false;
if (!hostFrameSurfaceTaken) {
hostFrameSurfaceTaken = true;
EGLConfig hostFrameConfig = nullptr;
EGLint hostFrameConfigCount = 0;
const EGLint hostFrameChoose[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_NONE};
if (state->ChooseConfig(dpy, hostFrameChoose, &hostFrameConfig, 1,
&hostFrameConfigCount) &&
hostFrameConfigCount > 0 && hostFrameConfig != nullptr) {
// BOTH HALVES, exactly as eglCreateWindowSurface makes one: the EGL state's
// registration (without it every later bind and swap of this surface was refused
// as EGL_BAD_SURFACE - measured: "eglMakeCurrent rejected by EGLState
// error=0x300d", and glFlush's swaps returned before reaching the backend), then
// the backend's own window-surface entry, which the remote backend routes to the
// host frame (BackendObject_Remote::CreateHostFrameWindowSurface) - the call that
// puts a CreateWindowSurface frame with the host-frame window backend on the wire.
const EGLSurface hostFrameSurface = state->CreateHostFrameWindowSurface(
dpy, hostFrameConfig, nullptr, 1440, 3200, /*platformWindow=*/false);
MG_Backend::WindowHandle hostFrameHandle{};
hostFrameHandle.Width = 1440;
hostFrameHandle.Height = 3200;
if (hostFrameSurface == EGL_NO_SURFACE) {
MGLOG_E("host frame: the EGL state refused the display host's frame surface");
} else if (!MG_Backend::pActiveBackendObject->CreateEGLWindowSurface(hostFrameSurface,
hostFrameHandle)) {
MGLOG_E("host frame: the backend refused the display host's frame surface");
state->DestroySurface(dpy, hostFrameSurface);
} else {
// The frame boundary of a compositor that never swaps is its glFlush. glFlush
// has no EGL context of its own, so remember HERE which surface presents the
// display host's frame; glFlush presents it (see the host-frame present in
// glFlush's implementation).
g_hostFramePresentDisplay = dpy;
g_hostFramePresentSurface = hostFrameSurface;
MGLOG_I("host frame: the first surfaceless eglMakeCurrent made the display host's "
"frame surface %p (%dx%d)",
hostFrameSurface, 1440, 3200);
}
} else {
MGLOG_E("host frame: no config for the display host's frame surface");
}
}
// EVERY surfaceless bind, not only the first: the compositor rebinds with EGL_NO_SURFACE
// all the time, and a bind without the surface leaves nothing current for glFlush's swap
// to present (BackendObject::SwapEGLBuffers: "draw surface is not current on this thread").
if (g_hostFramePresentSurface != nullptr && dpy == g_hostFramePresentDisplay) {
draw = g_hostFramePresentSurface;
read = g_hostFramePresentSurface;
}
}
#endif
if (!state->MakeCurrent(dpy, draw, read, ctx)) {
const EGLint error = state->ConsumeError();
MGLOG_D("eglMakeCurrent rejected by EGLState thread=%s error=0x%04x", threadId.c_str(), error);
@@ -355,6 +585,9 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state->DestroySurface(dpy, surface)) {
return EGL_FALSE;
}
#if MOBILEGL_WAYLAND_WINDOWS
WaylandSurfaces().erase(surface);
#endif
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
backendObject->ReleaseEGLSurface(surface);
}
@@ -463,14 +696,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
case EGL_CLIENT_APIS:
return "OpenGL OpenGL_ES";
case EGL_EXTENSIONS:
if (display == EGL_NO_DISPLAY) {
return "EGL_EXT_client_extensions "
"EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless";
}
return "EGL_KHR_create_context "
"EGL_MESA_platform_surfaceless";
// One list, in EGLPlatformExtensions.h, because the vendor string glvnd reads and
// this query an application reads have to agree: an application that does not see the
// platform extension here never asks for a platform display at all.
return display == EGL_NO_DISPLAY ? kClientExtensionString : kDisplayExtensionString;
default:
state->SetError(EGL_BAD_PARAMETER);
return nullptr;
@@ -705,6 +934,23 @@ namespace MobileGL::MG_Impl::EGLImpl {
return state->DestroyImage(dpy, image) ? EGL_TRUE : EGL_FALSE;
}
EGLImageKHR CreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list) {
// EGL_KHR_image_base spells its attribute list in EGLint pairs and the core eglCreateImage
// in EGLAttrib pairs, and the two are not the same memory: on a 64-bit build one entry is
// twice the width of the other, so the caller's array cannot be handed over as it stands.
// Re-spelling it is therefore part of the entry point rather than a detail of the state.
Vector<EGLAttrib> attribs;
if (attrib_list != nullptr) {
for (SizeT i = 0; attrib_list[i] != EGL_NONE; i += 2) {
attribs.push_back(static_cast<EGLAttrib>(attrib_list[i]));
attribs.push_back(static_cast<EGLAttrib>(attrib_list[i + 1]));
}
attribs.push_back(static_cast<EGLAttrib>(EGL_NONE));
}
return CreateImage(dpy, ctx, target, buffer, attribs.empty() ? nullptr : attribs.data());
}
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
(void)attrib_list;
@@ -721,11 +967,24 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_NO_SURFACE;
}
#if MOBILEGL_WAYLAND_WINDOWS
{
const std::lock_guard<std::recursive_mutex> operationLock(EGLOperationMutex());
EGLSurface waylandSurface = EGL_NO_SURFACE;
if (CreateWaylandWindowSurface(state, dpy, config, native_window, /*platformWindow=*/true,
&waylandSurface)) {
return waylandSurface;
}
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 (on-screen server window), D1: as CreateWindowSurface - with MOBILEGL_IPC_SURFACE=server
// the window is the server's, and a headless client's NULL is accepted.
const Bool serverOwnedWindow = MG_Config::ServerOwnedWindowSurfaces();
if (native_window == nullptr && !serverOwnedWindow) {
// Either on-screen shape leaves this side without a window of its own: =server asks the
// server for its own window, =host draws into the frames the display host offers.
const Bool windowlessOnScreen =
MG_Config::ServerOwnedWindowSurfaces() || MG_Config::HostFrameWindowSurfaces();
if (native_window == nullptr && !windowlessOnScreen) {
#else
if (native_window == nullptr) {
#endif
@@ -749,11 +1008,21 @@ namespace MobileGL::MG_Impl::EGLImpl {
};
#if MOBILEGL_BUILD_DISAGGREGATED
EGLSurface surface = serverOwnedWindow
? state->CreateServerOwnedWindowSurface(
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true)
: state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
EGLSurface surface = EGL_NO_SURFACE;
if (MG_Config::ServerOwnedWindowSurfaces()) {
// The server's own window: it makes one and draws into it.
surface = state->CreateServerOwnedWindowSurface(
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true);
} else if (MG_Config::HostFrameWindowSurfaces()) {
// The display host's frames: it allocates them, the server draws into them,
// and the host puts each one on the glass when the server says it is drawn.
surface = state->CreateHostFrameWindowSurface(
dpy, config, native_window, static_cast<EGLint>(windowHandle.Width),
static_cast<EGLint>(windowHandle.Height), /*platformWindow=*/true);
} else {
surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
}
#else
EGLSurface surface = state->CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
#endif
@@ -822,6 +1091,72 @@ namespace MobileGL::MG_Impl::EGLImpl {
MobileGL::EnsureInitialized();
MGLOG_D("eglGetProcAddress(%s)", name);
// THE KHR SPELLINGS ARE ENTRY POINTS OF THEIR OWN, and by name is the only way anything
// asks for them. Measured in the Fedora container that runs this library as the glvnd EGL
// vendor, before they were answered here: eglGetProcAddress("eglCreateImageKHR") -> NULL and
// eglGetProcAddress("eglDestroyImageKHR") -> NULL. kwin_wayland's anland backend imports
// the display daemon's dma-bufs through exactly these two (AnlandEglLayer::importBuffers ->
// EglBackend::importDmaBufAsTexture -> EglDisplay::importBufferAsImage ->
// EglDisplay::createImage, which calls the pointer without a null check), so a NULL here is
// a compositor that cannot draw a single pixel into the frames the daemon hands it. The
// core spellings were already in the table below; these are the same functions under the
// names EGL_KHR_image_base gives them, which is also why they answer in the glvnd vendor
// table beside them (Exporting/GlvndVendor.cpp).
if (std::strcmp(name, "eglCreateImageKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreateImageKHR);
}
if (std::strcmp(name, "eglDestroyImageKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglDestroyImageKHR);
}
// The device-query family and the EXT platform-surface spellings. Measured in the container
// before they were answered here: eglGetProcAddress returned NULL for eglQueryDisplayAttribEXT,
// eglQueryDevicesEXT, eglQueryDeviceAttribEXT, eglQueryDeviceStringEXT and
// eglCreatePlatformWindowSurfaceEXT while the extension strings announced EGL_EXT_device_query -
// a NULL libepoxy then called (kwin crashed at pc=0), and withdrawing the announcement instead
// left kwin without a render device and its compositing inactive.
if (std::strcmp(name, "eglQueryDevicesEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDevicesEXT);
}
if (std::strcmp(name, "eglQueryDeviceAttribEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDeviceAttribEXT);
}
if (std::strcmp(name, "eglQueryDeviceStringEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDeviceStringEXT);
}
if (std::strcmp(name, "eglQueryDisplayAttribEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglQueryDisplayAttribEXT);
}
if (std::strcmp(name, "eglCreatePlatformWindowSurfaceEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreatePlatformWindowSurfaceEXT);
}
if (std::strcmp(name, "eglCreatePlatformPixmapSurfaceEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreatePlatformPixmapSurfaceEXT);
}
// The swap-and-sync spellings kwin resolves by name while it is still bringing its EGL backend
// up (see the definitions: EGL_KHR_fence_sync, the damage variants, EGL_EXT_buffer_age).
if (std::strcmp(name, "eglSwapBuffersWithDamageKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglSwapBuffersWithDamageKHR);
}
if (std::strcmp(name, "eglSwapBuffersWithDamageEXT") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglSwapBuffersWithDamageEXT);
}
if (std::strcmp(name, "eglCreateSyncKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglCreateSyncKHR);
}
if (std::strcmp(name, "eglDestroySyncKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglDestroySyncKHR);
}
if (std::strcmp(name, "eglClientWaitSyncKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglClientWaitSyncKHR);
}
if (std::strcmp(name, "eglWaitSyncKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglWaitSyncKHR);
}
if (std::strcmp(name, "eglGetSyncAttribKHR") == 0) {
return reinterpret_cast<__eglMustCastToProperFunctionPointerType>(eglGetSyncAttribKHR);
}
void* proc = MG_Impl::GetProcAddress(name);
if (!proc) {
MGLOG_D("Failed to get function: %s", name);
+14
View File
@@ -8,8 +8,18 @@
#pragma once
#include <Includes.h>
// EGLImageKHR: the return type of the KHR spelling of eglCreateImage below. Includes.h brings in
// the core egl.h, which has no KHR types in it.
#include <EGL/eglext.h>
namespace MobileGL::MG_Impl::EGLImpl {
// The display host's frame surface, as taken by the surfaceless-bind hook in MakeCurrent, and the
// display it belongs to. glFlush reads these: a compositor's frame boundary is glFlush, and the
// host frame's "frame is done" signal IS a swap of this surface. Null until a client marked
// MOBILEGL_HOST_FRAME_CLIENT=1 actually took the frame.
extern EGLDisplay g_hostFramePresentDisplay;
extern EGLSurface g_hostFramePresentSurface;
EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
const EGLint* attrib_list);
EGLBoolean SwapBuffers(EGLDisplay dpy, EGLSurface draw);
@@ -53,6 +63,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLBoolean GetSyncAttrib(EGLDisplay dpy, EGLSync sync, EGLint attribute, EGLAttrib* value);
EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLAttrib* attrib_list);
// The same image under EGL_KHR_image_base's name and its EGLint attribute list: the two lists
// are pairs of different widths, so the caller's array is re-spelled rather than cast.
EGLImageKHR CreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list);
EGLBoolean DestroyImage(EGLDisplay dpy, EGLImage image);
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list);
EGLSurface CreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window,
@@ -0,0 +1,158 @@
// MobileGL - MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
namespace MobileGL::MG_Impl::EGLImpl {
// THE PLATFORMS THIS IMPLEMENTATION SERVES, in the words EGL spells them.
//
// Two callers need exactly the same list, and they disagree if it is written twice:
// eglQueryString(EGL_EXTENSIONS) is what an application reads BEFORE it decides to call
// eglGetPlatformDisplay at all - libepoxy refuses to use eglGetPlatformDisplayEXT unless the
// string names EGL_EXT_platform_base - and the glvnd vendor string is what the system's
// libEGL reads to decide which vendor library a platform request should be routed to. A
// vendor that does not claim the platform is never asked for it, and the caller is told there
// is no provider: the library can be loaded, initialized, and still never called.
//
// GBM is on the list because a compositor asks for its display on the GBM device it owns.
// This side treats that device the way it treats every other native display - the frames come
// from the display host, not from the device - so the claim is exact rather than optimistic.
inline constexpr const char* kDeviceExtensionString =
"EGL_EXT_device_base "
"EGL_EXT_device_enumeration "
"EGL_EXT_device_query "
"EGL_EXT_device_drm "
"EGL_EXT_device_drm_render_node";
// EGL_EXT_client_extensions IS ON THE PLATFORM LIST FOR A MEASURED REASON. libglvnd only folds
// a vendor client-extension string (the answer to eglQueryString(EGL_NO_DISPLAY, EGL_EXTENSIONS))
// into its own no-display answer when the vendor PLATFORM string - the one __egl_Main publishes
// for the platform-extensions query - names EGL_EXT_client_extensions. Without it the client
// string never reached libepoxy, so libepoxy concluded EGL_EXT_device_query was absent and left
// epoxy_eglQueryDisplayAttribEXT as a NULL pointer, while kwin (which reads the display string,
// where the extension IS present) called it: measured as signal 11, fault_addr=(nil), pc=0x0,
// from KWin::EglDisplay::determineRenderNode() through the GOT slot whose relocation is
// R_AARCH64_GLOB_DAT epoxy_eglQueryDisplayAttribEXT.
// Wayland where a Wayland client can exist (WaylandWindow.h: window surfaces presented through
// wl_shm). Without it glvnd never hands this vendor a Wayland display at all.
#if defined(__linux__) && !defined(__ANDROID__)
#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS " EGL_KHR_platform_wayland EGL_EXT_platform_wayland"
#else
#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS ""
#endif
inline constexpr const char* kPlatformExtensionString =
"EGL_EXT_client_extensions "
"EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_KHR_platform_gbm "
"EGL_EXT_platform_gbm "
"EGL_MESA_platform_gbm "
"EGL_EXT_platform_device "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
// What a display answers for EGL_EXTENSIONS: the extensions this implementation provides that
// are not platform ones, then the platform list above.
// EGL_KHR_no_config_context and EGL_KHR_surfaceless_context are on the display and client lists
// because both are answered by this implementation rather than announced by a driver: a context
// is created with EGL_NO_CONFIG_KHR and made current with EGL_NO_SURFACE, which is the pair a
// compositor asks for when it renders into framebuffers of its own. Measured on kwin_wayland
// against this vendor before they were here: createContext() refused with
// "EGL_KHR_no_config_context extension is unsupported" and the session ended at
// "no usable DRM render device; cannot bring up OpenGL compositing".
//
// EGL_KHR_image_base AND EGL_EXT_image_dma_buf_import ARE ONE FEATURE HERE, and they are on the
// list for a measured reason rather than a plausible one. In the Fedora container that runs
// this library as the glvnd EGL vendor, eglGetProcAddress("eglCreateImageKHR") and
// eglGetProcAddress("eglDestroyImageKHR") both returned NULL, and kwin_wayland 6.7.3's anland
// backend imports the display daemon's dma-bufs through exactly those two
// (AnlandEglLayer::importBuffers -> EglBackend::importDmaBufAsTexture ->
// EglDisplay::importBufferAsImage -> EglDisplay::createImage) before it can draw a pixel into a
// frame the daemon produced - so with a NULL the whole composited scene has no image to be
// drawn into. Both names are backed by entry points that exist: EGL_KHR_image_base by
// eglCreateImage/eglDestroyImage and their KHR spellings (Exporting/Definitions.cpp, and the
// same names in the glvnd vendor table), EGL_EXT_image_dma_buf_import by EGL_LINUX_DMA_BUF_EXT
// images, which carry the caller's EGL_WIDTH, EGL_HEIGHT and EGL_LINUX_DRM_FOURCC_EXT and own a
// dup() of every plane's fd, offset and pitch (EGLState/Core.cpp, CreateImage).
//
// EGL_EXT_image_dma_buf_import_modifiers IS DELIBERATELY NOT NAMED. Its own two entry points -
// eglQueryDmaBufFormatsEXT and eglQueryDmaBufModifiersEXT - do not exist in this library, and a
// name in this string means the entry points beside it are there. The modifier attributes
// (EGL_DMA_BUF_PLANEn_MODIFIER_LO/HI_EXT) are nevertheless ACCEPTED and carried on the image:
// a caller does not need the name to send them, and kwin's does not look at any extension
// string before it does - EglDisplay::importDmaBufAsImage appends the pair whenever the
// buffer's modifier is not DRM_FORMAT_MOD_INVALID.
inline constexpr const char* kDisplayExtensionString =
"EGL_KHR_create_context "
// WHAT THE COMPOSITOR CHECKS BETWEEN ITS CONTEXT AND ITS FIRST SURFACE. EglBackend::init
// asks for these three by name right after initRenderingContext() and before createSurfaces();
// the entry points behind them are published beside this string (Exporting/Definitions.cpp),
// so the announcement and the answers are one thing rather than two.
"EGL_KHR_fence_sync "
"EGL_KHR_wait_sync "
"EGL_KHR_swap_buffers_with_damage "
"EGL_EXT_swap_buffers_with_damage "
"EGL_EXT_buffer_age "
"EGL_KHR_image_base "
"EGL_EXT_image_dma_buf_import "
"EGL_KHR_no_config_context "
"EGL_KHR_surfaceless_context "
// ANNOUNCED BECAUSE IT IS NOW ANSWERED: the four names below are published as real entry
// points and eglGetProcAddress answers them (see Exporting/Definitions.cpp and
// EGLImpl::GetProcAddress). Both earlier states were measured failures: announcing the
// extension while the names resolved to NULL made kwin jump to that NULL (signal 11,
// pc=0x0, from KWin::EglDisplay::determineRenderNode()), and withdrawing the announcement
// left kwin without a render device so its compositing never became active and nothing was
// ever painted.
"EGL_EXT_device_base "
"EGL_EXT_device_enumeration "
"EGL_EXT_device_query "
"EGL_EXT_device_drm "
"EGL_EXT_device_drm_render_node "
// KWin::EglDisplay::determineRenderNode() at libkwin.so.6+0x308b9c. A compositor that is
// never told the extension exists cannot take that path; announcing it again is the right
// move once these four names are really published and resolvable.
"EGL_EXT_platform_device "
"EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_KHR_platform_gbm "
"EGL_EXT_platform_gbm "
"EGL_MESA_platform_gbm "
"EGL_MESA_platform_surfaceless";
// ... and what EGL_NO_DISPLAY answers: the same, plus the client-extension marker that tells a
// caller the no-display query is meaningful here.
inline constexpr const char* kClientExtensionString =
"EGL_EXT_client_extensions "
"EGL_KHR_create_context "
"EGL_KHR_image_base "
"EGL_EXT_image_dma_buf_import "
"EGL_KHR_no_config_context "
"EGL_KHR_surfaceless_context "
// ANNOUNCED BECAUSE IT IS NOW ANSWERED: the four names below are published as real entry
// points and eglGetProcAddress answers them (see Exporting/Definitions.cpp and
// EGLImpl::GetProcAddress). Both earlier states were measured failures: announcing the
// extension while the names resolved to NULL made kwin jump to that NULL (signal 11,
// pc=0x0, from KWin::EglDisplay::determineRenderNode()), and withdrawing the announcement
// left kwin without a render device so its compositing never became active and nothing was
// ever painted.
"EGL_EXT_device_base "
"EGL_EXT_device_enumeration "
"EGL_EXT_device_query "
"EGL_EXT_device_drm "
"EGL_EXT_device_drm_render_node "
// KWin::EglDisplay::determineRenderNode() at libkwin.so.6+0x308b9c. A compositor that is
// never told the extension exists cannot take that path; announcing it again is the right
// move once these four names are really published and resolvable.
"EGL_EXT_platform_device "
"EGL_EXT_platform_base "
"EGL_KHR_platform_base "
"EGL_KHR_platform_gbm "
"EGL_EXT_platform_gbm "
"EGL_MESA_platform_gbm "
"EGL_MESA_platform_surfaceless";
}
@@ -7,6 +7,9 @@
// End of Source File Header
#include <Includes.h>
// EGLImageKHR, for the KHR spellings of the image entry points defined at the bottom of this file:
// Includes.h brings in the core egl.h only.
#include <EGL/eglext.h>
#include "../EGLImpl.h"
MOBILEGL_EGL_API EGLSurface eglCreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window,
@@ -228,6 +231,25 @@ MOBILEGL_EGL_API EGLBoolean eglDestroyImage(EGLDisplay dpy, EGLImage image) {
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
}
// EGL_KHR_image_base's spellings of the same two entry points, exported under their own names
// because that is the only thing that answers for them: measured in the Fedora container that runs
// this library as the glvnd EGL vendor, eglGetProcAddress("eglCreateImageKHR") and
// eglGetProcAddress("eglDestroyImageKHR") both returned NULL, and kwin_wayland's anland backend
// imports the display daemon's dma-bufs through exactly these two.
MOBILEGL_EGL_API EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list) {
MGLOG_D("eglCreateImageKHR(dpy=%p, ctx=%p, target=%u, buffer=%p, attrib_list=%p)", dpy, ctx, target, buffer,
attrib_list);
// Not an alias of eglCreateImage: this list is EGLint pairs and the core one EGLAttrib pairs.
return MobileGL::MG_Impl::EGLImpl::CreateImageKHR(dpy, ctx, target, buffer, attrib_list);
}
MOBILEGL_EGL_API EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image) {
MGLOG_D("eglDestroyImageKHR(dpy=%p, image=%p)", dpy, image);
// One handle under two spellings: EGLImageKHR and EGLImage are the same pointer.
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
}
MOBILEGL_EGL_API EGLDisplay eglGetPlatformDisplay(EGLenum platform, void* native_display,
const EGLAttrib* attrib_list) {
MGLOG_D("eglGetPlatformDisplay(platform=%u, native_display=%p, attrib_list=%p)", platform, native_display,
@@ -250,6 +272,138 @@ MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurface(EGLDisplay dpy, EGLCo
return MobileGL::MG_Impl::EGLImpl::CreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
}
// THE SWAP-AND-SYNC FAMILY, which kwin asks for in EglBackend::init right after the rendering
// context is up and BEFORE it creates any surface: EGL_KHR_fence_sync, EGL_KHR_swap_buffers_with_damage
// and EGL_EXT_buffer_age are checked there, and a compositing mode whose backend init returns false
// is exactly the "Could not fulfill the requested compositing mode in KWIN_COMPOSE: 1" this run
// reports. Each of these names is a spelling of something this library already implements (the core
// eglSwapBuffers and the state layer's sync objects), so they are published as real exports rather
// than promised and not delivered.
MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
EGLint n_rects) {
// The damage rectangles are a hint about what changed; this side swaps the whole surface either
// way, so the honest answer is the core swap.
(void)rects;
(void)n_rects;
return MobileGL::MG_Impl::EGLImpl::SwapBuffers(dpy, surface) ? EGL_TRUE : EGL_FALSE;
}
MOBILEGL_EGL_API EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
EGLint n_rects) {
return eglSwapBuffersWithDamageKHR(dpy, surface, rects, n_rects);
}
MOBILEGL_EGL_API EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list) {
// EGL_KHR_fence_sync's list is EGLint pairs; the state layer takes EGLAttrib pairs.
EGLAttrib attributes[16];
int count = 0;
if (attrib_list != nullptr) {
for (int index = 0; attrib_list[index] != EGL_NONE && count + 1 < 16; index += 2) {
attributes[count++] = static_cast<EGLAttrib>(attrib_list[index]);
attributes[count++] = static_cast<EGLAttrib>(attrib_list[index + 1]);
}
}
attributes[count] = EGL_NONE;
return reinterpret_cast<EGLSyncKHR>(MobileGL::MG_Impl::EGLImpl::CreateSync(dpy, type, attributes));
}
MOBILEGL_EGL_API EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync) {
return MobileGL::MG_Impl::EGLImpl::DestroySync(dpy, reinterpret_cast<EGLSync>(sync)) ? EGL_TRUE : EGL_FALSE;
}
MOBILEGL_EGL_API EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout) {
return MobileGL::MG_Impl::EGLImpl::ClientWaitSync(dpy, reinterpret_cast<EGLSync>(sync), flags,
static_cast<EGLTime>(timeout));
}
MOBILEGL_EGL_API EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags) {
return MobileGL::MG_Impl::EGLImpl::WaitSync(dpy, reinterpret_cast<EGLSync>(sync), flags) ? EGL_TRUE : EGL_FALSE;
}
MOBILEGL_EGL_API EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value) {
EGLAttrib answered = EGL_NONE;
if (!MobileGL::MG_Impl::EGLImpl::GetSyncAttrib(dpy, reinterpret_cast<EGLSync>(sync), attribute, &answered)) {
return EGL_FALSE;
}
if (value != nullptr) {
*value = static_cast<EGLint>(answered);
}
return EGL_TRUE;
}
// THE DEVICE-QUERY FAMILY, PUBLISHED AS REAL ENTRY POINTS. It is not academic: with the extension
// announced but these names unresolvable, libepoxy leaves epoxy_eglQueryDisplayAttribEXT NULL and
// kwin, trusting the announcement, jumps to it (measured: signal 11, fault_addr=(nil), pc=0x0 from
// KWin::EglDisplay::determineRenderNode()); with the announcement withdrawn instead, kwin stops
// asking and its compositing never becomes active (measured: org.kde.kwin.Compositing active =
// false), so nothing is ever painted and the display daemon frames are never imported. Publishing
// them is the only answer that leaves kwin both safe and able to find its render device.
namespace {
// One device, one identity - the same shape the vendor side uses (Exporting/GlvndVendor.cpp): a
// pointer to a private object, so a device from somewhere else cannot compare equal by accident.
struct DeviceToken { int unused; };
DeviceToken g_deviceToken;
} // namespace
MOBILEGL_EGL_API EGLBoolean eglQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) {
if (num_devices == nullptr) return EGL_FALSE;
*num_devices = 1;
if (devices != nullptr && max_devices >= 1) devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
return EGL_TRUE;
}
MOBILEGL_EGL_API EGLBoolean eglQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) {
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken) || value == nullptr) return EGL_FALSE;
switch (attribute) {
case EGL_DRM_DEVICE_FILE_EXT:
case EGL_DRM_RENDER_NODE_FILE_EXT:
*value = reinterpret_cast<EGLAttrib>("/dev/dri/renderD128");
return EGL_TRUE;
default:
return EGL_FALSE;
}
}
MOBILEGL_EGL_API const char* eglQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) {
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken)) return nullptr;
// THE NODE IS NAMED, AND THAT CHANGED BECAUSE THE MEASUREMENT CHANGED. An empty answer was the
// right shape while this query was unreachable: nothing asked, so nothing depended on it. Now
// that the name resolves, it is exactly what kwin asks on the way into its EGL backend, and the
// fork says out loud what an empty answer costs it - "Error during init of AnlandEglBackend",
// after which it falls back to the plain AnlandBackend, whose compositing never becomes active
// (supportInformation: "LogicalOutput backend: KWin::AnlandBackend" + "Compositing is not
// active"), so nothing is ever painted and the display daemon frames are never imported. The
// path answered is the one the environment configures the compositor with (ANLAND_DRM_DEVICE),
// so the node named here and the node in use are the same node.
if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) {
return "/dev/dri/renderD128";
}
if (name == EGL_EXTENSIONS) return "EGL_EXT_device_drm_render_node";
return nullptr;
}
MOBILEGL_EGL_API EGLBoolean eglQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) {
(void)dpy;
if (value == nullptr) return EGL_FALSE;
if (attribute == EGL_DEVICE_EXT) {
*value = reinterpret_cast<EGLAttrib>(&g_deviceToken);
return EGL_TRUE;
}
return EGL_FALSE;
}
// The EXT spellings of the platform surface entry points: EGL_EXT_platform_base defines them by those
// names, and a dispatcher-based client resolves them by those names. Identical ABI (EGLAttrib list).
MOBILEGL_EGL_API EGLSurface eglCreatePlatformWindowSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_window,
const EGLAttrib* attrib_list) {
return eglCreatePlatformWindowSurface(dpy, config, native_window, attrib_list);
}
MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurfaceEXT(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
const EGLAttrib* attrib_list) {
return eglCreatePlatformPixmapSurface(dpy, config, native_pixmap, attrib_list);
}
MOBILEGL_EGL_API EGLSurface eglCreatePlatformPixmapSurface(EGLDisplay dpy, EGLConfig config, void* native_pixmap,
const EGLAttrib* attrib_list) {
MGLOG_D("eglCreatePlatformPixmapSurface(dpy=%p, config=%p, native_pixmap=%p, attrib_list=%p)", dpy, config,
@@ -0,0 +1,395 @@
// MobileGL - MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// MOBILEGL AS A GLVND EGL VENDOR.
//
// On a system with libglvnd, the libEGL.so.1 an application links is not an implementation at all:
// it is a dispatcher, and the implementations are vendor libraries it loads from
// /usr/share/glvnd/egl_vendor.d (or from whatever __EGL_VENDOR_LIBRARY_FILENAMES names) and routes
// every EGL call to. That is the supported way to be the implementation an application actually
// talks to, and on this machine it is 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.
//
// What glvnd asks a vendor for is the other half of that ABI, and this file is it: glvnd calls
// __egl_Main once, the library answers with the functions it wants dispatched to it, and - the part
// that decides whether a compositor can use it at all - with the string naming the PLATFORMS it
// serves. A vendor that does not claim EGL_PLATFORM_GBM_KHR is never asked for a GBM display, and
// the caller that asked for one is told there is no usable device: the client sitting in its maps,
// loaded and never called, is exactly what that looks like from the outside.
//
// The entry points below are this library's own (MG_Impl::EGLImpl, the same functions
// Exporting/Definitions.cpp exports), so a vendor call and a direct call cannot drift apart.
#include <Includes.h>
#include <glvnd/libeglabi.h>
// EGLImageKHR, for the KHR spellings the entry-point table below answers: Includes.h brings in the
// core egl.h only.
#include <EGL/eglext.h>
#include "../EGLImpl.h"
#include "../EGLPlatformExtensions.h"
// The KHR spellings of the image entry points, declared here because the bundled EGL headers
// declare them only under EGL_EGLEXT_PROTOTYPES and because glvnd asks for entry points BY NAME -
// the table below is where it finds this one.
extern "C" EGLImageKHR eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImageKHR image);
// THE SWAP-AND-SYNC EXPORTS, declared here for the same reason the image pair is: they are defined in
// Exporting/Definitions.cpp and this file only names them in the vendor table below.
extern "C" EGLBoolean eglSwapBuffersWithDamageKHR(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
EGLint n_rects);
extern "C" EGLBoolean eglSwapBuffersWithDamageEXT(EGLDisplay dpy, EGLSurface surface, const EGLint* rects,
EGLint n_rects);
extern "C" EGLSyncKHR eglCreateSyncKHR(EGLDisplay dpy, EGLenum type, const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroySyncKHR(EGLDisplay dpy, EGLSyncKHR sync);
extern "C" EGLint eglClientWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags, EGLTimeKHR timeout);
extern "C" EGLBoolean eglWaitSyncKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint flags);
extern "C" EGLBoolean eglGetSyncAttribKHR(EGLDisplay dpy, EGLSyncKHR sync, EGLint attribute, EGLint* value);
namespace {
using namespace MobileGL;
const __EGLapiExports* g_eglExports = nullptr;
// The platforms this vendor serves. GBM is the one that matters here: the compositor asks for
// a display on the GBM device it opened, and a vendor that does not name the platform is not
// asked at all. This side treats a GBM device as it treats every other native display - the
// frames come from the display host, not from the device - so claiming the platform is honest
// rather than optimistic.
constexpr const char* kPlatformExtensions = MG_Impl::EGLImpl::kPlatformExtensionString;
// glvnd asks for the client-only extension list separately from the platform one; the split is
// its own invention, and this side answers both from the same query.
const char* EGLAPIENTRY VendorQueryString(EGLDisplay dpy, EGLint name) {
return MG_Impl::EGLImpl::QueryString(dpy, name);
}
EGLDisplay EGLAPIENTRY VendorGetPlatformDisplay(EGLenum platform, void* native_display,
const EGLAttrib* attrib_list) {
// EGL_NONE means the application called eglGetDisplay(EGL_DEFAULT_DISPLAY): the vendor is
// being asked to name its own default display.
if (platform == EGL_NONE) {
return MG_Impl::EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
}
return MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list);
}
EGLBoolean EGLAPIENTRY VendorGetSupportsAPI(EGLenum api) {
// What this library answers for EGL_CLIENT_APIS, said as a question.
return (api == EGL_OPENGL_ES_API || api == EGL_OPENGL_API) ? EGL_TRUE : EGL_FALSE;
}
const char* EGLAPIENTRY VendorGetVendorString(int name) {
if (name == __EGL_VENDOR_STRING_PLATFORM_EXTENSIONS) {
return kPlatformExtensions;
}
return nullptr;
}
// GLVND ASKS FOR EVERY EGL ENTRY POINT BY NAME, AND DROPS THE VENDOR IF ONE IS MISSING: the
// first eight functions the table below names are looked up before this library is ever asked
// for a display, and a single null answer means glvnd quietly stops considering it - the
// library is loaded, __egl_Main has run, and nothing is ever dispatched to it.
struct EntryPoint {
const char* name;
void* address;
};
// ---- the device family -------------------------------------------------------------------
//
// A compositor does not ask for a display and hope. It asks which devices exist, reads the DRM
// node off the one it wants, and only then asks for a display ON that device - so a vendor that
// answers "no devices" is one it cannot use, however well the vendor draws. The message it
// gives is "no usable DRM render device", which names where it stopped rather than why.
//
// This implementation therefore names one device: the render node of the machine it runs on.
// That is an honest answer even though the frames do not come from that node - they come from
// the display host on the other end of the socket, which is why every surface here is a host
// frame - because the question is which device this display is associated with, and the answer
// is what lets the compositor match its own node to the display it is about to draw into.
const char* DrmRenderNode() {
// MOBILEGL_DEVICE_DRM_NODE names another node: a machine with more than one GPU, or a test
// that wants the mismatch refused rather than papered over.
const char* configured = std::getenv("MOBILEGL_DEVICE_DRM_NODE");
return (configured != nullptr && configured[0] != '\0') ? configured : "/dev/dri/renderD128";
}
// One device, one identity. A pointer to a private object rather than a number, so that a
// device from somewhere else cannot compare equal to this one by accident.
struct DeviceToken { int unused; };
DeviceToken g_deviceToken;
EGLBoolean EGLAPIENTRY VendorQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) {
if (num_devices == nullptr) {
return EGL_FALSE;
}
*num_devices = 1;
if (devices != nullptr && max_devices >= 1) {
devices[0] = reinterpret_cast<EGLDeviceEXT>(&g_deviceToken);
}
return EGL_TRUE;
}
EGLBoolean EGLAPIENTRY VendorQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) {
MGLOG_I("vendor eglQueryDeviceAttribEXT(device=%p, attribute=0x%04x)", static_cast<void*>(device),
static_cast<unsigned>(attribute));
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken) || value == nullptr) {
return EGL_FALSE;
}
switch (attribute) {
case EGL_DRM_DEVICE_FILE_EXT:
case EGL_DRM_RENDER_NODE_FILE_EXT:
// THE ATTRIBUTE FORM OF THE SAME QUERY, and the last place the empty answer lived. kwin
// asks for the node as an ATTRIBUTE as well as a string; with this one empty its log said
// "couldn't find dev node for drm device" even after the string form was answered, and
// the compositing mode it had just successfully started through this library was then
// declared unfulfillable. The path is the environment's own (ANLAND_DRM_DEVICE).
*value = reinterpret_cast<EGLAttrib>("/dev/dri/renderD128");
return EGL_TRUE;
default:
return EGL_FALSE;
}
}
const char* EGLAPIENTRY VendorQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) {
// WHICH DEVICE QUERY THE COMPOSITOR MAKES, and by which name: the string form and the attribute
// form are different entry points with different answers (glvnd answers the string form itself
// and only forwards the attribute one), so the caller decides which of the two it gets.
MGLOG_I("vendor eglQueryDeviceStringEXT(device=%p, name=0x%04x)", static_cast<void*>(device),
static_cast<unsigned>(name));
if (device != reinterpret_cast<EGLDeviceEXT>(&g_deviceToken)) {
return nullptr;
}
// THE NODE IS NAMED HERE TOO, AND THAT IS WHAT THE VENDOR-SIDE ANSWER WAS MISSING. An empty
// answer was measured to be harmless only while this query was unreachable; through glvnd it
// IS the query a compositor makes before it opens anything, and an empty string is what its
// next line reports - "Failed to open drm node : No such file or directory" (the name is the
// blank between "node" and ":"), followed by "couldn't find dev node for drm device" and a
// dead OpenGL compositing mode. The path is the one the environment configures the
// compositor with (ANLAND_DRM_DEVICE), so the node named here and the node in use are one.
if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) {
return "/dev/dri/renderD128";
}
if (name == EGL_EXTENSIONS) {
return "EGL_EXT_device_drm_render_node";
}
return nullptr;
}
EGLBoolean EGLAPIENTRY VendorQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) {
(void)dpy;
if (value == nullptr) {
return EGL_FALSE;
}
if (attribute == EGL_DEVICE_EXT) {
*value = reinterpret_cast<EGLAttrib>(&g_deviceToken);
return EGL_TRUE;
}
return EGL_FALSE;
}
// Three of the names glvnd insists on are debug extensions this implementation does not
// provide, and glvnd insists on them all the same - a vendor that answers null for any of them
// is dropped. They are answered honestly rather than pretended: KHR_debug is not in the
// extension strings above, so a caller that reads them will not call these at all.
EGLint EGLAPIENTRY VendorDebugMessageControlKHR(EGLDEBUGPROCKHR callback, const EGLAttrib* attrib_list) {
(void)callback;
(void)attrib_list;
return EGL_FALSE;
}
EGLBoolean EGLAPIENTRY VendorQueryDebugKHR(EGLint attribute, EGLAttrib* value) {
(void)attribute;
(void)value;
return EGL_FALSE;
}
EGLint EGLAPIENTRY VendorLabelObjectKHR(EGLDisplay dpy, EGLenum objectType, EGLObjectKHR object, EGLLabelKHR label) {
(void)dpy;
(void)objectType;
(void)object;
(void)label;
return EGL_BAD_PARAMETER;
}
const EntryPoint kEglEntryPoints[] = {
// The core of EGL 1.5, every one of which glvnd looks up by name before it considers this
// library usable.
{"eglInitialize", reinterpret_cast<void*>(eglInitialize)},
{"eglChooseConfig", reinterpret_cast<void*>(eglChooseConfig)},
{"eglCopyBuffers", reinterpret_cast<void*>(eglCopyBuffers)},
{"eglCreateContext", reinterpret_cast<void*>(eglCreateContext)},
{"eglCreatePbufferSurface", reinterpret_cast<void*>(eglCreatePbufferSurface)},
{"eglCreatePixmapSurface", reinterpret_cast<void*>(eglCreatePixmapSurface)},
{"eglCreateWindowSurface", reinterpret_cast<void*>(eglCreateWindowSurface)},
{"eglDestroyContext", reinterpret_cast<void*>(eglDestroyContext)},
{"eglDestroySurface", reinterpret_cast<void*>(eglDestroySurface)},
{"eglGetConfigAttrib", reinterpret_cast<void*>(eglGetConfigAttrib)},
{"eglGetConfigs", reinterpret_cast<void*>(eglGetConfigs)},
{"eglMakeCurrent", reinterpret_cast<void*>(eglMakeCurrent)},
{"eglQueryContext", reinterpret_cast<void*>(eglQueryContext)},
{"eglQuerySurface", reinterpret_cast<void*>(eglQuerySurface)},
{"eglSwapBuffers", reinterpret_cast<void*>(eglSwapBuffers)},
{"eglTerminate", reinterpret_cast<void*>(eglTerminate)},
{"eglWaitGL", reinterpret_cast<void*>(eglWaitGL)},
{"eglWaitNative", reinterpret_cast<void*>(eglWaitNative)},
{"eglBindTexImage", reinterpret_cast<void*>(eglBindTexImage)},
{"eglReleaseTexImage", reinterpret_cast<void*>(eglReleaseTexImage)},
{"eglSurfaceAttrib", reinterpret_cast<void*>(eglSurfaceAttrib)},
{"eglSwapInterval", reinterpret_cast<void*>(eglSwapInterval)},
{"eglBindAPI", reinterpret_cast<void*>(eglBindAPI)},
{"eglCreatePbufferFromClientBuffer", reinterpret_cast<void*>(eglCreatePbufferFromClientBuffer)},
{"eglReleaseThread", reinterpret_cast<void*>(eglReleaseThread)},
{"eglWaitClient", reinterpret_cast<void*>(eglWaitClient)},
{"eglGetError", reinterpret_cast<void*>(eglGetError)},
{"eglCreateSync", reinterpret_cast<void*>(eglCreateSync)},
{"eglDestroySync", reinterpret_cast<void*>(eglDestroySync)},
{"eglClientWaitSync", reinterpret_cast<void*>(eglClientWaitSync)},
{"eglGetSyncAttrib", reinterpret_cast<void*>(eglGetSyncAttrib)},
{"eglWaitSync", reinterpret_cast<void*>(eglWaitSync)},
{"eglCreateImage", reinterpret_cast<void*>(eglCreateImage)},
{"eglDestroyImage", reinterpret_cast<void*>(eglDestroyImage)},
// THE KHR SPELLINGS OF THOSE TWO, and they are entries rather than aliases because glvnd
// resolves a name it does not know through this table alone. Measured in the Fedora
// container that runs this library as the EGL vendor: with only the core pair here,
// eglGetProcAddress("eglCreateImageKHR") and eglGetProcAddress("eglDestroyImageKHR") both
// answered NULL, and kwin_wayland's anland backend reaches the display daemon's dma-bufs
// through them (AnlandEglLayer::importBuffers -> EglBackend::importDmaBufAsTexture ->
// EglDisplay::importBufferAsImage) - so the compositor had no entry point to import a frame
// with. eglCreateImageKHR is not the same ABI as eglCreateImage: EGL_KHR_image_base's
// attribute list is EGLint pairs, which is why Exporting/Definitions.cpp exports it as its
// own function rather than letting the core spelling stand in for it.
{"eglCreateImageKHR", reinterpret_cast<void*>(eglCreateImageKHR)},
{"eglDestroyImageKHR", reinterpret_cast<void*>(eglDestroyImageKHR)},
{"eglCreatePlatformWindowSurface", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
{"eglCreatePlatformPixmapSurface", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
// EGL_EXT_platform_base's OWN SPELLINGS, and the reason this file exists at all: the vendor
// string below advertises that extension, whose entry points ARE the EXT-named ones, and a
// dispatcher-based client resolves them by exactly those names (libepoxy in kwin does). With
// only the core spellings in this table the lookup answered NULL and the caller - which had
// already checked the extension string and so had every reason to trust it - jumped to it:
// measured as signal 11 with fault_addr=(nil) and pc=0x0 immediately after "MobileGL
// initialized", in both the systemd and the direct run, and it is what kept the compositor
// from ever reaching its output. The ABI is identical (an EGLAttrib list, not EGLint pairs),
// so the core implementation is what serves them.
{"eglCreatePlatformWindowSurfaceEXT", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
{"eglCreatePlatformPixmapSurfaceEXT", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
// The platform and current-state queries: not in glvnd's required list, but the names an
// application that talks to a vendor through a dispatcher will ask for by name.
{"eglGetDisplay", reinterpret_cast<void*>(eglGetDisplay)},
{"eglGetCurrentDisplay", reinterpret_cast<void*>(eglGetCurrentDisplay)},
{"eglGetCurrentSurface", reinterpret_cast<void*>(eglGetCurrentSurface)},
{"eglGetCurrentContext", reinterpret_cast<void*>(eglGetCurrentContext)},
{"eglQueryAPI", reinterpret_cast<void*>(eglQueryAPI)},
{"eglGetProcAddress", reinterpret_cast<void*>(eglGetProcAddress)},
// The two queries whose answer depends on which side of glvnd is asking.
{"eglQueryString", reinterpret_cast<void*>(VendorQueryString)},
{"eglGetPlatformDisplay", reinterpret_cast<void*>(VendorGetPlatformDisplay)},
{"eglGetPlatformDisplayEXT", reinterpret_cast<void*>(VendorGetPlatformDisplay)},
// What glvnd requires and this implementation does not otherwise have.
{"eglQueryDevicesEXT", reinterpret_cast<void*>(VendorQueryDevicesEXT)},
{"eglQueryDeviceAttribEXT", reinterpret_cast<void*>(VendorQueryDeviceAttribEXT)},
{"eglQueryDeviceStringEXT", reinterpret_cast<void*>(VendorQueryDeviceStringEXT)},
{"eglQueryDisplayAttribEXT", reinterpret_cast<void*>(VendorQueryDisplayAttribEXT)},
// The swap-and-sync family, published through the same table the names are answered from.
// Declared above this table because they are exports of Definitions.cpp, like the image pair.
{"eglSwapBuffersWithDamageKHR", reinterpret_cast<void*>(eglSwapBuffersWithDamageKHR)},
{"eglSwapBuffersWithDamageEXT", reinterpret_cast<void*>(eglSwapBuffersWithDamageEXT)},
{"eglCreateSyncKHR", reinterpret_cast<void*>(eglCreateSyncKHR)},
{"eglDestroySyncKHR", reinterpret_cast<void*>(eglDestroySyncKHR)},
{"eglClientWaitSyncKHR", reinterpret_cast<void*>(eglClientWaitSyncKHR)},
{"eglWaitSyncKHR", reinterpret_cast<void*>(eglWaitSyncKHR)},
{"eglGetSyncAttribKHR", reinterpret_cast<void*>(eglGetSyncAttribKHR)},
{"eglDebugMessageControlKHR", reinterpret_cast<void*>(VendorDebugMessageControlKHR)},
{"eglQueryDebugKHR", reinterpret_cast<void*>(VendorQueryDebugKHR)},
{"eglLabelObjectKHR", reinterpret_cast<void*>(VendorLabelObjectKHR)},
};
// THE SAME ANSWER TO BOTH OF GLVND'S QUESTIONS, and that is not a shortcut - glvnd asks them
// from two places and a library that answers one of them has a hole the size of the other:
//
// getProcAddress is asked for every entry point glvnd builds its own static EGL table
// from, so a null here drops the vendor outright.
// getDispatchAddress is what glvnd falls back to for eglGetProcAddress on any name it does
// not know itself - which is every extension entry point that is not in
// its list, the device queries among them. A vendor that answers these
// from a GL-only table hands the application a null for a function the
// vendor implements, and the caller reports the extension as missing.
void* LookupEntryPoint(const char* procName) {
if (procName == nullptr) {
return nullptr;
}
for (const EntryPoint& entry : kEglEntryPoints) {
if (std::strcmp(entry.name, procName) == 0) {
return entry.address;
}
}
// Anything that is not EGL is a GL or GLES entry point, and the tree's own resolver is the
// table for those.
return reinterpret_cast<void*>(MG_Impl::EGLImpl::GetProcAddress(procName));
}
void* EGLAPIENTRY VendorGetProcAddress(const char* procName) { return LookupEntryPoint(procName); }
// glvnd hands a vendor the index each EGL entry point occupies in its dispatch table, so that a
// vendor whose entry points are thin wrappers around glvnd can call back into the right slot.
// This library's entry points ARE the implementation - they never re-enter glvnd - so there is
// nothing to record; the callback exists because glvnd refuses a vendor that does not provide
// one, and refusing it means the library is loaded and never dispatched to.
void EGLAPIENTRY VendorSetDispatchIndex(const char* procName, int index) {
(void)procName;
(void)index;
}
void* EGLAPIENTRY VendorGetDispatchAddress(const char* procName) {
// glvnd asks THIS vendor for GL entry points as well: after eglMakeCurrent, the dispatch
// table of the now-current context is filled from here, one name at a time. LookupEntryPoint
// knows EGL names only, so every GL name (glGetString, glCreateProgram, ...) answered NULL,
// glvnd wrote NULL into its dispatch slots, and the first glGetString of the compositor's
// context came back NULL - kwin's "Could not initialize rendering context", while the very
// same context answered fine through a direct eglGetProcAddress (measured: probe6 vs probe12).
// GL names go through the full GL/EGL lookup, which is the same table eglGetProcAddress uses.
if (void* address = LookupEntryPoint(procName)) {
return address;
}
return reinterpret_cast<void*>(MG_Impl::EGLImpl::GetProcAddress(procName));
}
} // namespace
extern "C" MOBILEGL_EGL_API EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports,
__EGLvendorInfo* vendor, __EGLapiImports* imports) {
(void)vendor;
if (EGL_VENDOR_ABI_GET_MAJOR_VERSION(version) != EGL_VENDOR_ABI_MAJOR_VERSION) {
// A vendor that does not know the ABI it is being handed must say so, not guess: the
// structures on either side are the ABI.
MGLOG_E("__egl_Main: unsupported glvnd vendor ABI version 0x%08x (this library implements %u.%u)", version,
static_cast<unsigned>(EGL_VENDOR_ABI_MAJOR_VERSION),
static_cast<unsigned>(EGL_VENDOR_ABI_MINOR_VERSION));
return EGL_FALSE;
}
if (exports == nullptr || imports == nullptr) {
MGLOG_E("__egl_Main: glvnd handed no exports/imports table");
return EGL_FALSE;
}
g_eglExports = exports;
imports->getPlatformDisplay = VendorGetPlatformDisplay;
imports->getSupportsAPI = VendorGetSupportsAPI;
imports->getVendorString = VendorGetVendorString;
imports->getProcAddress = VendorGetProcAddress;
imports->getDispatchAddress = VendorGetDispatchAddress;
imports->setDispatchIndex = VendorSetDispatchIndex;
MGLOG_I("glvnd vendor: MobileGL is serving EGL through libglvnd (ABI %u.%u, platforms: %s)",
static_cast<unsigned>(EGL_VENDOR_ABI_MAJOR_VERSION), static_cast<unsigned>(EGL_VENDOR_ABI_MINOR_VERSION),
kPlatformExtensions);
return EGL_TRUE;
}
+398
View File
@@ -0,0 +1,398 @@
// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "WaylandWindow.h"
#if MOBILEGL_WAYLAND_WINDOWS
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
#include <MG_Util/Debug/Log.h>
#include <cerrno>
#include <cstdarg>
#include <cstdint>
#include <cstring>
#include <dlfcn.h>
#include <fcntl.h>
#include <mutex>
#include <sys/mman.h>
#include <unistd.h>
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
namespace {
// libwayland-client's types are opaque here: everything goes through wl_proxy_* and the
// exported interface descriptions, and nothing reads a field of either.
struct wl_proxy;
struct wl_interface;
struct wl_event_queue;
// wayland-egl-backend.h, WL_EGL_WINDOW_VERSION 3: the struct libwayland-egl allocates for
// wl_egl_window_create and the driver reads. The ABI is versioned by its first word.
struct EglWindow {
const intptr_t version;
int width;
int height;
int dx;
int dy;
int attached_width;
int attached_height;
void* driver_private;
void (*resize_callback)(EglWindow*, void*);
void (*destroy_window_callback)(void*);
wl_proxy* surface;
};
constexpr intptr_t kEglWindowVersion = 3;
// Request opcodes (wayland.xml, the order the requests are declared in).
constexpr uint32_t kDisplayGetRegistry = 1;
constexpr uint32_t kRegistryBind = 0;
constexpr uint32_t kShmCreatePool = 0;
constexpr uint32_t kShmPoolCreateBuffer = 0;
constexpr uint32_t kShmPoolDestroy = 1;
constexpr uint32_t kBufferDestroy = 0;
constexpr uint32_t kSurfaceAttach = 1;
constexpr uint32_t kSurfaceDamage = 2;
constexpr uint32_t kSurfaceCommit = 6;
constexpr uint32_t kSurfaceDamageBuffer = 9; // since wl_surface version 4
constexpr uint32_t kMarshalFlagDestroy = 1u << 0;
// wl_shm formats. The two every compositor must support; byte order in memory is
// B, G, R, A for both (little-endian 0xAARRGGBB).
constexpr uint32_t kShmFormatArgb8888 = 0;
constexpr uint32_t kShmFormatXrgb8888 = 1;
struct Api {
Bool loaded = false;
const wl_interface* displayInterface = nullptr;
const wl_interface* registryInterface = nullptr;
const wl_interface* shmInterface = nullptr;
const wl_interface* shmPoolInterface = nullptr;
const wl_interface* bufferInterface = nullptr;
wl_proxy* (*marshalFlags)(wl_proxy*, uint32_t, const wl_interface*, uint32_t, uint32_t, ...) = nullptr;
int (*addListener)(wl_proxy*, void (**)(void), void*) = nullptr;
void (*destroy)(wl_proxy*) = nullptr;
uint32_t (*getVersion)(wl_proxy*) = nullptr;
void* (*createWrapper)(void*) = nullptr;
void (*wrapperDestroy)(void*) = nullptr;
void (*setQueue)(wl_proxy*, wl_event_queue*) = nullptr;
wl_event_queue* (*createQueue)(void*) = nullptr;
void (*queueDestroy)(wl_event_queue*) = nullptr;
int (*roundtripQueue)(void*, wl_event_queue*) = nullptr;
int (*dispatchQueue)(void*, wl_event_queue*) = nullptr;
int (*dispatchQueuePending)(void*, wl_event_queue*) = nullptr;
int (*flush)(void*) = nullptr;
};
// Loaded once. RTLD_NOLOAD first: a Wayland client has the library already, and the copy
// it is using is the one whose wl_display this must talk to.
const Api& WaylandApi() {
static Api api;
static std::once_flag once;
std::call_once(once, [] {
void* lib = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_NOLOAD);
if (lib == nullptr) lib = dlopen("libwayland-client.so.0", RTLD_NOW);
if (lib == nullptr) return;
const auto sym = [lib](const char* name) { return dlsym(lib, name); };
api.displayInterface = static_cast<const wl_interface*>(sym("wl_display_interface"));
api.registryInterface = static_cast<const wl_interface*>(sym("wl_registry_interface"));
api.shmInterface = static_cast<const wl_interface*>(sym("wl_shm_interface"));
api.shmPoolInterface = static_cast<const wl_interface*>(sym("wl_shm_pool_interface"));
api.bufferInterface = static_cast<const wl_interface*>(sym("wl_buffer_interface"));
api.marshalFlags = reinterpret_cast<decltype(api.marshalFlags)>(sym("wl_proxy_marshal_flags"));
api.addListener = reinterpret_cast<decltype(api.addListener)>(sym("wl_proxy_add_listener"));
api.destroy = reinterpret_cast<decltype(api.destroy)>(sym("wl_proxy_destroy"));
api.getVersion = reinterpret_cast<decltype(api.getVersion)>(sym("wl_proxy_get_version"));
api.createWrapper = reinterpret_cast<decltype(api.createWrapper)>(sym("wl_proxy_create_wrapper"));
api.wrapperDestroy = reinterpret_cast<decltype(api.wrapperDestroy)>(sym("wl_proxy_wrapper_destroy"));
api.setQueue = reinterpret_cast<decltype(api.setQueue)>(sym("wl_proxy_set_queue"));
api.createQueue = reinterpret_cast<decltype(api.createQueue)>(sym("wl_display_create_queue"));
api.queueDestroy = reinterpret_cast<decltype(api.queueDestroy)>(sym("wl_event_queue_destroy"));
api.roundtripQueue = reinterpret_cast<decltype(api.roundtripQueue)>(sym("wl_display_roundtrip_queue"));
api.dispatchQueue = reinterpret_cast<decltype(api.dispatchQueue)>(sym("wl_display_dispatch_queue"));
api.dispatchQueuePending =
reinterpret_cast<decltype(api.dispatchQueuePending)>(sym("wl_display_dispatch_queue_pending"));
api.flush = reinterpret_cast<decltype(api.flush)>(sym("wl_display_flush"));
api.loaded = api.displayInterface && api.registryInterface && api.shmInterface &&
api.shmPoolInterface && api.bufferInterface && api.marshalFlags && api.addListener && api.destroy &&
api.getVersion && api.createWrapper && api.wrapperDestroy && api.setQueue &&
api.createQueue && api.queueDestroy && api.roundtripQueue && api.dispatchQueue &&
api.dispatchQueuePending && api.flush;
if (!api.loaded) MGLOG_E("Wayland: libwayland-client.so.0 lacks an entry point this library uses");
});
return api;
}
struct ShmBuffer {
wl_proxy* buffer = nullptr;
void* map = nullptr;
SizeT size = 0;
EGLint width = 0;
EGLint height = 0;
EGLint stride = 0;
Bool busy = false;
};
// The compositor is done with a buffer: it may be drawn into again.
void OnBufferRelease(void* data, wl_proxy*) { static_cast<ShmBuffer*>(data)->busy = false; }
void (*const kBufferListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnBufferRelease)};
} // namespace
struct WindowSurface::Impl {
void* display = nullptr;
EglWindow* window = nullptr;
wl_event_queue* queue = nullptr;
void* displayWrapper = nullptr;
wl_proxy* registry = nullptr;
wl_proxy* shm = nullptr;
uint32_t format = kShmFormatXrgb8888;
EGLint width = 0;
EGLint height = 0;
// Three: one on the glass, one the compositor may still be reading, one to draw into.
ShmBuffer buffers[3];
Vector<Uint8> scratch;
~Impl() {
const Api& api = WaylandApi();
for (auto& buffer : buffers) Release(buffer);
// Neither wl_shm (version 1) nor wl_registry has a destroy request: the proxies go
// on this side only.
if (shm) api.destroy(shm);
if (registry) api.destroy(registry);
if (displayWrapper) api.wrapperDestroy(displayWrapper);
if (queue) api.queueDestroy(queue);
if (window && window->driver_private == this) {
window->driver_private = nullptr;
window->destroy_window_callback = nullptr;
}
}
void Release(ShmBuffer& buffer) {
const Api& api = WaylandApi();
if (buffer.buffer) api.marshalFlags(buffer.buffer, kBufferDestroy, nullptr, api.getVersion(buffer.buffer),
kMarshalFlagDestroy);
if (buffer.map) munmap(buffer.map, buffer.size);
buffer = ShmBuffer{};
}
Bool Allocate(ShmBuffer& buffer) {
const Api& api = WaylandApi();
Release(buffer);
const EGLint stride = width * 4;
const SizeT size = static_cast<SizeT>(stride) * static_cast<SizeT>(height);
const int fd = memfd_create("mobilegl-wayland", MFD_CLOEXEC);
if (fd < 0) {
MGLOG_E("Wayland: memfd_create for a %dx%d buffer failed: %s", width, height, std::strerror(errno));
return false;
}
if (ftruncate(fd, static_cast<off_t>(size)) != 0) {
MGLOG_E("Wayland: sizing a %dx%d buffer failed: %s", width, height, std::strerror(errno));
close(fd);
return false;
}
void* map = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
MGLOG_E("Wayland: mapping a %dx%d buffer failed: %s", width, height, std::strerror(errno));
close(fd);
return false;
}
// One pool per buffer, destroyed at once: the buffer keeps the memory alive.
wl_proxy* pool = api.marshalFlags(shm, kShmCreatePool, api.shmPoolInterface, api.getVersion(shm), 0, nullptr,
fd, static_cast<int32_t>(size));
close(fd);
if (pool == nullptr) {
munmap(map, size);
return false;
}
wl_proxy* wlBuffer = api.marshalFlags(pool, kShmPoolCreateBuffer, api.bufferInterface, api.getVersion(pool),
0, nullptr, 0, width, height, stride, format);
api.marshalFlags(pool, kShmPoolDestroy, nullptr, api.getVersion(pool), kMarshalFlagDestroy);
if (wlBuffer == nullptr) {
munmap(map, size);
return false;
}
buffer.buffer = wlBuffer;
buffer.map = map;
buffer.size = size;
buffer.width = width;
buffer.height = height;
buffer.stride = stride;
buffer.busy = false;
api.addListener(wlBuffer, const_cast<void (**)(void)>(kBufferListener), &buffer);
return true;
}
// A buffer the compositor is not reading: the events already received first, then (all
// three still held) a blocking wait for the next release.
ShmBuffer* NextFree() {
const Api& api = WaylandApi();
for (;;) {
if (api.dispatchQueuePending(display, queue) < 0) return nullptr;
for (auto& buffer : buffers) {
if (buffer.buffer == nullptr || buffer.busy) continue;
if (buffer.width != width || buffer.height != height) {
if (!Allocate(buffer)) return nullptr;
}
return &buffer;
}
for (auto& buffer : buffers) {
if (buffer.buffer == nullptr) return Allocate(buffer) ? &buffer : nullptr;
}
if (api.dispatchQueue(display, queue) < 0) return nullptr;
}
}
};
namespace {
void OnRegistryGlobal(void* data, wl_proxy* registry, uint32_t name, const char* interface, uint32_t) {
auto* impl = static_cast<WindowSurface::Impl*>(data);
if (impl->shm != nullptr || std::strcmp(interface, "wl_shm") != 0) return;
const Api& api = WaylandApi();
impl->shm = api.marshalFlags(registry, kRegistryBind, api.shmInterface, 1, 0, name, "wl_shm", 1u, nullptr);
}
void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {}
void (*const kRegistryListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnRegistryGlobal),
reinterpret_cast<void (*)(void)>(&OnRegistryGlobalRemove)};
// The application destroyed its wl_egl_window first: nothing may be read from it after this.
void OnWindowDestroyed(void* data) { static_cast<WindowSurface::Impl*>(data)->window = nullptr; }
} // namespace
Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform) {
if (platform == kPlatformWayland) return true;
if (nativeDisplay == 0 || (platform != EGL_NONE && platform != 0)) return false;
const Api& api = WaylandApi();
if (!api.loaded) return false;
return *reinterpret_cast<const void* const*>(static_cast<uintptr_t>(nativeDisplay)) == api.displayInterface;
}
Bool IsWaylandWindow(const void* window) {
if (window == nullptr) return false;
const auto* eglWindow = static_cast<const EglWindow*>(window);
return eglWindow->version >= kEglWindowVersion && eglWindow->surface != nullptr;
}
Bool WindowSize(const void* window, EGLint* width, EGLint* height) {
if (!IsWaylandWindow(window)) return false;
const auto* eglWindow = static_cast<const EglWindow*>(window);
*width = eglWindow->width;
*height = eglWindow->height;
return eglWindow->width > 0 && eglWindow->height > 0;
}
WindowSurface::WindowSurface(UniquePtr<Impl> impl) : m_impl(std::move(impl)) {}
WindowSurface::~WindowSurface() = default;
UniquePtr<WindowSurface> WindowSurface::Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height,
Bool hasAlpha) {
const Api& api = WaylandApi();
if (!api.loaded) {
MGLOG_E("Wayland: a window surface needs libwayland-client.so.0, and it could not be used");
return nullptr;
}
if (wlDisplay == nullptr || !IsWaylandWindow(wlEglWindow) || width <= 0 || height <= 0) return nullptr;
auto impl = MakeUnique<Impl>();
impl->display = wlDisplay;
impl->window = static_cast<EglWindow*>(wlEglWindow);
impl->width = width;
impl->height = height;
impl->format = hasAlpha ? kShmFormatArgb8888 : kShmFormatXrgb8888;
// A queue of this surface's own, so the application's dispatching never sees these events
// and this never dispatches the application's.
impl->queue = api.createQueue(wlDisplay);
impl->displayWrapper = api.createWrapper(wlDisplay);
if (impl->queue == nullptr || impl->displayWrapper == nullptr) return nullptr;
api.setQueue(static_cast<wl_proxy*>(impl->displayWrapper), impl->queue);
auto* wrapper = static_cast<wl_proxy*>(impl->displayWrapper);
impl->registry = api.marshalFlags(wrapper, kDisplayGetRegistry, api.registryInterface, api.getVersion(wrapper),
0, nullptr);
if (impl->registry == nullptr) return nullptr;
api.addListener(impl->registry, const_cast<void (**)(void)>(kRegistryListener), impl.get());
if (api.roundtripQueue(wlDisplay, impl->queue) < 0 || impl->shm == nullptr) {
MGLOG_E("Wayland: the compositor offers no wl_shm; a window surface cannot be presented");
return nullptr;
}
impl->window->driver_private = impl.get();
impl->window->destroy_window_callback = &OnWindowDestroyed;
MGLOG_I("Wayland: window surface %dx%d presented through wl_shm (%s)", width, height,
hasAlpha ? "ARGB8888" : "XRGB8888");
return UniquePtr<WindowSurface>(new WindowSurface(std::move(impl)));
}
Bool WindowSurface::Present() {
Impl& impl = *m_impl;
if (impl.window == nullptr) return false;
const Api& api = WaylandApi();
ShmBuffer* buffer = impl.NextFree();
if (buffer == nullptr) {
MGLOG_E_ONCE("Wayland: no wl_shm buffer could be taken for the frame; it is not shown");
return false;
}
// The frame, out of the default framebuffer, through this library's own entry points: the
// application's read framebuffer, pack buffer and pack state are saved and put back, so
// the readback is invisible to it.
namespace GL = MG_Impl::GLImpl;
GLint readFramebuffer = 0, packBuffer = 0, alignment = 4, rowLength = 0, skipRows = 0, skipPixels = 0;
GL::GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer);
GL::GetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer);
GL::GetIntegerv(GL_PACK_ALIGNMENT, &alignment);
GL::GetIntegerv(GL_PACK_ROW_LENGTH, &rowLength);
GL::GetIntegerv(GL_PACK_SKIP_ROWS, &skipRows);
GL::GetIntegerv(GL_PACK_SKIP_PIXELS, &skipPixels);
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, 0);
if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, 0);
GL::PixelStorei(GL_PACK_ALIGNMENT, 4);
GL::PixelStorei(GL_PACK_ROW_LENGTH, 0);
GL::PixelStorei(GL_PACK_SKIP_ROWS, 0);
GL::PixelStorei(GL_PACK_SKIP_PIXELS, 0);
const SizeT rowBytes = static_cast<SizeT>(impl.width) * 4;
impl.scratch.resize(rowBytes * static_cast<SizeT>(impl.height));
GL::ReadPixels(0, 0, impl.width, impl.height, GL_RGBA, GL_UNSIGNED_BYTE, impl.scratch.data());
GL::PixelStorei(GL_PACK_ALIGNMENT, alignment);
GL::PixelStorei(GL_PACK_ROW_LENGTH, rowLength);
GL::PixelStorei(GL_PACK_SKIP_ROWS, skipRows);
GL::PixelStorei(GL_PACK_SKIP_PIXELS, skipPixels);
if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, static_cast<GLuint>(packBuffer));
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readFramebuffer));
// GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A.
auto* dst = static_cast<Uint8*>(buffer->map);
for (EGLint y = 0; y < impl.height; ++y) {
const Uint8* src = impl.scratch.data() + static_cast<SizeT>(impl.height - 1 - y) * rowBytes;
Uint8* row = dst + static_cast<SizeT>(y) * static_cast<SizeT>(buffer->stride);
for (EGLint x = 0; x < impl.width; ++x) {
row[x * 4 + 0] = src[x * 4 + 2];
row[x * 4 + 1] = src[x * 4 + 1];
row[x * 4 + 2] = src[x * 4 + 0];
row[x * 4 + 3] = src[x * 4 + 3];
}
}
wl_proxy* surface = impl.window->surface;
const uint32_t surfaceVersion = api.getVersion(surface);
api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, buffer->buffer, impl.window->dx,
impl.window->dy);
if (surfaceVersion >= 4) {
api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, impl.width,
impl.height);
} else {
api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, impl.width, impl.height);
}
api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0);
buffer->busy = true;
impl.window->attached_width = impl.width;
impl.window->attached_height = impl.height;
impl.window->dx = 0;
impl.window->dy = 0;
api.flush(impl.display);
return true;
}
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
#endif
+72
View File
@@ -0,0 +1,72 @@
// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
// WAYLAND WINDOW SURFACES. A Wayland client hands eglCreateWindowSurface a wl_egl_window, and what
// the compositor shows is whatever wl_buffer the client attaches to that window's wl_surface. The
// pixels of a MobileGL frame are drawn where the backend is - on a split build, in another process
// on another OS - so a window surface here is a pbuffer the backend draws into, plus this: at every
// swap the frame is read back and attached to the wl_surface as a wl_shm buffer.
//
// libwayland-client is loaded at run time (it is already in every Wayland client's process), so the
// library neither links it nor needs its headers to build.
#include <Includes.h>
#if defined(__linux__) && !defined(__ANDROID__)
#define MOBILEGL_WAYLAND_WINDOWS 1
#else
#define MOBILEGL_WAYLAND_WINDOWS 0
#endif
#if MOBILEGL_WAYLAND_WINDOWS
#include <EGL/egl.h>
namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// EGL_PLATFORM_WAYLAND_KHR and EGL_PLATFORM_WAYLAND_EXT are the same enum.
inline constexpr EGLenum kPlatformWayland = 0x31D8;
// Whether a display is a Wayland one: asked for as EGL_PLATFORM_WAYLAND, or (eglGetDisplay with
// no platform) a native display that is a wl_display - recognised the way Mesa does, by its
// first word being libwayland-client's wl_display_interface.
Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform);
// Whether `window` looks like a wl_egl_window this library can present to.
Bool IsWaylandWindow(const void* window);
// The size the application last gave its wl_egl_window.
Bool WindowSize(const void* window, EGLint* width, EGLint* height);
// One window surface's presentation: the shm buffers, the event queue they are dispatched on,
// and the readback scratch. Created for a wl_egl_window on a wl_display; null with the reason
// logged when libwayland-client or the compositor's wl_shm is not there.
class WindowSurface {
public:
static UniquePtr<WindowSurface> Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height,
Bool hasAlpha);
~WindowSurface();
WindowSurface(const WindowSurface&) = delete;
WindowSurface& operator=(const WindowSurface&) = delete;
// Reads the frame back out of the current context's default framebuffer and puts it on
// the window: attach, damage, commit. The application's read-framebuffer and pack state
// are put back exactly as they were. False (logged) when the frame did not reach the
// window; the swap itself goes on regardless.
Bool Present();
struct Impl;
private:
explicit WindowSurface(UniquePtr<Impl> impl);
UniquePtr<Impl> m_impl;
};
} // namespace MobileGL::MG_Impl::EGLImpl::Wayland
#endif
+475 -11
View File
@@ -6,6 +6,9 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// The dma-buf import's client half: eglCreateImageKHR keeps a dup() of every plane's descriptor,
// and this is what tells the render server which texture that buffer backs (see the header).
#include <MG_Remote/Client/DmaBufImport.h>
#include <Includes.h>
#include "../Buffer/GL_Buffer.h"
#include "../Getter/GL_Getter.h"
@@ -22,6 +25,13 @@
#include "../Sync/GL_Sync.h"
#include "../Debug/GL_Debug.h"
#include <MG_State/GLState/Core.h>
// The EGL side of the state, for the two GL_OES_EGL_image entry points: what an EGLImage handle
// names is an object eglCreateImageKHR built (its geometry, its fourcc and the descriptors it owns),
// and that object lives in the EGL state. Same include the other GLImpl files that touch it use.
#include <MG_State/EGLState/Core.h>
// The compositor's frame boundary: glFlush presents the display host's frame when the surfaceless
// bind hook took one (see EGLImpl.cpp). The EGL side declares the pair and the swap entry.
#include <MG_Impl/EGLImpl/EGLImpl.h>
#define DECLARE_GL_FUNCTION_STUB_HEAD(type, name, ...) MOBILEGL_GL_API type gl##name(__VA_ARGS__) {
@@ -125,6 +135,17 @@ MOBILEGL_GL_API void glFlush() {
#if MOBILEGL_BUILD_DISAGGREGATED
MobileGL::MG_Pipe::MGPipeClientFlush();
#endif
// A COMPOSITOR'S FRAME BOUNDARY. kwin binds the display host's frame surface and draws the
// desktop into it, then calls glFlush - and glFlush alone. It never calls eglSwapBuffers, so
// without this the stream reached the server and the server drew, but the display host was
// never told a frame was done: the host's own present loop (AHardwareBuffer_acquire ->
// draw -> notify) is what puts pixels on the glass, and its notify is exactly this swap.
// Guarded by whether the surfaceless-bind hook actually took the host frame.
if (MobileGL::MG_Impl::EGLImpl::g_hostFramePresentSurface != nullptr &&
MobileGL::MG_Impl::EGLImpl::g_hostFramePresentDisplay != nullptr) {
MobileGL::MG_Impl::EGLImpl::SwapBuffers(MobileGL::MG_Impl::EGLImpl::g_hostFramePresentDisplay,
MobileGL::MG_Impl::EGLImpl::g_hostFramePresentSurface);
}
}
DECLARE_GL_FUNCTION_HEAD(void, FramebufferRenderbuffer, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferRenderbuffer, target, attachment, renderbuffertarget, renderbuffer)
DECLARE_GL_FUNCTION_HEAD(void, FramebufferTexture2D, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) DECLARE_GL_FUNCTION_END_NO_RETURN(void, FramebufferTexture2D, target, attachment, textarget, texture, level)
@@ -1297,17 +1318,325 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, GetnMinmaxARB, GLenum target, GLboolean rese
DECLARE_GL_FUNCTION_STUB_HEAD(void, FramebufferSampleLocationsfvARB, GLenum target, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, FramebufferSampleLocationsfvARB, target, start, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedFramebufferSampleLocationsfvARB, GLuint framebuffer, GLuint start, GLsizei count, const GLfloat* v) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedFramebufferSampleLocationsfvARB, framebuffer, start, count, v)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EvaluateDepthValuesARB, void) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EvaluateDepthValuesARB, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteObjectARB, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteObjectARB, obj)
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, GetHandleARB, GLenum pname) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, GetHandleARB, pname)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DetachObjectARB, GLhandleARB containerObj, GLhandleARB attachedObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DetachObjectARB, containerObj, attachedObj)
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateShaderObjectARB, GLenum shaderType) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateShaderObjectARB, shaderType)
DECLARE_GL_FUNCTION_STUB_HEAD(GLhandleARB, CreateProgramObjectARB, void) DECLARE_GL_FUNCTION_STUB_END(GLhandleARB, CreateProgramObjectARB, )
DECLARE_GL_FUNCTION_STUB_HEAD(void, AttachObjectARB, GLhandleARB containerObj, GLhandleARB obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, AttachObjectARB, containerObj, obj)
DECLARE_GL_FUNCTION_STUB_HEAD(void, UseProgramObjectARB, GLhandleARB programObj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UseProgramObjectARB, programObj)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterfvARB, GLhandleARB obj, GLenum pname, GLfloat* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterfvARB, obj, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetObjectParameterivARB, GLhandleARB obj, GLenum pname, GLint* params) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetObjectParameterivARB, obj, pname, params)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetInfoLogARB, GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetInfoLogARB, obj, maxLength, length, infoLog)
DECLARE_GL_FUNCTION_STUB_HEAD(void, GetAttachedObjectsARB, GLhandleARB containerObj, GLsizei maxCount, GLsizei* count, GLhandleARB* obj) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, GetAttachedObjectsARB, containerObj, maxCount, count, obj)
// GL_ARB_shader_objects, GL_ARB_vertex_shader, GL_ARB_fragment_shader.
//
// kwin_wayland 6.7.3 refuses a MobileGL context over three of the names these extensions add.
// EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the DESKTOP-OpenGL branch for
// every context whose GL_VERSION is not an "OpenGL ES" string:
// const bool supportsGLSL = m_isOpenglES || (hasOpenglExtension("GL_ARB_shader_objects") &&
// hasOpenglExtension("GL_ARB_fragment_shader") && hasOpenglExtension("GL_ARB_vertex_shader"));
// GL_VERSION here is "4.6.0 MobileGL ...", so all three names were REQUIRED of a context that did
// not carry them: checkSupported() answered false, EglContext::create() returned nullptr,
// EglBackend::createContext() returned false, and kwin printed "Could not initialize rendering
// context" and quit without drawing a frame. Both backends now advertise the three, and this block
// is what makes that advertisement true instead of a test unlock - every entry point the three
// extensions define is implemented here, over the shader and program objects the core entry points
// already own:
//
// ARB_shader_objects the 39 functions of the block include/GL/glext.h:4095-4173 declares.
// ARB_vertex_shader the three attrib-location entry points of glext.h:4856-4862.
// ARB_fragment_shader none. Its "New Procedures and Functions" section reads "None" - it is the
// capability half of the pair, and the reason kwin names it at all.
//
// HANDLES. The ARB family names objects with GLhandleARB. This repository's own glext.h types that
// as void* on Apple and as unsigned int on every other platform (glext.h:4055-4059, the same split
// the system headers carry), so on Linux, Windows and Android a handle IS the GLuint name this
// implementation hands out and both conversions below are the identity there. The pointer arm is
// not decoration: a caller on Apple passes a pointer, and an id written straight into its void*
// slot would be a size error rather than a cast error. H is a template parameter, and both arms
// cast through it, so the arm a platform does not use is never instantiated - and never has to be
// well-formed for the other typedef: reinterpret_cast between two integers, which is what the
// pointer arm becomes on Linux, is not a cast clang accepts. The shape is the one
// MG_State/EGLState/Core.h already uses for EGL's handles.
template <typename H = GLhandleARB>
static GLhandleARB ToGLHandleARB(GLuint name) {
if constexpr (std::is_pointer_v<H>) {
return reinterpret_cast<H>(static_cast<std::uintptr_t>(name));
} else {
return static_cast<H>(name);
}
}
template <typename H = GLhandleARB>
static GLuint FromGLHandleARB(GLhandleARB handle) {
if constexpr (std::is_pointer_v<H>) {
return static_cast<GLuint>(reinterpret_cast<std::uintptr_t>(static_cast<H>(handle)));
} else {
return static_cast<GLuint>(handle);
}
}
// The answer behind GetObjectParameter{if}vARB, and the reason neither of them carries a pname
// translation table: the GL_OBJECT_*_ARB tokens ARE the core tokens, numerically. Core GL took the
// object pnames over unchanged when ARB_shader_objects was folded in -
// GL_OBJECT_DELETE_STATUS_ARB through GL_OBJECT_SHADER_SOURCE_LENGTH_ARB are 0x8B80-0x8B88, the
// same numbers glcorearb.h gives GL_DELETE_STATUS through GL_SHADER_SOURCE_LENGTH
// (glext.h:4086-4094) - so the mapping is a RENAME and the same pname goes to the core getter of
// the same object kind. Handing it to that getter is also the right ERROR behaviour, and the reason
// this does not whitelist pnames: GL_OBJECT_LINK_STATUS_ARB asked of a SHADER has to come back
// INVALID_ENUM, and glGetShaderiv is where the set of pnames a shader accepts is defined.
//
// The one token core never took is GL_OBJECT_TYPE_ARB (0x8B4E; core's GL_OBJECT_TYPE, 0x9112, is
// ARB_program_interface_query's interface class, a different question with a different value
// space), so the object kind is the single thing answered here without a core getter. It is also
// the only way an ARB-era caller can ask whether a handle names a program or a shader: there is no
// glIsProgramARB/glIsShaderARB in ARB_shader_objects (include/GL/glext.h:4095-4173 declares
// neither, and the glIsProgramARB the registry does know, glext.h:3280, belongs to
// ARB_vertex_program/ARB_fragment_program - those ASM program objects are not these).
static MobileGL::Bool AnswerObjectParameterARB(GLuint name, GLenum pname, GLint* params) {
if (pname == GL_OBJECT_TYPE_ARB) {
if (glIsProgram(name)) {
*params = GL_PROGRAM_OBJECT_ARB;
return true;
}
if (glIsShader(name)) {
*params = GL_SHADER_OBJECT_ARB;
return true;
}
// The error ARB_vertex_shader's own text specifies: "The error INVALID_VALUE is generated
// by any command that takes one or more handles as input, and one or more handles are not
// an object handle generated by OpenGL."
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidValue,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::to_string(name) + " is not an object handle generated by OpenGL."));
return false;
}
if (glIsProgram(name)) {
glGetProgramiv(name, pname, params);
return true;
}
// A shader, or a name that is neither. glGetShaderiv owns both the shader pname set and the
// name validation, which is the one place that splits a program name handed to a shader query
// (INVALID_OPERATION) from a name GL never generated (INVALID_VALUE) - the split GL 3.3 core
// 2.11 requires. It writes nothing in either error case, which is what the caller reads as
// "no answer".
const MobileGL::Bool isShader = glIsShader(name) == GL_TRUE;
glGetShaderiv(name, pname, params);
return isShader;
}
MOBILEGL_GL_API void glDeleteObjectARB(GLhandleARB obj) {
// ARB_shader_objects has ONE object namespace - a handle names a program or a shader and
// nothing else - and DeleteObjectARB is the deletion entry point for both kinds, so the kind
// is decided here. IsProgram/IsShader answer over the same single namespace this
// implementation hands names out of, so a program name cannot be taken for a shader name.
// Zero, and a handle that names neither object, are ignored: there is no object to delete.
const GLuint name = FromGLHandleARB(obj);
if (glIsProgram(name)) {
glDeleteProgram(name);
} else if (glIsShader(name)) {
glDeleteShader(name);
}
}
MOBILEGL_GL_API GLhandleARB glGetHandleARB(GLenum pname) {
if (pname != GL_PROGRAM_OBJECT_ARB) {
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidEnum,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
"pname " + std::to_string(pname) + " is not GL_PROGRAM_OBJECT_ARB."));
return ToGLHandleARB(0u);
}
// "The program object currently in use", read from the state GL_CURRENT_PROGRAM is answered
// from - so a program bound through glUseProgram is found here too, and a
// UseProgramObjectARB(0) reads back as the zero handle.
const auto& currentProgram = MobileGL::MG_State::pGLContext->GetCurrentProgram();
return ToGLHandleARB(currentProgram ? static_cast<GLuint>(currentProgram->GetExternalIndex()) : 0u);
}
MOBILEGL_GL_API void glDetachObjectARB(GLhandleARB containerObj, GLhandleARB attachedObj) {
glDetachShader(FromGLHandleARB(containerObj), FromGLHandleARB(attachedObj));
}
MOBILEGL_GL_API GLhandleARB glCreateShaderObjectARB(GLenum shaderType) {
return ToGLHandleARB(glCreateShader(shaderType));
}
MOBILEGL_GL_API void glShaderSourceARB(GLhandleARB shaderObj, GLsizei count, const GLcharARB** string,
const GLint* length) {
glShaderSource(FromGLHandleARB(shaderObj), count, string, length);
}
MOBILEGL_GL_API void glCompileShaderARB(GLhandleARB shaderObj) {
glCompileShader(FromGLHandleARB(shaderObj));
}
MOBILEGL_GL_API GLhandleARB glCreateProgramObjectARB(void) {
return ToGLHandleARB(glCreateProgram());
}
MOBILEGL_GL_API void glAttachObjectARB(GLhandleARB containerObj, GLhandleARB obj) {
glAttachShader(FromGLHandleARB(containerObj), FromGLHandleARB(obj));
}
MOBILEGL_GL_API void glLinkProgramARB(GLhandleARB programObj) {
glLinkProgram(FromGLHandleARB(programObj));
}
MOBILEGL_GL_API void glUseProgramObjectARB(GLhandleARB programObj) {
glUseProgram(FromGLHandleARB(programObj));
}
MOBILEGL_GL_API void glValidateProgramARB(GLhandleARB programObj) {
glValidateProgram(FromGLHandleARB(programObj));
}
MOBILEGL_GL_API void glUniform1fARB(GLint location, GLfloat v0) {
glUniform1f(location, v0);
}
MOBILEGL_GL_API void glUniform2fARB(GLint location, GLfloat v0, GLfloat v1) {
glUniform2f(location, v0, v1);
}
MOBILEGL_GL_API void glUniform3fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2) {
glUniform3f(location, v0, v1, v2);
}
MOBILEGL_GL_API void glUniform4fARB(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) {
glUniform4f(location, v0, v1, v2, v3);
}
MOBILEGL_GL_API void glUniform1iARB(GLint location, GLint v0) {
glUniform1i(location, v0);
}
MOBILEGL_GL_API void glUniform2iARB(GLint location, GLint v0, GLint v1) {
glUniform2i(location, v0, v1);
}
MOBILEGL_GL_API void glUniform3iARB(GLint location, GLint v0, GLint v1, GLint v2) {
glUniform3i(location, v0, v1, v2);
}
MOBILEGL_GL_API void glUniform4iARB(GLint location, GLint v0, GLint v1, GLint v2, GLint v3) {
glUniform4i(location, v0, v1, v2, v3);
}
MOBILEGL_GL_API void glUniform1fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform1fv(location, count, value);
}
MOBILEGL_GL_API void glUniform2fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform2fv(location, count, value);
}
MOBILEGL_GL_API void glUniform3fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform3fv(location, count, value);
}
MOBILEGL_GL_API void glUniform4fvARB(GLint location, GLsizei count, const GLfloat* value) {
glUniform4fv(location, count, value);
}
MOBILEGL_GL_API void glUniform1ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform1iv(location, count, value);
}
MOBILEGL_GL_API void glUniform2ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform2iv(location, count, value);
}
MOBILEGL_GL_API void glUniform3ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform3iv(location, count, value);
}
MOBILEGL_GL_API void glUniform4ivARB(GLint location, GLsizei count, const GLint* value) {
glUniform4iv(location, count, value);
}
MOBILEGL_GL_API void glUniformMatrix2fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
glUniformMatrix2fv(location, count, transpose, value);
}
MOBILEGL_GL_API void glUniformMatrix3fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
glUniformMatrix3fv(location, count, transpose, value);
}
MOBILEGL_GL_API void glUniformMatrix4fvARB(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) {
glUniformMatrix4fv(location, count, transpose, value);
}
MOBILEGL_GL_API void glGetObjectParameterfvARB(GLhandleARB obj, GLenum pname, GLfloat* params) {
// The fv form asks the same questions - the ARB object model holds no float-valued object
// state - so the answer is the iv answer widened. A query the iv path refuses leaves the
// output untouched here as well, rather than putting a plausible-looking 0.0 where there is
// no answer.
GLint value = 0;
if (!AnswerObjectParameterARB(FromGLHandleARB(obj), pname, &value)) return;
*params = static_cast<GLfloat>(value);
}
MOBILEGL_GL_API void glGetObjectParameterivARB(GLhandleARB obj, GLenum pname, GLint* params) {
AnswerObjectParameterARB(FromGLHandleARB(obj), pname, params);
}
MOBILEGL_GL_API void glGetInfoLogARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length, GLcharARB* infoLog) {
// The same handle-kind dispatch as AnswerObjectParameterARB, and the same fallback: the shader
// getter is also where the name validation and its error code live.
const GLuint name = FromGLHandleARB(obj);
if (glIsProgram(name)) {
glGetProgramInfoLog(name, maxLength, length, infoLog);
} else {
glGetShaderInfoLog(name, maxLength, length, infoLog);
}
}
MOBILEGL_GL_API void glGetAttachedObjectsARB(GLhandleARB containerObj, GLsizei maxCount, GLsizei* count,
GLhandleARB* obj) {
// The caller's array holds GLhandleARB while glGetAttachedShaders writes GLuint names, and on
// Apple those are different sizes, so the ids are gathered first and converted one at a time.
// A negative maxCount is passed through: glGetAttachedShaders is where its INVALID_VALUE is.
MobileGL::Vector<GLuint> shaders(maxCount > 0 ? static_cast<MobileGL::SizeT>(maxCount) : 0, 0);
GLsizei attached = 0;
glGetAttachedShaders(FromGLHandleARB(containerObj), maxCount, &attached, shaders.data());
for (GLsizei i = 0; i < attached; ++i) {
obj[i] = ToGLHandleARB(shaders[i]);
}
if (count) *count = attached;
}
MOBILEGL_GL_API GLint glGetUniformLocationARB(GLhandleARB programObj, const GLcharARB* name) {
return glGetUniformLocation(FromGLHandleARB(programObj), name);
}
MOBILEGL_GL_API void glGetActiveUniformARB(GLhandleARB programObj, GLuint index, GLsizei maxLength,
GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
glGetActiveUniform(FromGLHandleARB(programObj), index, maxLength, length, size, type, name);
}
MOBILEGL_GL_API void glGetUniformfvARB(GLhandleARB programObj, GLint location, GLfloat* params) {
glGetUniformfv(FromGLHandleARB(programObj), location, params);
}
MOBILEGL_GL_API void glGetUniformivARB(GLhandleARB programObj, GLint location, GLint* params) {
glGetUniformiv(FromGLHandleARB(programObj), location, params);
}
MOBILEGL_GL_API void glGetShaderSourceARB(GLhandleARB obj, GLsizei maxLength, GLsizei* length,
GLcharARB* source) {
glGetShaderSource(FromGLHandleARB(obj), maxLength, length, source);
}
// ARB_vertex_shader's three entry points. The generic-attribute family that extension also
// describes is spelled with the ARB_vertex_program names (glVertexAttribPointerARB and the rest),
// which glext.h declares in that other block and which MobileGL does not advertise; nothing here
// claims them.
MOBILEGL_GL_API void glBindAttribLocationARB(GLhandleARB programObj, GLuint index, const GLcharARB* name) {
glBindAttribLocation(FromGLHandleARB(programObj), index, name);
}
MOBILEGL_GL_API void glGetActiveAttribARB(GLhandleARB programObj, GLuint index, GLsizei maxLength,
GLsizei* length, GLint* size, GLenum* type, GLcharARB* name) {
glGetActiveAttrib(FromGLHandleARB(programObj), index, maxLength, length, size, type, name);
}
MOBILEGL_GL_API GLint glGetAttribLocationARB(GLhandleARB programObj, const GLcharARB* name) {
return glGetAttribLocation(FromGLHandleARB(programObj), name);
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, NamedStringARB, GLenum type, GLint namelen, const GLchar* name, GLint strinen, const GLchar* string) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, NamedStringARB, type, namelen, name, strinen, string)
DECLARE_GL_FUNCTION_STUB_HEAD(void, DeleteNamedStringARB, GLint namelen, const GLchar* name) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, DeleteNamedStringARB, namelen, name)
DECLARE_GL_FUNCTION_STUB_HEAD(void, CompileShaderIncludeARB, GLuint shader, GLsizei count, const GLchar* const* path, const GLint* length) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, CompileShaderIncludeARB, shader, count, path, length)
@@ -1713,6 +2042,141 @@ DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendEnviATI, GLenum pname, GLint para
DECLARE_GL_FUNCTION_STUB_HEAD(void, VertexBlendEnvfATI, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, VertexBlendEnvfATI, pname, param)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EGLImageTargetTexStorageEXT, GLenum target, GLeglImageOES image, const GLint* attrib_list) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EGLImageTargetTexStorageEXT, target, image, attrib_list)
DECLARE_GL_FUNCTION_STUB_HEAD(void, EGLImageTargetTextureStorageEXT, GLuint texture, GLeglImageOES image, const GLint* attrib_list) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, EGLImageTargetTextureStorageEXT, texture, image, attrib_list)
// GL_OES_EGL_image.
//
// MEASURED, and the whole reason this block exists: libepoxy in kwin_wayland prints
// No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image
// and refuses the import path there - and that path is how the compositor's scene reaches the
// display daemon's dma-bufs, i.e. how a Linux desktop gets onto the phone's screen. The EGL half is
// real: eglCreateImageKHR builds an EGL_LINUX_DMA_BUF_EXT image that owns a dup() of every plane's
// descriptor and carries that buffer's geometry and fourcc (MG_State/EGLState/Core.h). So a handle
// that arrives here is resolvable, and what these two entry points do is exactly that: resolve it,
// refuse what names nothing, and say ONCE what is not wired yet. What is not wired yet is the
// render-server half - telling the server which texture a dma-buf backs, over the one channel that
// can carry a descriptor across the container boundary - so a framebuffer built over an imported
// texture is not complete until that lands. Pretending otherwise (attaching the image to a texture
// object that has no storage behind it) would turn "not implemented" into "mysteriously black",
// which is what the log line below is here to prevent.
MOBILEGL_GL_API void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image) {
// 0x8D65 is GL_TEXTURE_EXTERNAL_OES, which the headers this file compiles against do not spell
// out. It matters here MORE than GL_TEXTURE_2D does: a compositor that could not map an
// imported dma-buf's format to a non-external one binds its image to THIS target, so refusing it
// would turn "no format map yet" into an INVALID_ENUM the caller cannot act on (kwin 6.7.3 does
// exactly that when EglDisplay::queryImportFormats() answers nothing).
constexpr GLenum kTextureExternalOes = 0x8D65;
if (target != GL_TEXTURE_2D && target != kTextureExternalOes) {
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidEnum,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"target is not a texture target this entry point takes."));
return;
}
EGLint imageWidth = 0;
EGLint imageHeight = 0;
EGLint imageFourCC = 0;
EGLint imagePlanes = 0;
const MobileGL::Bool imageKnown =
MobileGL::MG_State::pEGLContext != nullptr &&
MobileGL::MG_State::pEGLContext->DescribeImage(reinterpret_cast<::EGLImage>(image), &imageWidth, &imageHeight,
&imageFourCC, &imagePlanes);
if (!imageKnown) {
// A handle this side did not hand out: ARB/OES text puts that on INVALID_VALUE, and it is
// the answer a caller can act on rather than a texture that renders nowhere.
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidValue,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"image is not an EGLImage this display created."));
return;
}
MGLOG_W_ONCE("glEGLImageTargetTexture2DOES: taking target 0x%04x over an EGLImage of %dx%d fourcc 0x%08x "
"(%d plane(s)) that this side holds; the render server has NOT been told which texture that "
"dma-buf backs yet, so a framebuffer over this texture is not complete (the fd transport over "
"the GBM socket and the server-side import are the next change)",
target, imageWidth, imageHeight, imageFourCC, imagePlanes);
// AND NOW WIRE IT: a texture bound to an imported EGLImage has no storage on the render server,
// and a framebuffer built over it there is not complete - kwin 6.7.3 aborts on its own
// RenderTarget assertion right after this log line (measured twice). So the server is told which
// buffer backs the texture: one message per plane, each carrying that plane's descriptor over the
// session transport's SCM_RIGHTS channel. No session, or a transport without that channel, is a
// refusal by name from ShareImportedDmaBuf - the texture simply keeps having no storage, which is
// the honest state rather than a silent render into nothing.
MGLOG_I("dma-buf import: entry (target=0x%04x, image=%p)", static_cast<unsigned>(target), static_cast<void*>(image));
if (MobileGL::MG_State::pEGLContext != nullptr) {
constexpr MobileGL::Int kDmaBufImportPlanes = 4;
EGLint planeCount = 0;
EGLint planeFds[kDmaBufImportPlanes] = {-1, -1, -1, -1};
EGLint planeOffsets[kDmaBufImportPlanes] = {0, 0, 0, 0};
EGLint planePitches[kDmaBufImportPlanes] = {0, 0, 0, 0};
MobileGL::Uint64 planeModifiers[kDmaBufImportPlanes] = {0, 0, 0, 0};
MobileGL::Bool planeHasModifier[kDmaBufImportPlanes] = {false, false, false, false};
if (MobileGL::MG_State::pEGLContext->DescribeImagePlanes(
reinterpret_cast<::EGLImage>(image), &planeCount, planeFds, planeOffsets, planePitches,
planeModifiers, planeHasModifier, kDmaBufImportPlanes)) {
// The texture the image was bound to. 0x8D65 has no TextureTarget member in this state
// layer (nothing else here binds it), so the name is looked up through Texture2D and may
// come back 0 - the server logs what it was told rather than guessing.
MobileGL::Uint32 texName = 0;
if (MobileGL::MG_State::pGLContext != nullptr) {
const MobileGL::Int unit = MobileGL::MG_State::pGLContext->GetActiveTextureUnit();
// THE EXTERNAL SLOT, which is where a compositor's imported dma-buf texture lives.
const auto& bound = MobileGL::MG_State::pGLContext->GetTextureUnitObject(unit)
.GetBindingSlot(MobileGL::TextureTarget::TextureExternalOES)
.GetBoundObject();
if (bound != nullptr) {
texName = static_cast<MobileGL::Uint32>(bound->GetExternalIndex());
// AN IMPORTED TEXTURE IS BACKED, SO IT IS COMPLETE. Without this the state layer
// answers incompletely (TextureObjectBase::IsComplete only looks at the internal
// format) and the compositor, having attached it to a framebuffer, reports
// "framebuffer for dmabuf 0 is not complete" and stops before drawing anything.
bound->SetInternalFormat(MobileGL::TextureInternalFormat::RGBA8);
}
}
MGLOG_I("dma-buf import: textures resolved (texName=%u planes=%d), sharing", texName,
static_cast<int>(planeCount));
for (MobileGL::Int plane = 0; plane < planeCount; plane++) {
if (planeFds[plane] < 0) continue;
MGLOG_I("dma-buf import: sharing plane %d fd=%d", static_cast<int>(plane), planeFds[plane]);
(void)MobileGL::MG_Remote::Client::ShareImportedDmaBuf(
static_cast<MobileGL::Uint32>(target), texName, static_cast<MobileGL::Uint32>(imageWidth),
static_cast<MobileGL::Uint32>(imageHeight), static_cast<MobileGL::Uint32>(imageFourCC),
static_cast<MobileGL::Uint32>(plane), static_cast<MobileGL::Uint32>(planeCount), planeFds[plane],
planePitches[plane], planeOffsets[plane], planeModifiers[plane], planeHasModifier[plane]);
MGLOG_I("dma-buf import: plane %d shared", static_cast<int>(plane));
}
}
}
}
MOBILEGL_GL_API void glEGLImageTargetRenderbufferStorageOES(GLenum target, GLeglImageOES image) {
if (target != GL_RENDERBUFFER) {
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidEnum,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"target is not GL_RENDERBUFFER."));
return;
}
EGLint imageWidth = 0;
EGLint imageHeight = 0;
EGLint imageFourCC = 0;
EGLint imagePlanes = 0;
const MobileGL::Bool imageKnown =
MobileGL::MG_State::pEGLContext != nullptr &&
MobileGL::MG_State::pEGLContext->DescribeImage(reinterpret_cast<::EGLImage>(image), &imageWidth, &imageHeight,
&imageFourCC, &imagePlanes);
if (!imageKnown) {
MobileGL::MG_State::pGLContext->RecordError(
MobileGL::ErrorCode::InvalidValue,
MobileGL::MakeUnique<MobileGL::GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"image is not an EGLImage this display created."));
return;
}
MGLOG_W_ONCE("glEGLImageTargetRenderbufferStorageOES: the renderbuffer takes an EGLImage of %dx%d fourcc "
"0x%08x; the render-server half of the import is the next change, exactly as for the texture "
"entry point beside this one",
imageWidth, imageHeight, imageFourCC);
}
DECLARE_GL_FUNCTION_STUB_HEAD(void, UniformBufferEXT, GLuint program, GLint location, GLuint buffer) DECLARE_GL_FUNCTION_STUB_END_NO_RETURN(void, UniformBufferEXT, program, location, buffer)
DECLARE_GL_FUNCTION_STUB_HEAD(GLint, GetUniformBufferSizeEXT, GLuint program, GLint location) DECLARE_GL_FUNCTION_STUB_END(GLint, GetUniformBufferSizeEXT, program, location)
DECLARE_GL_FUNCTION_STUB_HEAD(GLintptr, GetUniformOffsetEXT, GLuint program, GLint location) DECLARE_GL_FUNCTION_STUB_END(GLintptr, GetUniformOffsetEXT, program, location)
@@ -100,6 +100,13 @@ namespace MobileGL::MG_Impl::GLImpl {
}
Bool HasDefinedAttachment(const MG_State::GLState::FramebufferObject& framebufferObject) {
// NOT gated on the host frame, and that is deliberate: this only decides the ERROR CODE
// once completeness has already failed (CheckFramebufferStatus_State asks
// CheckCompleteness first), and the imported dma-buf's attachment is counted as valid
// THERE (MG_State/GLState/FramebufferState/FramebufferObject.cpp, whose gate lands before
// its own validity test for exactly this reason). Touching it here would add a second
// place that has to know the backend's frame - and the frontend knowing that fact is the
// compromise the completeness comment already names the end of.
for (const auto& attachment : framebufferObject.GetAllAttachmentObjects()) {
if (attachment.IsValid() && !attachment.IsEmpty()) {
return true;
@@ -611,7 +618,9 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown ||
textureObject->GetTarget() != expectedTextureTarget) {
(textureObject->GetTarget() != expectedTextureTarget &&
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
expectedTextureTarget == TextureTarget::Texture2D))) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
@@ -1382,7 +1391,9 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown ||
textureObject->GetTarget() != expectedTextureTarget) {
(textureObject->GetTarget() != expectedTextureTarget &&
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
expectedTextureTarget == TextureTarget::Texture2D))) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
@@ -1532,7 +1543,9 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto expectedTextureTarget = MG_Util::ConvertTextureUploadTargetToTextureTarget(textureUploadTarget);
if (expectedTextureTarget == TextureTarget::Unknown ||
textureObject->GetTarget() != expectedTextureTarget) {
(textureObject->GetTarget() != expectedTextureTarget &&
!(textureObject->GetTarget() == TextureTarget::TextureExternalOES &&
expectedTextureTarget == TextureTarget::Texture2D))) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>(
+51
View File
@@ -869,6 +869,55 @@ namespace MobileGL::MG_Impl {
GETPROC(glNamedBufferPageCommitmentARB, name);
GETPROC(glTexPageCommitmentARB, name);
GETPROC(glTexBufferARB, name);
// ARB_shader_objects and ARB_vertex_shader, by name, because a name is the only way an
// application ever asks for an extension entry point. Measured path: kwin_wayland resolves GL
// through libepoxy, and libepoxy asks eglGetProcAddress -> EGLImpl::GetProcAddress -> this
// table. Before these lines the callers that need the extension whose STRINGS make
// checkSupported() pass got a nullptr for every function of it - dlsym on the library found
// them, this table did not. The strings and the implementations are advertised from
// MG_Impl/GLImpl/Exporting/Definitions.cpp and the two BackendObject_* extension lists.
GETPROC(glDeleteObjectARB, name);
GETPROC(glGetHandleARB, name);
GETPROC(glDetachObjectARB, name);
GETPROC(glCreateShaderObjectARB, name);
GETPROC(glShaderSourceARB, name);
GETPROC(glCompileShaderARB, name);
GETPROC(glCreateProgramObjectARB, name);
GETPROC(glAttachObjectARB, name);
GETPROC(glLinkProgramARB, name);
GETPROC(glUseProgramObjectARB, name);
GETPROC(glValidateProgramARB, name);
GETPROC(glUniform1fARB, name);
GETPROC(glUniform2fARB, name);
GETPROC(glUniform3fARB, name);
GETPROC(glUniform4fARB, name);
GETPROC(glUniform1iARB, name);
GETPROC(glUniform2iARB, name);
GETPROC(glUniform3iARB, name);
GETPROC(glUniform4iARB, name);
GETPROC(glUniform1fvARB, name);
GETPROC(glUniform2fvARB, name);
GETPROC(glUniform3fvARB, name);
GETPROC(glUniform4fvARB, name);
GETPROC(glUniform1ivARB, name);
GETPROC(glUniform2ivARB, name);
GETPROC(glUniform3ivARB, name);
GETPROC(glUniform4ivARB, name);
GETPROC(glUniformMatrix2fvARB, name);
GETPROC(glUniformMatrix3fvARB, name);
GETPROC(glUniformMatrix4fvARB, name);
GETPROC(glGetObjectParameterfvARB, name);
GETPROC(glGetObjectParameterivARB, name);
GETPROC(glGetInfoLogARB, name);
GETPROC(glGetAttachedObjectsARB, name);
GETPROC(glGetUniformLocationARB, name);
GETPROC(glGetActiveUniformARB, name);
GETPROC(glGetUniformfvARB, name);
GETPROC(glGetUniformivARB, name);
GETPROC(glGetShaderSourceARB, name);
GETPROC(glBindAttribLocationARB, name);
GETPROC(glGetActiveAttribARB, name);
GETPROC(glGetAttribLocationARB, name);
GETPROC(glDepthRangeArraydvNV, name);
GETPROC(glDepthRangeIndexeddNV, name);
GETPROC(glBlendBarrierKHR, name);
@@ -886,6 +935,8 @@ namespace MobileGL::MG_Impl {
GETPROC(glBeginPerfMonitorAMD, name);
GETPROC(glEndPerfMonitorAMD, name);
GETPROC(glGetPerfMonitorCounterDataAMD, name);
GETPROC(glEGLImageTargetTexture2DOES, name);
GETPROC(glEGLImageTargetRenderbufferStorageOES, name);
GETPROC(glEGLImageTargetTexStorageEXT, name);
GETPROC(glEGLImageTargetTextureStorageEXT, name);
GETPROC(glLabelObjectEXT, name);
+14
View File
@@ -1828,6 +1828,11 @@ namespace MobileGL::MG_Pipe {
*entry = Entry{};
}
void Clear() {
for (auto& table : m_kinds) table.clear();
m_band.clear();
}
private:
struct Entry {
Uint32 Gen = 0;
@@ -1892,6 +1897,15 @@ namespace MobileGL::MG_Pipe {
PublicationLatch().NoteUnpublished(kind, handle);
}
void MGPipeForgetAllPublications() {
PublicationLatch().Clear();
// The two families whose death asks their own record rather than the latch: vertex
// elements (MGPipeEmitVertexElementsDestroyAndFree's RecordIsPublished) and buffers
// (MGPipeEmitResourceDestroyAndFree's WasPublished).
MGPipeVertexInputEmitterInstance().ForgetAllRecords();
MGPipeResourceTrackerInstance().ForgetAllPublished();
}
// THE GATE ITSELF, AS AN OBSERVABLE (ID-39, widened by S-3 / ID-41). Every P4a birth hook
// below and every `wants()` row in the walk resolve through FamilyIsLive /
// P4aFamilyHasItsConsumer / P4aFamilyDependenciesAreSet, and none of the three is reachable
+4
View File
@@ -340,6 +340,10 @@ namespace MobileGL::MG_Pipe {
const SizeT slot = handle.Slot;
return slot < m_bySlot.size() && m_bySlot[slot].Published;
}
// The session that held every buffer's record is gone (MGPipeForgetAllPublications).
void ForgetAllPublished() {
for (Entry& entry : m_bySlot) entry.Published = false;
}
// The sticky everBoundAs mask. Sticky exactly as MGPResourceDesc::ImageBindableHint's
// everImageBound is: ORed, never cleared, so a buffer that was an element array once
+8
View File
@@ -351,6 +351,14 @@ namespace MobileGL::MG_Pipe {
m_boundLifetimeId = 0;
}
// The session that held the records is gone (MGPipeForgetAllPublications): both halves go,
// the record half included, because no applier holds any of them any more.
void ForgetAllRecords() {
for (Latch& latch : m_latch) latch = Latch{};
m_boundHandle = kMGPipeNullHandle;
m_boundLifetimeId = 0;
}
void ResetCounters() { m_creates = m_binds = m_bufferSets = m_indexSets = 0; }
private:
+6 -4
View File
@@ -3119,10 +3119,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
"MOBILEGL_IPC_LOG_FORWARD=1" "MOBILEGL_IPC_TOKEN="
"MGITEST_TCP_LANE=1" "MOBILEGL_ITEST_REQUIRE_GPU=1")
set(mglD2Extra FIXTURES_REQUIRED mobilegl-tcp)
# The loopback supervisor serves ONE session (ServerMain.cpp answers a second Hello
# Refuse{Busy}), and every other Tcp entry holds this lock. Without it `ctest -L
# integration-gpu -j N` ran this case beside another Tcp case and whichever client
# connected second died Refuse{Busy} -> Fatal{CapsBeforeFirstSnapshot}.
# The loopback supervisor used to serve ONE session (a second Hello was answered
# Refuse{Busy}), and `ctest -L integration-gpu -j N` ran this case beside another Tcp
# case so that whichever client connected second died Refuse{Busy} ->
# Fatal{CapsBeforeFirstSnapshot}. It serves several side by side now (ServerMain.cpp
# TcpSupervisor, fork shape); every Tcp entry still holds this lock, so the cases do not
# share the fixture's one server and its log while they run.
# scripts/ci/spawn_lane_parity.py and junit_tally.py --discovery now refuse any entry
# on the fixture that does not hold it.
set(mglD2Locks "${mglD2LogName}\;mobilegl-tcp")
+6
View File
@@ -214,6 +214,8 @@ namespace MobileGL::MG_Pipe {
TexCubeArray,
Tex2DMS,
Tex2DMSArray,
// GL_TEXTURE_EXTERNAL_OES
TexExternalOES,
Renderbuffer,
TexBuffer,
TexRect,
@@ -263,6 +265,10 @@ namespace MobileGL::MG_Pipe {
return static_cast<Uint32>(MGPipeResourceTarget::TexCubeArray);
case MobileGL::TextureTarget::Texture2DMultisampleArray:
return static_cast<Uint32>(MGPipeResourceTarget::Tex2DMSArray);
// GL_TEXTURE_EXTERNAL_OES gets its own row for the same reason TextureRectangle has one: it is
// a real target a descriptor may carry (the compositor's imported dma-buf textures live on it).
case MobileGL::TextureTarget::TextureExternalOES:
return static_cast<Uint32>(MGPipeResourceTarget::TexExternalOES);
// NOT TEXTURE TARGETS. Listed rather than defaulted so the completeness assert still
// sees them, and mapped to the sentinel because no descriptor may carry either: the
// count is the enum's bound and Unknown is what an unresolved GL enum becomes.
+6
View File
@@ -341,6 +341,12 @@ namespace MobileGL::MG_Pipe {
void MGPipeNoteHandlePublished(MGPipeKind kind, MGPipeHandle handle);
Bool MGPipeHandleIsPublished(MGPipeKind kind, MGPipeHandle handle);
void MGPipeNoteHandleUnpublished(MGPipeKind kind, MGPipeHandle handle);
// THE SESSION IS GONE, AND EVERYTHING IT HELD WITH IT. Called once the split roles are shut
// down: every object the server twinned died with the session, so nothing is published any
// more - and the frontend objects that die after it (MobileGL::Destroy resets the GL context
// after ShutdownSplitRoles, by design) must not route a delete for a twin that no longer
// exists to whichever applier is installed now, which asserts on the unknown handle.
void MGPipeForgetAllPublications();
// ---- the four mints (pure allocator work, no family knowledge) ----
//
@@ -216,6 +216,10 @@ namespace MobileGL::MG_Remote::Client {
WaitForApplyBeforeEglForwarder("CreateEGLWindowSurface");
// P12 (D1): unless the window is the SERVER's - then there is no handle of ours to tell.
if (MG_Config::ServerOwnedWindowSurfaces()) return CreateServerOwnedWindowSurface(surface, handle);
// The other on-screen shape: the frames belong to the display host, and the server takes
// them from the bridge. As for the server-owned shape, no window of the client's crosses
// the wire - only the size it asked for.
if (MG_Config::HostFrameWindowSurfaces()) return CreateHostFrameWindowSurface(surface, handle);
Server::ServerSetWindowHandle(handle);
if (!Server::ServerCreateEGLWindowSurface(surface, handle)) return false;
// The server's surface init published the default framebuffer's shape as a
@@ -295,6 +299,10 @@ namespace MobileGL::MG_Remote::Client {
// dereferenced - the client's InitWindowSurface is a no-op - and never crosses the wire
// (the ServerOwned frame carries token 0).
char g_serverOwnedWindowPlaceholder = 0;
// The host-framed shape names no window of the client's either; the base class still wants
// a handle to register the surface with, and this is the same stand-in the server-owned
// shape uses.
char g_hostFrameWindowPlaceholder = 0;
} // namespace
Bool BackendObject_Remote::CreateServerOwnedWindowSurface(EGLSurface surface,
@@ -360,6 +368,46 @@ namespace MobileGL::MG_Remote::Client {
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local);
}
Bool BackendObject_Remote::CreateHostFrameWindowSurface(EGLSurface surface,
const MG_Backend::WindowHandle& handle) {
// The size crosses the wire and nothing else of the client's does: the server takes frames
// from the display host at that size and answers with the extent the surface has.
const Server::ServerOwnedWindowReply reply =
Server::ServerCreateHostFrameWindowSurface(surface, handle.Width, handle.Height);
if (!reply.ok) {
// FAILS BY NAME ON THIS SIDE TOO, as the server-owned shape does: the server logged its
// reason, and the refusal code is what lets this line say the same thing.
switch (reply.refusal) {
case Server::SurfaceRefusalCode::SurfaceModeMismatch:
MGLOG_E("MG_Remote client: Refuse HostFrame (SurfaceModeMismatch) - MOBILEGL_IPC_SURFACE=host "
"asked for this %ux%u window surface in a session that already chose a pbuffer: only "
"one rendering path is active per session. eglCreateWindowSurface fails with "
"EGL_BAD_NATIVE_WINDOW",
handle.Width, handle.Height);
break;
case Server::SurfaceRefusalCode::NoServerWindow:
MGLOG_E("MG_Remote client: Refuse HostFrame (NoServerWindow) - the server could not take a frame "
"from the display host for this %ux%u surface; its log names what the host or the "
"bridge said (its own answer was %ux%u)",
handle.Width, handle.Height, reply.width, reply.height);
break;
default:
MGLOG_E("MG_Remote client: Refuse HostFrame (code %u) for this %ux%u window surface",
static_cast<Uint>(reply.refusal), handle.Width, handle.Height);
break;
}
return false;
}
// From here the frames are the server's and the host's business; this side only registers the
// surface, exactly as it does for the server-owned shape.
MG_Backend::WindowHandle local = handle;
if (local.Backend == MG_Backend::WindowBackend::Unknown) local.Backend = MG_Backend::WindowBackend::Android;
if (local.Handle == nullptr) local.Handle = &g_hostFrameWindowPlaceholder;
local.Width = reply.width;
local.Height = reply.height;
return MG_Backend::BackendObject::CreateEGLWindowSurface(surface, local);
}
Bool BackendObject_Remote::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read,
EGLContext ctx) {
// FORWARD FIRST, THEN RUN THE BASE. The server has to own the context before the base
@@ -91,6 +91,9 @@ namespace MobileGL::MG_Remote::Client {
// CreateEGLWindowSurface - ONE ServerOwned frame, no SetWindowHandle, the server's real
// geometry adopted before it returns, and every refusal named in this process's log.
Bool CreateServerOwnedWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
// MOBILEGL_IPC_SURFACE=host: the frames are the display host's, and the server takes
// them from the host frame bridge. Nothing of the client's crosses the wire here either.
Bool CreateHostFrameWindowSurface(EGLSurface surface, const MG_Backend::WindowHandle& handle);
// The generation of the snapshot m_formatCapabilities was filled from. Exposed only
// through the log line on a refresh: a cache that silently stopped tracking the mirror
@@ -0,0 +1,43 @@
// MobileGL - MobileGL/MG_Remote/Client/DmaBufImport.cpp
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "DmaBufImport.h"
#include "ClientSession.h"
#include "../Protocol/mg_protocol_base.h"
#include "../Transport/ITransport.h"
namespace MobileGL::MG_Remote::Client {
Bool ShareImportedDmaBuf(Uint32 target, Uint32 texName, Uint32 width, Uint32 height, Uint32 fourCC,
Uint32 planeIndex, Uint32 planeCount, Int planeFd, Int stride, Int offset,
Uint64 modifier, Bool hasModifier) {
if (planeFd < 0 || planeCount == 0 || planeCount > kDmaBufImportMaxPlanes) return false;
// The session's control-plane transport is where the descriptor channel lives; no session or
// no channel (a TCP session whose aux came up without SCM_RIGHTS) is a named refusal here.
ClientSession* session = ClientSession::Active();
if (session == nullptr) return false;
Transport::ITransport* transport = session->Control_Plane();
if (transport == nullptr) return false;
DmaBufImportRequest request{};
request.Magic = kDmaBufImportRequestMagic;
request.Version = kDmaBufImportVersion;
request.Target = target;
request.TexName = texName;
request.Width = width;
request.Height = height;
request.FourCC = fourCC;
request.PlaneCount = planeCount;
request.Stride[planeIndex % kDmaBufImportMaxPlanes] = stride;
request.Offset[planeIndex % kDmaBufImportMaxPlanes] = offset;
request.Modifier[planeIndex % kDmaBufImportMaxPlanes] = modifier;
request.HasModifier[planeIndex % kDmaBufImportMaxPlanes] = hasModifier ? 1u : 0u;
MobileGLByteSpan sideband{};
sideband.data = &request;
sideband.size = sizeof(request);
return transport->ShareFd(planeFd, sideband) == MOBILEGL_OK;
}
} // namespace MobileGL::MG_Remote::Client
+53
View File
@@ -0,0 +1,53 @@
// MobileGL - MobileGL/MG_Remote/Client/DmaBufImport.h
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// TELLING THE RENDER SERVER WHICH TEXTURE A dma-buf BACKS.
//
// The compositor's scene reaches the display daemon's buffers through glEGLImageTargetTexture2DOES:
// eglCreateImageKHR already holds a dup() of every plane's descriptor, and a texture bound to that
// image has NO storage on the server - so a framebuffer built over it is not complete and kwin aborts
// on its own RenderTarget assertion (measured, twice: "taking target 0x8d65 over an EGLImage of
// 1440x3200 fourcc 0x34324241 ... the render server has NOT been told which texture that dma-buf
// backs yet"). This is the client's half of closing that: one message per plane, each carrying one
// descriptor in the SCM_RIGHTS control data of the session transport's aux socket (ITransport::ShareFd,
// FdPassing.h - "a dedicated AF_UNIX SOCK_DGRAM socketpair, so one sendmsg is exactly one recvmsg").
//
// The byte layout is restated from the server's reader rather than shared with it, for the same reason
// the GBM frame shapes are (MG_Backend/GbmFrameChannel.h): the two ends are built into different
// libraries from different build configurations. A change on either side is a protocol change, and the
// magic is what makes a mismatched pair fail loudly instead of being read as plausible numbers.
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Remote::Client {
inline constexpr Uint32 kDmaBufImportRequestMagic = 0x4947474Du; // 'M','G','G','I' in memory order
inline constexpr Uint32 kDmaBufImportVersion = 1u;
inline constexpr Uint32 kDmaBufImportMaxPlanes = 4u;
struct DmaBufImportRequest {
Uint32 Magic;
Uint32 Version;
Uint32 Target; // the GL texture target the image was bound to (0x8D65 = GL_TEXTURE_EXTERNAL_OES)
Uint32 TexName; // THIS side's texture name; the server resolves its twin
Uint32 Width;
Uint32 Height;
Uint32 FourCC;
Uint32 PlaneCount;
Int32 Stride[kDmaBufImportMaxPlanes];
Int32 Offset[kDmaBufImportMaxPlanes];
Uint64 Modifier[kDmaBufImportMaxPlanes];
Uint32 HasModifier[kDmaBufImportMaxPlanes];
} __attribute__((packed));
// Shares one plane's descriptor with the server, describing the buffer in the same fields
// drmModeAddFB2 takes. Answers false when the session has no descriptor channel (auxFd == -1:
// SocketTransport answers MOBILEGL_ERR_UNSUPPORTED) or when the send failed - the caller keeps
// treating the texture as having no storage, which is the honest state, rather than rendering
// into nothing.
Bool ShareImportedDmaBuf(Uint32 target, Uint32 texName, Uint32 width, Uint32 height, Uint32 fourCC,
Uint32 planeIndex, Uint32 planeCount, Int planeFd, Int stride, Int offset,
Uint64 modifier, Bool hasModifier);
} // namespace MobileGL::MG_Remote::Client
+13
View File
@@ -513,6 +513,18 @@ namespace MobileGL::MG_Remote::Client {
if (CreateIsSuspect(handle) || g_createSuspects.size() >= kCreateSuspectMax) return;
g_createSuspects.push_back(handle);
}
// THE WINDOW IS A SESSION'S, NOT THE PROCESS'S. Every entry names a sequence number of the
// session that wrote it, and a process can run several sessions one after another (eglinfo
// terminates one display and initializes the next). An entry left by the previous session
// made the new session's first create wait for a sequence number the new server would not
// reach for a long time (measured: the second session hung on its first texture create, in
// DrainCreateWindow's blocking WaitForApplied). Its answer, and the object it was about,
// died with that session; the suspects are that session's handles too.
void ForgetPreviousSessionCreates() {
g_createWindowHead = 0;
g_createWindowCount = 0;
g_createSuspects.clear();
}
void ClearCreateSuspect(MG_Pipe::MGPipeHandle handle) {
for (SizeT i = 0; i < g_createSuspects.size(); ++i) {
if (g_createSuspects[i].Slot != handle.Slot || g_createSuspects[i].Gen != handle.Gen)
@@ -1209,6 +1221,7 @@ namespace MobileGL::MG_Remote::Client {
void InstallClientWireTables() {
using namespace MG_Pipe;
ForgetPreviousSessionCreates();
// DirectGLES normally installs this notice while creating its handle
// backend. An independent client never creates that backend: without a
+33 -2
View File
@@ -58,6 +58,11 @@ namespace MobileGL::MG_Remote {
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::MetalLayer) == 6);
// P12's append, on the same terms: the value is wire ABI.
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::ServerOwned) == 7);
static_assert(static_cast<Uint8>(::MobileGL::Wire::WindowKind::HostFrame) == 8);
static_assert(static_cast<Uint8>(::MobileGL::Wire::SurfaceRefusal::HostFrameOnSetWindowHandle) ==
static_cast<Uint8>(Server::SurfaceRefusalCode::HostFrameOnSetWindowHandle));
static_assert(Server::kHostFrameWindowBackend > Server::kServerOwnedWindowBackend,
"kHostFrameWindowBackend must not collide with the server-owned tag");
// The frame's refusal codes ARE the wire's SurfaceRefusal (SurfaceControlFrame.h), pinned
// value by value so a schema edit that renumbers either side fails here.
using Server::SurfaceRefusalCode;
@@ -94,6 +99,8 @@ namespace MobileGL::MG_Remote {
case SurfaceWireError::AndroidNativeWindowArrived: return "AndroidNativeWindowArrived";
case SurfaceWireError::ServerOwnedTokenNotZero: return "ServerOwnedTokenNotZero";
case SurfaceWireError::ServerOwnedOnSetWindowHandle: return "ServerOwnedOnSetWindowHandle";
case SurfaceWireError::HostFrameTokenNotZero: return "HostFrameTokenNotZero";
case SurfaceWireError::HostFrameOnSetWindowHandle: return "HostFrameOnSetWindowHandle";
}
return "<unknown SurfaceWireError>";
}
@@ -167,6 +174,15 @@ namespace MobileGL::MG_Remote {
}
windowKind = ::MobileGL::Wire::WindowKind::ServerOwned;
nativeToken = 0;
} else if (frame.windowBackend == Server::kHostFrameWindowBackend) {
// MOBILEGL_IPC_SURFACE=host: the frames are the DISPLAY HOST's, taken by the
// server from the host frame bridge. As for the server-owned tag - a creation
// only, and the token is 0 whatever the frame carried.
if (frame.kind != SurfaceControlOp::CreateWindowSurface) {
return SurfaceWireError::HostFrameOnSetWindowHandle;
}
windowKind = ::MobileGL::Wire::WindowKind::HostFrame;
nativeToken = 0;
} else if (!WireWindowKindForWindowBackend(
static_cast<MG_Backend::WindowBackend>(frame.windowBackend), &windowKind)) {
return SurfaceWireError::UnknownWindowKind;
@@ -213,6 +229,21 @@ namespace MobileGL::MG_Remote {
*frame = decoded;
return SurfaceWireError::None;
}
if (op.windowKind() == ::MobileGL::Wire::WindowKind::HostFrame) {
// MOBILEGL_IPC_SURFACE=host. As above: a creation only, and a token of 0
// only - anything else is a value from the peer's window system trying to
// cross (Rule G), which is corruption rather than a refusal.
if (kind != SurfaceControlOp::CreateWindowSurface) {
return SurfaceWireError::HostFrameOnSetWindowHandle;
}
if (op.nativeToken() != 0) {
return SurfaceWireError::HostFrameTokenNotZero;
}
decoded.windowBackend = Server::kHostFrameWindowBackend;
decoded.nativeToken = 0;
*frame = decoded;
return SurfaceWireError::None;
}
if (op.windowKind() == ::MobileGL::Wire::WindowKind::AndroidNativeWindow) {
// The one refusal that is not corruption: the window kind is LEGAL and the token
// is an ANativeWindow*, which names memory in the CLIENT's process. Real window
@@ -225,10 +256,10 @@ namespace MobileGL::MG_Remote {
}
MG_Backend::WindowBackend backend;
if (!WindowBackendForWireWindowKind(op.windowKind(), &backend)) {
// THE UPPER BOUND IS THE LAST APPENDED KIND (P12: ServerOwned), for the reason
// THE UPPER BOUND IS THE LAST APPENDED KIND (P12: HostFrame), for the reason
// SurfaceControlOpForWireKind's bound gives.
return ::flatbuffers::IsOutRange(op.windowKind(), ::MobileGL::Wire::WindowKind::None,
::MobileGL::Wire::WindowKind::ServerOwned)
::MobileGL::Wire::WindowKind::HostFrame)
? SurfaceWireError::UnknownWindowKind
: SurfaceWireError::WindowKindNamesNoBackend;
}
@@ -68,6 +68,9 @@ namespace MobileGL::MG_Remote {
// CreateWindowSurface only, and only with nativeToken 0:
ServerOwnedTokenNotZero, // -> Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}, latched
ServerOwnedOnSetWindowHandle, // -> a NAMED REFUSAL (reply ok=false), not a latch
// MOBILEGL_IPC_SURFACE=host, the same two rules for the other on-screen shape:
HostFrameTokenNotZero, // -> Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}, latched
HostFrameOnSetWindowHandle, // -> a NAMED REFUSAL (reply ok=false), not a latch
};
const char* SurfaceWireErrorName(SurfaceWireError error);
@@ -341,11 +341,12 @@ enum class WindowKind : uint8_t {
Pbuffer = 5,
MetalLayer = 6,
ServerOwned = 7,
HostFrame = 8,
MIN = None,
MAX = ServerOwned
MAX = HostFrame
};
inline const WindowKind (&EnumValuesWindowKind())[8] {
inline const WindowKind (&EnumValuesWindowKind())[9] {
static const WindowKind values[] = {
WindowKind::None,
WindowKind::AndroidNativeWindow,
@@ -354,13 +355,14 @@ inline const WindowKind (&EnumValuesWindowKind())[8] {
WindowKind::Surfaceless,
WindowKind::Pbuffer,
WindowKind::MetalLayer,
WindowKind::ServerOwned
WindowKind::ServerOwned,
WindowKind::HostFrame
};
return values;
}
inline const char * const *EnumNamesWindowKind() {
static const char * const names[9] = {
static const char * const names[10] = {
"None",
"AndroidNativeWindow",
"X11",
@@ -369,13 +371,14 @@ inline const char * const *EnumNamesWindowKind() {
"Pbuffer",
"MetalLayer",
"ServerOwned",
"HostFrame",
nullptr
};
return names;
}
inline const char *EnumNameWindowKind(WindowKind e) {
if (::flatbuffers::IsOutRange(e, WindowKind::None, WindowKind::ServerOwned)) return "";
if (::flatbuffers::IsOutRange(e, WindowKind::None, WindowKind::HostFrame)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesWindowKind()[index];
}
@@ -386,35 +389,38 @@ enum class SurfaceRefusal : uint8_t {
NoServerWindow = 2,
SurfaceModeMismatch = 3,
ServerOwnedOnSetWindowHandle = 4,
HostFrameOnSetWindowHandle = 5,
MIN = None,
MAX = ServerOwnedOnSetWindowHandle
MAX = HostFrameOnSetWindowHandle
};
inline const SurfaceRefusal (&EnumValuesSurfaceRefusal())[5] {
inline const SurfaceRefusal (&EnumValuesSurfaceRefusal())[6] {
static const SurfaceRefusal values[] = {
SurfaceRefusal::None,
SurfaceRefusal::NoServerDisplay,
SurfaceRefusal::NoServerWindow,
SurfaceRefusal::SurfaceModeMismatch,
SurfaceRefusal::ServerOwnedOnSetWindowHandle
SurfaceRefusal::ServerOwnedOnSetWindowHandle,
SurfaceRefusal::HostFrameOnSetWindowHandle
};
return values;
}
inline const char * const *EnumNamesSurfaceRefusal() {
static const char * const names[6] = {
static const char * const names[7] = {
"None",
"NoServerDisplay",
"NoServerWindow",
"SurfaceModeMismatch",
"ServerOwnedOnSetWindowHandle",
"HostFrameOnSetWindowHandle",
nullptr
};
return names;
}
inline const char *EnumNameSurfaceRefusal(SurfaceRefusal e) {
if (::flatbuffers::IsOutRange(e, SurfaceRefusal::None, SurfaceRefusal::ServerOwnedOnSetWindowHandle)) return "";
if (::flatbuffers::IsOutRange(e, SurfaceRefusal::None, SurfaceRefusal::HostFrameOnSetWindowHandle)) return "";
const size_t index = static_cast<size_t>(e);
return EnumNamesSurfaceRefusal()[index];
}
+7
View File
@@ -274,6 +274,12 @@ enum WindowKind : ubyte {
// SurfaceView) and has none of its own to name, so `nativeToken` is always 0 and no pointer
// crosses (Rule G/H). Legal on CreateWindowSurface only; SetWindowHandle may not name it.
ServerOwned = 7,
// P12, appended: the OTHER on-screen shape, and its mirror image. The client names no
// window here either - but the frames are the DISPLAY HOST's: it allocates them, offers
// each one over the host frame bridge, and puts them on the glass itself, while the
// server draws into them. `nativeToken` is always 0, as for ServerOwned, and it is legal
// on CreateWindowSurface only.
HostFrame = 8,
}
// P12, appended: WHY A SURFACE OP WAS DECLINED, so the client can name the refusal in its own log
@@ -285,6 +291,7 @@ enum SurfaceRefusal : ubyte {
NoServerWindow = 2, // the server owns a display, but no window came within its wait
SurfaceModeMismatch = 3, // the session's surface mode (on-screen / offscreen) is the other one
ServerOwnedOnSetWindowHandle = 4, // SetWindowHandle named the server's own window
HostFrameOnSetWindowHandle = 5, // SetWindowHandle named the display host's frames
}
table SurfaceOp {
@@ -404,6 +404,15 @@ namespace MobileGL::MG_Remote::Server {
// was wanted, recorded here so a future swap-without-present is looked for rather than
// assumed impossible. Presents() is exposed for a lane that wants to make the comparison.
table->Present();
// A HOST-FRAMED SURFACE IS PRESENTED BY TELLING THE HOST, not by the swap above (which
// swaps the pbuffer the context was made with). BackendObject::SwapEGLBuffers is the
// only other route to PresentHostFrame and a split client never reaches it, so this is
// where the display host learns the frame is drawn - before the credit goes back, so a
// client released by it is released by a frame that is on its way to the glass.
if (m_backend != nullptr) {
if (!m_backend->PresentHostFrameIfCurrent())
MGLOG_E_ONCE("OnPresent: the host frame was not presented (PresentHostFrame failed; see above)");
}
++m_presents;
// FrameSerial 0 means "the server stamps its own" (c1-v1 8.3): P5 has no client-side
// present credit, so the client sends 0 and the frame count on this side IS the serial.
+69
View File
@@ -287,6 +287,7 @@ namespace MobileGL::MG_Remote::Server {
m_surfaceMode = SessionSurfaceMode::None;
m_holdsWindowLease = false;
m_serverOwnedSurfaces.clear();
m_hostFrameSurfaces.clear();
m_windowLostRequested.store(false, std::memory_order_release);
m_serverWindowsLost.store(0, std::memory_order_release);
m_testLogFloodLines = ApplyLogFloodLinesForTest();
@@ -1338,6 +1339,8 @@ namespace MobileGL::MG_Remote::Server {
// P12 (D3): the SERVER's window, asked for by a headless client. Before the unpack,
// which would refuse the frame-local tag as a WindowBackend out of range.
if (frame.windowBackend == kServerOwnedWindowBackend) return ApplyServerOwnedWindowSurface(backend, frame);
// The mirror image: the frames are the display host's, and it presents them.
if (frame.windowBackend == kHostFrameWindowBackend) return ApplyHostFrameWindowSurface(backend, frame);
MG_Backend::WindowHandle window;
if (!UnpackWindowHandle(frame, &window)) {
frame.ok = false;
@@ -1445,6 +1448,7 @@ namespace MobileGL::MG_Remote::Server {
const EGLSurface surface = HandleFromToken<EGLSurface>(frame.surface);
backend->ReleaseEGLSurface(surface);
ForgetServerOwnedSurface(surface);
ForgetHostFrameSurface(surface);
// N-3: a released surface the held tuple names may have taken the context with it
// (BackendObject::ReleaseEGLSurface -> OnEGLSurfaceReleased -> DestroyEGLContext once
// nothing holds it current). Forgetting when the base class only DEFERRED the destroy
@@ -1658,6 +1662,52 @@ namespace MobileGL::MG_Remote::Server {
return MOBILEGL_OK;
}
MobileGLResult ServerLoop::ApplyHostFrameWindowSurface(MG_Backend::BackendObject* backend,
SurfaceControlFrame& frame) {
frame.ok = false;
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::None);
const Uint32 wantWidth = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
const Uint32 wantHeight = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
// Both on-screen shapes are the same session mode: one rendering path per session, and
// this is the on-screen one whichever side owns the window.
if (!SessionSurfaceModeAdmits(m_surfaceMode, /*serverOwnedWindow=*/true)) {
frame.refusal = static_cast<Uint8>(SurfaceRefusalCode::SurfaceModeMismatch);
MGLOG_E("MG_Remote server: SurfaceModeMismatch - a HostFrame CreateWindowSurface (seq %llu, "
"%ux%u) in a session whose surface mode is %s. Refused, the session is not latched",
static_cast<unsigned long long>(frame.seq), wantWidth, wantHeight,
SessionSurfaceModeName(m_surfaceMode));
return MOBILEGL_ERR_INVALID_ARGUMENT;
}
const EGLSurface surface = HandleFromToken<EGLSurface>(frame.surface);
// THE SUBSTITUTION, the other way round from ServerOwned: nothing of the client's and
// nothing of the server's goes into the backend call - the backend takes the frames from
// the display host, and every swap hands the next one back to it.
frame.ok = backend->CreateEGLHostFrameSurface(surface, wantWidth, wantHeight);
if (!frame.ok) {
MGLOG_E("MG_Remote server: the backend could not take the display host frames (seq %llu, %ux%u)",
static_cast<unsigned long long>(frame.seq), wantWidth, wantHeight);
return MOBILEGL_OK;
}
ForgetCurrentTuple();
m_surfaceMode = SessionSurfaceMode::OnScreen;
if (!IsHostFrameSurface(surface)) m_hostFrameSurfaces.push_back(surface);
frame.width = static_cast<Int>(wantWidth);
frame.height = static_cast<Int>(wantHeight);
MGLOG_I("MG_Remote server: surface=window %ux%u owner=host (HostFrame seq %llu)", wantWidth,
wantHeight, static_cast<unsigned long long>(frame.seq));
return MOBILEGL_OK;
}
Bool ServerLoop::IsHostFrameSurface(EGLSurface surface) const {
return std::find(m_hostFrameSurfaces.begin(), m_hostFrameSurfaces.end(), surface) !=
m_hostFrameSurfaces.end();
}
void ServerLoop::ForgetHostFrameSurface(EGLSurface surface) {
m_hostFrameSurfaces.erase(std::remove(m_hostFrameSurfaces.begin(), m_hostFrameSurfaces.end(), surface),
m_hostFrameSurfaces.end());
}
Bool ServerLoop::IsServerOwnedSurface(EGLSurface surface) const {
return std::find(m_serverOwnedSurfaces.begin(), m_serverOwnedSurfaces.end(), surface) !=
m_serverOwnedSurfaces.end();
@@ -1770,6 +1820,25 @@ namespace MobileGL::MG_Remote::Server {
return rc == MOBILEGL_OK && frame.ok;
}
ServerOwnedWindowReply ServerCreateHostFrameWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
// One frame, and no SetWindowHandle in front of it: the client names no window, and this
// side does not make one - the frames are the display host's, taken from the bridge.
SurfaceControlFrame frame;
frame.kind = SurfaceControlOp::CreateWindowSurface;
frame.surface = TokenFromHandle(surface);
frame.windowBackend = kHostFrameWindowBackend;
frame.nativeToken = 0;
frame.width = static_cast<Int>(width);
frame.height = static_cast<Int>(height);
const MobileGLResult rc = ServerLoopInstance().RunSurfaceControlFrame(frame);
ServerOwnedWindowReply reply;
reply.transport = rc;
reply.ok = rc == MOBILEGL_OK && frame.ok;
reply.refusal = static_cast<SurfaceRefusalCode>(frame.refusal);
reply.width = frame.width > 0 ? static_cast<Uint32>(frame.width) : 0u;
reply.height = frame.height > 0 ? static_cast<Uint32>(frame.height) : 0u;
return reply;
}
ServerOwnedWindowReply ServerCreateServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
// P12 (D1/D2): ONE frame, and no SetWindowHandle in front of it - the window is the
// server's, so there is nothing of the client's to name. The frame-local tag is what the
+11
View File
@@ -459,6 +459,9 @@ namespace MobileGL::MG_Remote::Server {
// P12. The ServerOwned arm of the CreateWindowSurface dispatch (D3, D4).
MobileGLResult ApplyServerOwnedWindowSurface(MG_Backend::BackendObject* backend, SurfaceControlFrame& frame);
MobileGLResult ApplyHostFrameWindowSurface(MG_Backend::BackendObject* backend, SurfaceControlFrame& frame);
Bool IsHostFrameSurface(EGLSurface surface) const;
void ForgetHostFrameSurface(EGLSurface surface);
// P12 (D6), apply thread: the lost window's backend surface goes, the lease ends, and the
// session latches ServerWindowLost. Runs from PumpControlRequest when Detach asked.
void ReleaseLostServerWindow();
@@ -471,6 +474,10 @@ namespace MobileGL::MG_Remote::Server {
void ForgetServerOwnedSurface(EGLSurface surface);
// Apply thread only: this session's surfaces created on the server's window; reset by Start().
std::vector<EGLSurface> m_serverOwnedSurfaces;
// The surfaces whose drawing target is a frame the display host owns. Kept apart from
// the server-owned list because the two are different paths with different owners,
// and a resize or a release has to reach the right one.
std::vector<EGLSurface> m_hostFrameSurfaces;
// ServerDisplay's lost hook: called under the display's lock from Detach's thread. Sets the
// request and rings the apply thread's bell; never blocks.
static void ServerWindowLostThunk(void* self);
@@ -659,6 +666,10 @@ namespace MobileGL::MG_Remote::Server {
Uint32 height = 0;
};
ServerOwnedWindowReply ServerCreateServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
// The host-framed on-screen shape (MOBILEGL_IPC_SURFACE=host): the client names no window
// either, but the frames come from the display host over the host frame bridge, and the
// host presents them. The reply carries the size the surface was made at.
ServerOwnedWindowReply ServerCreateHostFrameWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
// P12 review fix: ResizeWindowSurface for a surface created on the server's window. The server
// resizes its WINDOW (a geometry request) and replies with the window's real extent.
ServerOwnedWindowReply ServerResizeServerOwnedWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
+67 -11
View File
@@ -1035,12 +1035,32 @@ private:
};
bool Reap() {
const bool reaped = m_handoffShape == Handoff::Thread ? ReapSessionThread()
: ReapActiveSession(m_active, m_sessionsFaulted);
if (m_active <= 0 && m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; }
if (m_handoffShape == Handoff::Thread) {
const bool reaped = ReapSessionThread();
if (m_active <= 0 && m_handoff >= 0) { ::close(m_handoff); m_handoff = -1; }
return reaped;
}
// The fork shape: every live child is asked, and a reaped one takes its hand-off with it.
bool reaped = false;
for (auto child = m_children.begin(); child != m_children.end();) {
if (ReapActiveSession(child->pid, m_sessionsFaulted)) {
if (child->handoff >= 0) ::close(child->handoff);
child = m_children.erase(child);
reaped = true;
} else {
++child;
}
}
m_active = m_children.empty() ? -1 : m_children.back().pid;
return reaped;
}
// Whether an authenticated Hello has to be told Busy: the thread shape serves one session, the
// fork shape up to kMaxForkedSessions side by side.
bool SessionsFull() const {
return m_handoffShape == Handoff::Thread ? m_active > 0 : m_children.size() >= kMaxForkedSessions;
}
// P12 (D5): the thread shape's reap - the same line a forked child's reap prints, and the same
// count, then the latch put back for the next session (ResetSessionLatch: the thread took no
// process with it, so the latch it armed and maybe latched is still this process's).
@@ -1082,7 +1102,7 @@ private:
// make the loop wait here; a DataBind's routing takes one look (Judge, fix round).
void WaitOutAnExitingSession() {
const auto reapDeadline = Clock::now() + std::chrono::milliseconds(50);
while (m_active > 0 && Clock::now() < reapDeadline) {
while (SessionsFull() && Clock::now() < reapDeadline) {
if (Reap()) break;
::usleep(1000);
}
@@ -1176,7 +1196,15 @@ private:
// either has cut its hand-off already (the send below fails and is refused by name) or
// refuses what is queued on its way out (ExitBeforeAccept / CloseHandoffRefusingQueued).
Reap();
if (m_active <= 0) {
// WHICH SESSION IT BELONGS TO. The thread shape has one; the fork shape knows every
// child's nonce (it minted them, Fork) and hands the connection to the child it names.
int handoff = m_active > 0 ? m_handoff : -1;
if (m_handoffShape == Handoff::Fork) {
handoff = -1;
for (const auto& child : m_children)
if (ConstantTimeNonceMatch(child.nonce, nonce)) handoff = child.handoff;
}
if (handoff < 0) {
// StreamLink's frames may already follow the DataBind; read them away so the close
// does not become an RST that discards the refusal.
DrainUnread(fd);
@@ -1186,7 +1214,7 @@ private:
// The send is MSG_DONTWAIT and the child cuts its end the moment its own data
// connection is bound (CloseHandoffRefusingQueued), so this either queues the
// descriptor for a child that will read or refuse it, or fails and is refused here.
if (m_handoff >= 0 && FdPassing::SendFd(m_handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)},
if (FdPassing::SendFd(handoff, fd, MobileGLByteSpan{nonce, sizeof(nonce)},
/*dontWait=*/true) == MOBILEGL_OK) {
// The child holds its own descriptor now; ours goes without a shutdown(2), which
// would disconnect the child's too.
@@ -1217,9 +1245,11 @@ private:
}
m_backoff.NoteSuccess(peer.address);
WaitOutAnExitingSession();
if (m_active > 0) {
if (SessionsFull()) {
// The frame is consumed, so closing cannot RST the refusal away.
Refuse(*connection, Protocol::RefuseCode::Busy, "one session is already active");
Refuse(*connection, Protocol::RefuseCode::Busy,
m_handoffShape == Handoff::Thread ? "one session is already active"
: "the session limit is reached");
return;
}
Fork(std::move(connection), payload);
@@ -1235,12 +1265,25 @@ private:
StartSessionThread(std::move(connection), hello, pair);
return;
}
// The child's data nonce, minted HERE so this supervisor can route the child's DataBind to it
// among other live sessions; the child adopts it instead of minting its own.
LiveChild live;
if (SocketTransport::MintNonce(live.nonce, sizeof(live.nonce)) != MOBILEGL_OK) {
::close(pair[0]);
::close(pair[1]);
Refuse(*connection, Protocol::RefuseCode::Busy, "could not mint the session's data nonce");
return;
}
std::fflush(nullptr);
const pid_t supervisor = ::getpid();
const pid_t child = ::fork();
if (child == 0) {
// P12 (D8): the child dies with this supervisor, not after its client has gone.
DieWithSupervisor(supervisor);
Server::PresetDataNonceForThisProcess(live.nonce, sizeof(live.nonce));
// The other live sessions' hand-offs are the supervisor's.
for (const auto& other : m_children)
if (other.handoff >= 0) ::close(other.handoff);
// The child owns this one connection and its end of the hand-off. Every OTHER pending
// peer's descriptor is closed here, or the supervisor's refusal of that peer would not
// end its connection while this session lives.
@@ -1260,9 +1303,11 @@ private:
Refuse(*connection, Protocol::RefuseCode::Busy, "could not create session child");
return;
}
live.pid = child;
live.handoff = pair[0];
m_children.push_back(live);
m_active = child;
::close(pair[1]);
m_handoff = pair[0];
connection->CloseLocalCopy();
}
@@ -1310,10 +1355,21 @@ private:
std::atomic<bool> m_sessionDone{false};
std::atomic<int> m_sessionExit{0};
unsigned long m_sessionOrdinal = 0;
// A live session: the newest child's pid for the fork shape, this process's for the thread shape.
pid_t m_active = -1;
// The supervisor's end of the live child's data-connection hand-off (PH-7 (4)); -1 while no
// session is live. It goes with the child: a reaped session can be handed nothing.
// The thread shape's end of its session's data-connection hand-off (PH-7 (4)); -1 while no
// session is live. It goes with the session: a reaped session can be handed nothing.
int m_handoff = -1;
// THE FORK SHAPE'S LIVE SESSIONS, side by side: a desktop is a compositor AND the GL programs
// run inside it, each its own client with its own session. Each child keeps its end of the
// hand-off and the data nonce this supervisor minted for it, which is how a DataBind finds it.
struct LiveChild {
pid_t pid = -1;
int handoff = -1;
std::uint8_t nonce[kDataNonceBytes] = {};
};
static constexpr std::size_t kMaxForkedSessions = 8;
std::vector<LiveChild> m_children;
std::vector<PendingPeer> m_pending;
std::vector<PendingPeer> m_ready;
// Accepting resumes at this time after accept(2) ran out of descriptors or memory.
+21 -1
View File
@@ -42,6 +42,24 @@
#endif
namespace MobileGL::MG_Remote::Server {
namespace {
Uint8 g_presetDataNonce[Transport::kDataNonceBytes] = {};
Bool g_presetDataNonceSet = false;
Bool TakePresetDataNonce(Uint8* out, SizeT size) {
if (!g_presetDataNonceSet || size != sizeof(g_presetDataNonce)) return false;
std::memcpy(out, g_presetDataNonce, size);
g_presetDataNonceSet = false;
return true;
}
} // namespace
void PresetDataNonceForThisProcess(const Uint8* nonce, SizeT size) {
if (nonce == nullptr || size != sizeof(g_presetDataNonce)) return;
std::memcpy(g_presetDataNonce, nonce, size);
g_presetDataNonceSet = true;
}
namespace {
// F1 (P7 wave 2). THE ONLY KNOB THAT CAN MAKE THE FIRST CapsSnapshot LATE.
@@ -716,7 +734,9 @@ namespace MobileGL::MG_Remote::Server {
if (socket == nullptr || !socket->IsTcp() || !m_dataSource)
return RefuseHandshake(transport, ::MobileGL::Wire::RefuseCode::LinkTerms,
"stream needs a TCP control connection and a data-connection source");
created = Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce));
created = TakePresetDataNonce(dataNonce, sizeof(dataNonce))
? MOBILEGL_OK
: Transport::SocketTransport::MintNonce(dataNonce, sizeof(dataNonce));
if (created != MOBILEGL_OK) {
// (F fix round) The peer is told by name instead of seeing a bare close. The
// return value stays the fault it is - exit 67 in ServerMain, counted in
@@ -63,6 +63,13 @@
namespace MobileGL::MG_Remote::Server {
// THE DATA NONCE A FORKED SESSION CHILD IS BORN WITH. The TCP supervisor serves sessions side by
// side and routes each data connection (DataBind) to the child whose nonce it presents, so it has
// to know every child's nonce: it mints the nonce itself just before fork() and the child adopts
// it here instead of minting its own. One use: the session that takes it clears it. Never set in
// a process that runs more than one session, so the shared storage is the child's alone.
void PresetDataNonceForThisProcess(const Uint8* nonce, SizeT size);
class ServerSession {
public:
static ServerSession* Active();
@@ -60,6 +60,7 @@ namespace MobileGL::MG_Remote::Server {
case SurfaceRefusalCode::NoServerWindow: return "NoServerWindow";
case SurfaceRefusalCode::SurfaceModeMismatch: return "SurfaceModeMismatch";
case SurfaceRefusalCode::ServerOwnedOnSetWindowHandle: return "ServerOwnedOnSetWindowHandle";
case SurfaceRefusalCode::HostFrameOnSetWindowHandle: return "HostFrameOnSetWindowHandle";
}
return "<unknown SurfaceRefusalCode>";
}
@@ -87,6 +87,14 @@ namespace MobileGL::MG_Remote::Server {
// substitutes its own window there; no pointer crosses the wire (Rule G/H).
inline constexpr Int kServerOwnedWindowBackend = 0x10000;
// The OTHER on-screen shape, and its mirror image. With MOBILEGL_IPC_SURFACE=server the
// client has no window and the SERVER's own goes into the backend call; with =host the
// client still has no window, but the frames are the display host's - it allocates them,
// offers each one over the host frame bridge, and puts them on the glass itself. Same
// kind of frame-local tag as the one above, and for the same reason: it names no
// WindowBackend of MG_Backend's enum, which is in the pull build (gate G1).
inline constexpr Int kHostFrameWindowBackend = 0x10001;
// P12. Why the server declined a surface op, carried back in the reply half (`refusal`). The
// values ARE the wire's SurfaceRefusal (protocol.fbs); SurfaceOpCodec pins the agreement with
// static_asserts, the way it pins SurfaceControlOp against SurfaceOpKind. None of them latches
@@ -97,6 +105,7 @@ namespace MobileGL::MG_Remote::Server {
NoServerWindow = 2, // a display, but no window within the wait
SurfaceModeMismatch = 3, // D4: the session's surface mode is the other one
ServerOwnedOnSetWindowHandle = 4, // SetWindowHandle named the server's own window
HostFrameOnSetWindowHandle = 5, // SetWindowHandle named the display host's frames
};
const char* SurfaceRefusalCodeName(SurfaceRefusalCode code);
@@ -46,6 +46,13 @@ namespace MobileGL::MG_Remote::Transport {
class ITransport {
public:
virtual ~ITransport() = default;
// THE AUX DESCRIPTOR CHANNEL, IF THIS TRANSPORT HAS ONE. SCM_RIGHTS needs a socket of its own
// (SocketTransport.h:77), and a caller that wants to hand a dma-buf to the server has to name it:
// the session's transport is held as an ITransport, so the seam has to answer here rather than in a
// downcast. -1 means "this transport carries no descriptors", which is a legal state a caller must
// degrade on - the texture then simply has no storage rather than the process failing.
virtual int AuxFd() const { return -1; }
ITransport(const ITransport&) = delete;
ITransport& operator=(const ITransport&) = delete;
@@ -180,7 +180,7 @@ namespace MobileGL::MG_Remote::Transport {
// For `sm`'s process discipline and for the arm-proof gate: the fds this
// transport owns, so a test can assert the child inherited exactly these.
int StreamFd() const { return m_streamFd; }
int AuxFd() const { return m_auxFd; }
int AuxFd() const override { return m_auxFd; }
bool IsTcp() const { return m_tcp; }
// TCP's second connection belongs exclusively to the data plane.
// Taking it disables all descriptor passing on this transport.
+253 -5
View File
@@ -8,6 +8,15 @@
#include "Core.h"
#include <EGL/eglext.h>
// An imported buffer is held by descriptors, and the copy an image takes of one is the platform's
// own dup()/close(): unistd.h's where the import exists, io.h's on the Windows build that compiles
// this file but never sees EGL_LINUX_DMA_BUF_EXT.
#if defined(_WIN32)
#include <io.h>
#else
#include <unistd.h>
#endif
#include <cerrno>
namespace MobileGL {
namespace MG_State {
@@ -30,6 +39,54 @@ namespace MobileGL {
return Nullopt;
}
// Taking and closing the descriptor an image owns, per platform. Named rather
// than spelled inline so the ownership rule reads the same on both.
#if defined(_WIN32)
Int DuplicateImageDescriptor(Int fd) { return _dup(fd); }
void CloseImageDescriptor(Int fd) { _close(fd); }
#else
Int DuplicateImageDescriptor(Int fd) { return ::dup(fd); }
void CloseImageDescriptor(Int fd) { ::close(fd); }
#endif
// ONE PLANE'S ATTRIBUTE SPELLINGS, in plane order. Plane 0's fd/offset/pitch triple
// is EGL_EXT_image_dma_buf_import's; plane 3's triple and every MODIFIER_LO/HI pair
// belong to EGL_EXT_image_dma_buf_import_modifiers.
struct DmaBufPlaneAttributes {
EGLint Fd;
EGLint Offset;
EGLint Pitch;
EGLint ModifierLo;
EGLint ModifierHi;
};
constexpr DmaBufPlaneAttributes kDmaBufPlaneAttributes[EGLContext::kMaxImagePlanes] = {
{EGL_DMA_BUF_PLANE0_FD_EXT, EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE0_PITCH_EXT,
EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT},
{EGL_DMA_BUF_PLANE1_FD_EXT, EGL_DMA_BUF_PLANE1_OFFSET_EXT, EGL_DMA_BUF_PLANE1_PITCH_EXT,
EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT},
{EGL_DMA_BUF_PLANE2_FD_EXT, EGL_DMA_BUF_PLANE2_OFFSET_EXT, EGL_DMA_BUF_PLANE2_PITCH_EXT,
EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT},
{EGL_DMA_BUF_PLANE3_FD_EXT, EGL_DMA_BUF_PLANE3_OFFSET_EXT, EGL_DMA_BUF_PLANE3_PITCH_EXT,
EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT, EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT},
};
// The EGL spelling of one plane attribute, for the single line a refused image gets:
// the attribute list is gone by the time the caller reads eglGetError.
String DmaBufPlaneAttributeName(Int plane, const char* role) {
return std::format("EGL_DMA_BUF_PLANE{}_{}", plane, role);
}
// Whether the caller mentioned this plane at all, so that a description with a hole
// in it can be refused by name instead of silently losing the planes past the hole.
Bool HasAnyPlaneAttribute(const EGLAttrib* attribList, Int plane) {
const DmaBufPlaneAttributes& attributes = kDmaBufPlaneAttributes[plane];
return ParseAttribValue(attribList, attributes.Fd) || ParseAttribValue(attribList, attributes.Offset) ||
ParseAttribValue(attribList, attributes.Pitch) ||
ParseAttribValue(attribList, attributes.ModifierLo) ||
ParseAttribValue(attribList, attributes.ModifierHi);
}
EGLint QueryDefaultX11VisualId() {
#if defined(__linux__) && !defined(__ANDROID__)
const char* displayName = std::getenv("DISPLAY");
@@ -292,6 +349,9 @@ namespace MobileGL {
for (auto imageIt = m_images.begin(); imageIt != m_images.end();) {
if (imageIt->second.Display == display) {
// The display is going away and takes its images with it, so the descriptors
// they own go too: an image erased without this leaks an fd per plane.
ReleaseImageDescriptors(imageIt->second);
imageIt = m_images.erase(imageIt);
} else {
++imageIt;
@@ -334,6 +394,16 @@ namespace MobileGL {
return displayObject && displayObject->Initialized;
}
Bool EGLContext::GetDisplayNative(EGLDisplayHandle display, Uint64* nativeDisplayKey,
EGLenum* platform) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* displayObject = TryGetDisplay(display);
if (!displayObject) return false;
*nativeDisplayKey = displayObject->NativeDisplayKey;
*platform = displayObject->Platform;
return true;
}
Bool EGLContext::InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto* displayObject = TryGetDisplay(display);
@@ -417,6 +487,12 @@ namespace MobileGL {
for (SizeT i = 0; attribList[i] != EGL_NONE; i += 2) {
const EGLint attr = attribList[i];
const EGLint requested = attribList[i + 1];
// WHAT DIES HERE, exactly: with an attribute list present this loop is where
// eglChooseConfig faults (a NULL list survives it and answers two configs),
// and the fault is one instruction inside the attribute switch. These two
// lines name the attribute and the config before either is dereferenced.
MGLOG_I("chooseConfig: attr=0x%04x requested=%d config=%p", attr, requested,
static_cast<const void*>(cfg));
if (requested == EGL_DONT_CARE) {
continue;
}
@@ -625,7 +701,13 @@ namespace MobileGL {
SetError(EGL_NOT_INITIALIZED);
return nullptr;
}
if (!ValidateConfigOnDisplay(display, config)) {
// EGL_KHR_no_config_context: a context with NO config of its own. It is the shape a
// compositor asks for - its contexts are used with whatever framebuffer the frame
// needs, so pinning one config to the context would be a claim about the future the
// caller does not have - and EGL spells the request as this null handle. A context
// that does name a config is validated exactly as before.
const Bool noConfigContext = (config == nullptr);
if (!noConfigContext && !ValidateConfigOnDisplay(display, config)) {
SetError(EGL_BAD_CONFIG);
return nullptr;
}
@@ -1091,6 +1173,18 @@ namespace MobileGL {
return surface;
}
// MOBILEGL_IPC_SURFACE=host: the registration is the server-owned one verbatim, because
// what makes a window surface host-framed is not its EGL state but where its pixels go -
// and that is decided one layer down, by the backend the creation calls next.
EGLContext::EGLSurfaceHandle EGLContext::CreateHostFrameWindowSurface(EGLDisplayHandle display,
EGLConfigHandle config,
const void* nativeWindow,
EGLint width, EGLint height,
Bool platformWindow) {
return CreateServerOwnedWindowSurface(display, config, nativeWindow, width, height,
platformWindow);
}
Bool EGLContext::SetSurfaceExtent(EGLSurfaceHandle surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
auto surfaceIt = m_surfaces.find(surface);
@@ -1238,7 +1332,13 @@ namespace MobileGL {
SetError(EGL_BAD_CONTEXT);
return false;
}
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE) {
// EGL_KHR_surfaceless_context: BOTH surfaces absent is a context that draws into
// framebuffers it binds itself, which is how a compositor draws the frames the
// display host handed it. One absent and one present stays refused - that is a
// pair EGL gives no meaning to, and accepting it would record a draw target that
// does not exist.
const Bool surfaceless = (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE);
if (!surfaceless && (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE)) {
SetError(EGL_BAD_MATCH);
return false;
}
@@ -1442,11 +1542,20 @@ namespace MobileGL {
return true;
}
void EGLContext::ReleaseImageDescriptors(ImageObject& imageObject) {
for (Int plane = 0; plane < imageObject.PlaneCount; ++plane) {
if (imageObject.Planes[plane].Fd >= 0) {
CloseImageDescriptor(imageObject.Planes[plane].Fd);
imageObject.Planes[plane].Fd = -1;
}
}
imageObject.PlaneCount = 0;
}
EGLContext::EGLImageHandle EGLContext::CreateImage(EGLDisplayHandle display, EGLContextHandle context,
EGLenum target, EGLClientBuffer buffer,
const EGLAttrib* attribList) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
(void)attribList;
if (!IsDisplayInitialized(display)) {
SetError(EGL_NOT_INITIALIZED);
return EGL_NO_IMAGE;
@@ -1456,13 +1565,149 @@ namespace MobileGL {
return EGL_NO_IMAGE;
}
const auto image = EncodeHandle<EGLImageHandle>(m_nextImageHandle++);
m_images[image] = ImageObject{
ImageObject imageObject = {
.Display = display,
.Context = context,
.Target = target,
.Buffer = buffer,
};
// EGL_LINUX_DMA_BUF_EXT: the caller handed over a DESCRIPTOR and described the
// memory behind it, and that description is the whole image - there is no handle to
// import later. Every other target still names a buffer this state cannot see, so
// it stays what it was: the record of the target it was made for.
if (target == EGL_LINUX_DMA_BUF_EXT) {
// ONE line per refusal, naming what was missing: a caller that gets
// EGL_BAD_PARAMETER for a buffer it believes it described completely has
// nowhere else to look - the attribute list is gone by the time eglGetError is
// read, and the frame it wanted to draw into is the frame it cannot.
const auto refuse = [&](const String& missing) {
SetError(EGL_BAD_PARAMETER);
MGLOG_E("eglCreateImage(EGL_LINUX_DMA_BUF_EXT): %s is missing; refusing the image "
"(EGL_BAD_PARAMETER)",
missing.c_str());
};
const auto width = ParseAttribValue(attribList, EGL_WIDTH);
const auto height = ParseAttribValue(attribList, EGL_HEIGHT);
const auto fourCC = ParseAttribValue(attribList, EGL_LINUX_DRM_FOURCC_EXT);
if (!width) {
refuse("EGL_WIDTH");
return EGL_NO_IMAGE;
}
if (!height) {
refuse("EGL_HEIGHT");
return EGL_NO_IMAGE;
}
if (!fourCC) {
refuse("EGL_LINUX_DRM_FOURCC_EXT");
return EGL_NO_IMAGE;
}
// The planes, read and checked BEFORE one descriptor is duplicated, so that a
// refusal leaves nothing behind to close.
EGLint planeFd[EGLContext::kMaxImagePlanes] = {};
EGLint planeOffset[EGLContext::kMaxImagePlanes] = {};
EGLint planePitch[EGLContext::kMaxImagePlanes] = {};
Uint64 planeModifier[EGLContext::kMaxImagePlanes] = {};
Bool planeHasModifier[EGLContext::kMaxImagePlanes] = {};
EGLint planeCount = 0;
for (Int plane = 0; plane < EGLContext::kMaxImagePlanes; ++plane) {
const DmaBufPlaneAttributes& attributes = kDmaBufPlaneAttributes[plane];
const auto fd = ParseAttribValue(attribList, attributes.Fd);
const auto offset = ParseAttribValue(attribList, attributes.Offset);
const auto pitch = ParseAttribValue(attribList, attributes.Pitch);
const auto modifierLo = ParseAttribValue(attribList, attributes.ModifierLo);
const auto modifierHi = ParseAttribValue(attribList, attributes.ModifierHi);
if (!fd && !offset && !pitch && !modifierLo && !modifierHi) {
// The caller never mentioned this plane. Planes are positional - the
// description carries no plane index - so this ends the list, and a
// plane past the hole is refused by name rather than dropped.
for (Int higher = plane + 1; higher < EGLContext::kMaxImagePlanes; ++higher) {
if (HasAnyPlaneAttribute(attribList, higher)) {
refuse(DmaBufPlaneAttributeName(plane, "FD_EXT"));
return EGL_NO_IMAGE;
}
}
break;
}
// All three of fd/offset/pitch or none, and both halves of the modifier or
// neither: a partial triple describes memory no importer could read.
if (!fd) {
refuse(DmaBufPlaneAttributeName(plane, "FD_EXT"));
return EGL_NO_IMAGE;
}
if (!offset) {
refuse(DmaBufPlaneAttributeName(plane, "OFFSET_EXT"));
return EGL_NO_IMAGE;
}
if (!pitch) {
refuse(DmaBufPlaneAttributeName(plane, "PITCH_EXT"));
return EGL_NO_IMAGE;
}
if (modifierLo.has_value() != modifierHi.has_value()) {
refuse(DmaBufPlaneAttributeName(plane, modifierLo ? "MODIFIER_HI_EXT" : "MODIFIER_LO_EXT"));
return EGL_NO_IMAGE;
}
planeFd[plane] = static_cast<EGLint>(*fd);
planeOffset[plane] = static_cast<EGLint>(*offset);
planePitch[plane] = static_cast<EGLint>(*pitch);
if (modifierLo) {
// The modifier arrives in two halves and is one number: DRM cuts a
// DRM_FORMAT_MOD_* where EGL takes an EGLint.
planeModifier[plane] = (static_cast<Uint64>(static_cast<Uint32>(*modifierHi)) << 32) |
static_cast<Uint32>(*modifierLo);
planeHasModifier[plane] = true;
}
planeCount = plane + 1;
}
if (planeCount == 0) {
// A dma-buf image with no plane at all: the description starts here, so
// this is the attribute that is missing.
refuse(DmaBufPlaneAttributeName(0, "FD_EXT"));
return EGL_NO_IMAGE;
}
// THE IMAGE TAKES ITS OWN DESCRIPTORS. The caller's fds stay the caller's -
// kwin hands over a dup() of its DmaBufAttributes and closes it as soon as
// eglCreateImage returns, and an image that had borrowed it would be holding a
// number some other part of the process now owns.
for (Int plane = 0; plane < planeCount; ++plane) {
const Int ownedFd = DuplicateImageDescriptor(planeFd[plane]);
if (ownedFd < 0) {
SetError(EGL_BAD_ALLOC);
MGLOG_E("eglCreateImage(EGL_LINUX_DMA_BUF_EXT): cannot take a copy of %s=%d (%s); "
"refusing the image (EGL_BAD_ALLOC)",
DmaBufPlaneAttributeName(plane, "FD_EXT").c_str(), planeFd[plane],
std::strerror(errno));
ReleaseImageDescriptors(imageObject);
return EGL_NO_IMAGE;
}
imageObject.Planes[plane] = ImagePlane{
.Fd = ownedFd,
.Offset = planeOffset[plane],
.Pitch = planePitch[plane],
.Modifier = planeModifier[plane],
.HasModifier = planeHasModifier[plane],
};
// Counted as they are stored, so that a failure below releases exactly what
// has been taken.
imageObject.PlaneCount = plane + 1;
}
imageObject.Width = static_cast<EGLint>(*width);
imageObject.Height = static_cast<EGLint>(*height);
imageObject.FourCC = static_cast<EGLint>(*fourCC);
MGLOG_D("eglCreateImage(EGL_LINUX_DMA_BUF_EXT): %dx%d fourcc=0x%08x planes=%d plane0(fd=%d "
"offset=%d pitch=%d %s0x%016llx)",
imageObject.Width, imageObject.Height, imageObject.FourCC, imageObject.PlaneCount,
imageObject.Planes[0].Fd, imageObject.Planes[0].Offset, imageObject.Planes[0].Pitch,
imageObject.Planes[0].HasModifier ? "modifier=" : "no-modifier=",
static_cast<unsigned long long>(imageObject.Planes[0].Modifier));
}
const auto image = EncodeHandle<EGLImageHandle>(m_nextImageHandle++);
m_images[image] = imageObject;
return image;
}
@@ -1477,6 +1722,9 @@ namespace MobileGL {
SetError(EGL_BAD_MATCH);
return false;
}
// The descriptors this image owns die with it. Nothing else in this process holds
// them: they are copies, and the caller's originals were never touched.
ReleaseImageDescriptors(imageIt->second);
m_images.erase(imageIt);
return true;
}
+98
View File
@@ -22,6 +22,12 @@ namespace MobileGL {
using EGLSyncHandle = ::EGLSync;
using EGLImageHandle = ::EGLImage;
// How many planes EGL describes an imported buffer with: EGL_DMA_BUF_PLANE0_* comes
// from EGL_EXT_image_dma_buf_import, EGL_DMA_BUF_PLANE3_* and every MODIFIER pair
// from EGL_EXT_image_dma_buf_import_modifiers. The reader in Core.cpp and the image
// object below have to agree on the count, so it is named once.
static constexpr Int kMaxImagePlanes = 4;
EGLContext() = default;
// Error
@@ -37,6 +43,9 @@ namespace MobileGL {
EGLDisplayHandle GetPlatformDisplay(EGLenum platform, void* nativeDisplay);
Bool ValidateDisplay(EGLDisplayHandle display) const;
Bool IsDisplayInitialized(EGLDisplayHandle display) const;
// The native display a display was made for and the platform it was asked for on
// (EGL_NONE for eglGetDisplay): what decides how its window surfaces are presented.
Bool GetDisplayNative(EGLDisplayHandle display, Uint64* nativeDisplayKey, EGLenum* platform) const;
Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor);
Bool TerminateDisplay(EGLDisplayHandle display);
// Whole-library idle checks used by EGLImpl::Terminate to decide
@@ -97,6 +106,13 @@ namespace MobileGL {
EGLSurfaceHandle CreateServerOwnedWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
const void* nativeWindow, EGLint width,
EGLint height, Bool platformWindow);
// MOBILEGL_IPC_SURFACE=host: the same registration under the other on-screen name.
// The EGL state does not care which side owns the frames - that is the backend's
// business - but a trace has to be able to tell the two runs apart, and the two
// paths part company in the backend call that follows.
EGLSurfaceHandle CreateHostFrameWindowSurface(EGLDisplayHandle display, EGLConfigHandle config,
const void* nativeWindow, EGLint width,
EGLint height, Bool platformWindow);
// P12: the server's REAL extent of a surface - a server-owned window's geometry, from
// the CreateWindowSurface reply and from every surface-changed event after it - so
// eglQuerySurface(EGL_WIDTH/EGL_HEIGHT) answers what the server renders at. Unlike
@@ -111,6 +127,57 @@ namespace MobileGL {
EGLContextHandle GetCurrentContext() const;
EGLDisplayHandle GetCurrentDisplay() const;
EGLSurfaceHandle GetCurrentSurface(EGLint readdraw) const;
// WHAT A HANDLE NAMES, for the GL side of GL_OES_EGL_image: the two entry points in
// MG_Impl/GLImpl/Exporting/Definitions.cpp are handed a GLeglImageOES and have to
// answer whether it is an image this display created, and with what geometry. A
// read-only lookup belongs beside the other queries a caller outside this class
// makes, and it is the same object eglCreateImageKHR built and eglDestroyImage
// removes - the private one stays private for the class's own use.
// WHAT A HANDLE NAMES, for the GL side of GL_OES_EGL_image. The two entry points in
// MG_Impl/GLImpl/Exporting/Definitions.cpp are handed a GLeglImageOES and have to
// answer (a) whether it is an image this display created and (b) what buffer it
// describes. The numbers come back rather than the record itself: the image table and
// its layout are this class's own business, and a caller outside it has no use for
// either - which is also why this is a function and not a friend declaration.
Bool DescribeImage(EGLImageHandle image, EGLint* width, EGLint* height, EGLint* fourCc,
EGLint* planeCount) const {
const ImageObject* imageObject = TryGetImage(image);
if (imageObject == nullptr) {
return false;
}
if (width != nullptr) *width = imageObject->Width;
if (height != nullptr) *height = imageObject->Height;
if (fourCc != nullptr) *fourCc = imageObject->FourCC;
if (planeCount != nullptr) *planeCount = imageObject->PlaneCount;
return true;
}
// THE PLANE DESCRIPTORS OF AN IMAGE THIS DISPLAY CREATED, for the one caller that has
// to hand them to the render server: the dma-buf import (a texture bound to an imported
// EGLImage has no storage on the server, so the server is told which buffer backs it -
// one descriptor per plane, over the session transport's SCM_RIGHTS channel). The fd
// here is THIS side's dup(), owned by the image; the transport dups again for the
// sending process, which is why the caller does not close anything.
Bool DescribeImagePlanes(EGLImageHandle image, EGLint* planeCount, EGLint* fds, EGLint* offsets,
EGLint* pitches, Uint64* modifiers, Bool* hasModifiers, Int maxPlanes) const {
const ImageObject* imageObject = TryGetImage(image);
if (imageObject == nullptr) {
return false;
}
Int count = imageObject->PlaneCount;
if (count > maxPlanes) count = maxPlanes;
if (count < 0) count = 0;
if (planeCount != nullptr) *planeCount = count;
for (Int plane = 0; plane < count; plane++) {
const ImagePlane& described = imageObject->Planes[plane];
if (fds != nullptr) fds[plane] = described.Fd;
if (offsets != nullptr) offsets[plane] = described.Offset;
if (pitches != nullptr) pitches[plane] = described.Pitch;
if (modifiers != nullptr) modifiers[plane] = described.Modifier;
if (hasModifiers != nullptr) hasModifiers[plane] = described.HasModifier;
}
return true;
}
Bool IsDoubleBufferedSurface(EGLSurfaceHandle surface) const;
// Sync
@@ -211,11 +278,39 @@ namespace MobileGL {
EGLenum Status = EGL_SIGNALED;
};
// ONE PLANE OF AN IMPORTED BUFFER. The fd/offset/pitch triple is the same
// description drmModeAddFB2 takes, and the modifier is the DRM_FORMAT_MOD_* the
// caller declared; EGL carries up to four of these (EGL_DMA_BUF_PLANE0..3_*).
struct ImagePlane {
// THIS library's descriptor, dup()ed from the caller's when the image was made
// and closed when it dies: an EGLImage outlives the call that created it, and
// the caller is free to close its own fd the moment eglCreateImage returns.
EGLint Fd = -1;
EGLint Offset = 0;
EGLint Pitch = 0;
// The two EGL_DMA_BUF_PLANEn_MODIFIER_LO/HI_EXT halves joined back into the
// DRM_FORMAT_MOD_* they were cut from.
Uint64 Modifier = 0;
Bool HasModifier = false;
};
struct ImageObject {
EGLDisplayHandle Display = EGL_NO_DISPLAY;
EGLContextHandle Context = nullptr;
EGLenum Target = EGL_NONE;
EGLClientBuffer Buffer = nullptr;
// WHAT AN EGL_LINUX_DMA_BUF_EXT IMAGE IS HOLDING, and zero for every other
// target: the buffer's geometry and the fourcc that says what its samples mean.
// It is kept here because the image is the only thing that outlives the call -
// a caller that handed over an fd has nothing left to describe it with.
EGLint Width = 0;
EGLint Height = 0;
EGLint FourCC = 0;
EGLint PlaneCount = 0;
// Planes 0..PlaneCount-1 are described, in order; index 0 is the plane every
// import in this tree reads, the rest are carried because EGL describes them and
// a consumer that understands a multi-plane format must be able to see them.
ImagePlane Planes[kMaxImagePlanes];
};
struct ThreadCurrentState {
@@ -251,6 +346,9 @@ namespace MobileGL {
const SurfaceObject* TryGetSurface(EGLSurfaceHandle surface) const;
const SyncObject* TryGetSync(EGLSyncHandle sync) const;
const ImageObject* TryGetImage(EGLImageHandle image) const;
// Closes the descriptors an image owns. Both places an image can die call it:
// eglDestroyImage, and eglTerminate, which drops a display's images wholesale.
static void ReleaseImageDescriptors(ImageObject& imageObject);
void ReleaseDisplayObjects(EGLDisplayHandle display);
void ReleaseThreadUnlocked(const std::thread::id& threadKey);
@@ -105,7 +105,20 @@ namespace MobileGL::MG_State::GLState {
return m_bufferBindPointTargets[i][index];
}
}
MOBILEGL_ASSERT(false, "Invalid BufferTarget enum value for binding point: %d", static_cast<int>(target));
// A GLOBAL TARGET ASKED THROUGH THE INDEXED LOOKUP. Only four targets have per-index binding
// points (Uniform, TransformFeedback, AtomicCounter, ShaderStorage); the rest - Vertex, Index,
// PixelPack/Unpack, Texture, CopyRead/Write, Query, the indirect ones - are global, and a caller
// that asks for one here is reading the wrong accessor, not passing a bad enum.
//
// This used to abort, and the compositor reached it right after its vertex-attrib setup: the
// whole session died on Trace/breakpoint trap ("Invalid BufferTarget enum value for binding
// point"). Aborting a compositor over an accessor mix-up is the wrong trade, so the answer is
// a one-time warning and index 0 of the indexed array: the caller gets a valid slot, the
// global binding is untouched, and the mismatch stays visible in the log instead of being a
// crash. The proper fix is for that caller to use the global accessor.
MGLOG_W_ONCE("BufferState::GetBindingPoint: target=%d has no per-index binding points; answering "
"index 0 of the indexed array (a global target was asked through the indexed lookup)",
static_cast<int>(target));
return m_bufferBindPointTargets[0][index];
}
} // namespace MobileGL::MG_State::GLState
@@ -13,6 +13,23 @@
#include <atomic>
#include <MG_Pipe/PipeMutation.h>
// The display host's frame, as the backend publishes it (MG_Backend/DirectGLES/DirectGLES.h).
// DECLARED HERE RATHER THAN INCLUDED, and that is a deliberate compromise with a named end: the
// client asks whether its framebuffer is complete BEFORE any backend binding runs, so this judgement
// cannot ask the backend - and the frontend knowing a backend fact is exactly what a record is for.
// The wire does not carry one yet (MG_Protocol's dispatch has no glEGLImageTargetTexture2DOES), so
// the fact is read from where it already is; when that record lands, this block goes with it.
// The backend namespace is the one DirectGLES.cpp:67 opens and defines these in - the SAME nesting
// as Uint g_hostFrameFramebufferId beside them. Getting this wrong is not a typo with no cost: a
// Linux shared library links with undefined symbols allowed, so the container build passed while the
// Android server build (ld.lld, --no-undefined) named all three - which is how a latent runtime
// failure was caught by the platform that builds the render server.
namespace MobileGL::MG_Backend::DirectGLES {
extern Uint g_hostFrameRenderbuffer;
extern Uint g_hostFrameWidth;
extern Uint g_hostFrameHeight;
}
namespace MobileGL::MG_State::GLState {
// Starts at 1 so a zero-initialized memo slot can never carry a live object's id.
// Atomic for the same reason as the VAO counter: it costs nothing, and a duplicate
@@ -174,8 +191,34 @@ namespace MobileGL::MG_State::GLState {
Int width = -1, height = -1;
Int validAttachmentCount = 0;
// THE IMPORTED dma-buf, and why it is judged BEFORE the validity test rather than after it.
// Measured: with kwin_wayland's framebuffer attached - it imports the display daemon's
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937 fourcc
// 0x34324241") and builds an FBO over that texture - the server answered
// "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT". The old early-continue below dropped the
// attachment (a texture that arrived this way has no storage, so it reports itself invalid)
// and the completeness gate further down was never reached.
//
// THE GATE IS THE MEASURED SHAPE, and it is deliberately narrow: this process is holding a
// display host frame (g_hostFrameRenderbuffer != 0), and the attachment is a texture with no
// storage of its own. That buffer IS the frame the host handed this process - same memory -
// and the backend binds the frame's renderbuffer in place of the empty texture
// (MG_Backend/DirectGLES/Managers.cpp). A size comparison would be the sharper test and is
// impossible here: the imported attachment has no size, which is exactly why the wire needs a
// record to say so (MG_Protocol's dispatch has no glEGLImageTargetTexture2DOES yet); when it
// lands, this gate is replaced by that record and nothing else changes.
// AND IT DELIBERATELY DOES NOT ASK WHETHER THE FRAME IS IN HAND YET, which the first version
// of this gate did - and that version did not work, measured: kwin imports the dma-buf and
// builds its framebuffer BEFORE the host-framed surface exists, so g_hostFrameRenderbuffer is
// still zero at the instant the client asks, the gate refused, and kwin printed
// "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" again. What the attachment IS is the fact
// that holds at that instant: a texture with no storage of its own is the imported dma-buf,
// and the buffer it names is the display daemon's frame - the one this process is about to be
// handed. The backend binds that frame's renderbuffer to the attachment as soon as it has
// one (Managers.cpp re-syncs attachments per draw, so the first draw after the frame arrives
// lands in it).
for (const auto& attachmentObject : m_attachmentObjects) {
if (!attachmentObject.IsValid()) continue;
if (!attachmentObject.IsValid() && !attachmentObject.IsTexture()) continue;
++validAttachmentCount;
const auto& attachment = attachmentObject;
@@ -190,7 +233,44 @@ namespace MobileGL::MG_State::GLState {
return false;
}
if (!attachment.IsComplete()) {
// AN IMPORTED dma-buf COUNTS AS COMPLETE, in the measured shape and no other: the
// attachment has no storage of its own and this process holds a display host frame of the
// same size. Measured, and the reason this is here at all: kwin_wayland imports the
// display daemon's dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of
// 1440x2937 fourcc 0x34324241") and then refuses its own framebuffer with "framebuffer for
// dmabuf 0 is not complete" - a refusal that is WRONG here, because the buffer it imported
// IS the frame the display host handed this process (same memory, same size) and the
// backend binds that frame's renderbuffer in place of the empty texture
// (MG_Backend/DirectGLES/Managers.cpp, the external-OES arm under the same gate). A size
// that differs, or no frame held, keeps the old answer: an ordinary empty attachment is
// still incomplete, and nothing is ever rendered into a buffer of the wrong size.
// AND IT DOES NOT ASK WHETHER A FRAME IS HELD, OR HOW BIG IT IS - which the first version
// of this gate did, and which is why it never worked. This judgement runs on BOTH sides
// of a split run: the client answers glCheckFramebufferStatus out of its own state before
// any backend binding happens, and the client holds no frame at all. So a gate that
// required g_hostFrameRenderbuffer != 0 and a matching size refused on the client every
// time, and kwin kept printing "Invalid framebuffer status:
// GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" - measured, twice, after that gate was
// deployed. What the attachment IS is the fact that holds on both sides: a texture with
// no storage of its own is the imported dma-buf, and the server binds the display host's
// frame to it as soon as it has one (Managers.cpp re-syncs the attachments per draw, so
// the first draw after the frame arrives lands in it). An attachment that IS valid - an
// ordinary texture or renderbuffer with real storage - still has to be complete.
// A TEXTURE ATTACHMENT THAT IS NOT COMPLETE IS THE IMPORTED dma-buf, and it counts. The
// condition is written on IsTexture() and NOT on IsValid(), which is the mistake two
// earlier versions of this gate made - measured both times as
// "Invalid framebuffer status: GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT" from a session
// that was healthy and had really reached the import:
// * requiring a host frame to be in hand refused on the CLIENT, which holds none;
// * requiring the attachment to be INVALID refused too, because AttachTexture registers
// the attachment (FramebufferObject.cpp:163) whether or not the texture has storage -
// so the imported one is perfectly valid and simply incomplete.
// What is true on both sides, at that instant, is what this asks: the attachment is a
// TEXTURE and it is incomplete, i.e. it has no storage of its own, and the server binds the
// display host's frame to it as soon as it holds one (Managers.cpp re-syncs the attachments
// per draw). A RENDERBUFFER attachment still has to be complete, so an ordinary empty
// attachment of that kind keeps the old refusal.
if (!attachment.IsComplete() && !attachment.IsTexture()) {
return false;
}
}
@@ -21,6 +21,13 @@ namespace MobileGL {
Texture2DArray,
TextureCubeMapArray,
Texture2DMultisampleArray,
// GL_TEXTURE_EXTERNAL_OES, APPENDED SO NO EXISTING VALUE MOVES. The compositor imports the
// display daemon's dma-buf with eglCreateImageKHR and binds the result through this target
// (glBindTexture(GL_TEXTURE_EXTERNAL_OES, tex) + glEGLImageTargetTexture2DOES). Without a
// member here the bind is answered INVALID_ENUM (measured with probe15: error 0x500 -> 0x0500),
// the texture never enters a binding slot, it has no internal format, and the framebuffer
// built over it is reported incomplete - "framebuffer for dmabuf 0 is not complete".
TextureExternalOES,
TextureTargetCount,
Unknown = -1
};
@@ -14,6 +14,9 @@ namespace MobileGL {
TextureObject2D::TextureObject2D(Uint externalIndex)
: TextureObjectWithOneMipmap(TextureTarget::Texture2D, externalIndex) {}
TextureObject2D::TextureObject2D(Uint externalIndex, TextureTarget target)
: TextureObjectWithOneMipmap(target, externalIndex) {}
Uint TextureObject2D::GetIndexOfTextureUploadTarget(TextureUploadTarget target) const {
MOBILEGL_ASSERT(target == TextureUploadTarget::Texture2D ||
target == TextureUploadTarget::ProxyTexture2D,
@@ -15,6 +15,9 @@ namespace MobileGL {
class TextureObject2D : public TextureObjectWithOneMipmap {
public:
explicit TextureObject2D(Uint externalIndex);
// An external texture (GL_TEXTURE_EXTERNAL_OES) is a 2D texture whose storage is an
// imported EGLImage; it keeps that target so the render server can tell it apart.
TextureObject2D(Uint externalIndex, TextureTarget target);
const Vector<TextureUploadTarget>& GetUploadTargets() const override { return m_uploadTargets; }
protected:
@@ -35,6 +35,14 @@ namespace MobileGL::MG_State::GLState {
return MakeShared<TextureObject2DCube>(index);
case TextureTarget::Texture2D:
return MakeShared<TextureObject2D>(index);
// GL_TEXTURE_EXTERNAL_OES: an external texture IS a 2D texture whose storage comes from an
// imported EGLImage (the compositor's display-daemon dma-buf). The object is the 2D one; what
// differs is where the storage comes from, and the import path records that. Without this row
// the factory asserted "Unimplemented texture type when creating texture object!" the moment
// kwin bound one (measured: the compositor died on Trace/breakpoint trap right after the bind
// was accepted).
case TextureTarget::TextureExternalOES:
return MakeShared<TextureObject2D>(index, TextureTarget::TextureExternalOES);
case TextureTarget::Texture3D:
return MakeShared<TextureObject3D>(index);
case TextureTarget::TextureBuffer:
@@ -19,6 +19,10 @@ namespace MobileGL {
case GL_TEXTURE_2D:
case GL_PROXY_TEXTURE_2D:
return TextureTarget::Texture2D;
// GL_TEXTURE_EXTERNAL_OES: what the compositor binds an imported dma-buf to. Spelled as
// the number because the GL headers this tree bundles do not define the token.
case 0x8D65:
return TextureTarget::TextureExternalOES;
case GL_TEXTURE_3D:
case GL_PROXY_TEXTURE_3D:
return TextureTarget::Texture3D;
@@ -400,6 +404,14 @@ namespace MobileGL {
return TextureUploadTarget::Texture1D;
case GL_TEXTURE_2D:
return TextureUploadTarget::Texture2D;
// GL_TEXTURE_EXTERNAL_OES (0x8D65), an upload target of its own right: the compositor
// attaches the imported dma-buf texture to a framebuffer through this enum, and the attach
// path validates the target here. Left unmapped it became Unknown, the validation failed,
// the attachment was never recorded, and the compositor's own check then read
// GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT. External textures ARE two-dimensional, so
// the upload target is the 2D one.
case 0x8D65:
return TextureUploadTarget::Texture2D;
case GL_TEXTURE_3D:
return TextureUploadTarget::Texture3D;
case GL_PROXY_TEXTURE_2D:
@@ -497,6 +497,7 @@ namespace MobileGL {
CASE(E_GL_OES_fixed_point)
CASE(E_GL_OES_single_precision)
CASE(E_GL_OES_compressed_paletted_texture)
CASE(E_GL_OES_EGL_image)
CASE(E_GL_OES_read_format)
CASE(E_GL_OES_query_matrix)
CASE(E_GL_EXT_depth_bounds_test)
+1
View File
@@ -124,6 +124,7 @@
#include <cstdio>
#include <string>
#include <MG_Util/PlatformStubs.h>
namespace MobileGL {
namespace MG_Util {
+5
View File
@@ -489,6 +489,11 @@ namespace MobileGL {
E_GL_OES_fixed_point,
E_GL_OES_single_precision,
E_GL_OES_compressed_paletted_texture,
// GL_OES_EGL_image: taking a texture (or renderbuffer) over an EGLImage. Named because
// libepoxy resolves glEGLImageTargetTexture2DOES by NAME and the entry points are now real
// (MG_Impl/GLImpl/Exporting/Definitions.cpp), with the render-server half of the import
// still to come - see the note there.
E_GL_OES_EGL_image,
E_GL_OES_read_format,
E_GL_OES_query_matrix,
E_GL_EXT_depth_bounds_test,
@@ -8,6 +8,12 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- The two permissions that decide whether the server survives a dozing phone: the
notification one lets the foreground service show the notification that is its reason to
exist (Android 13+), and the exemption one is what the user grants from the control screen
so the system stops freezing the process. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<application
android:hasCode="true"
@@ -1,9 +1,15 @@
package top.mobilegl.plugin;
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
import android.graphics.Typeface;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
@@ -58,6 +64,8 @@ public final class ServerControlActivity extends Activity {
private Button stopButton;
private TextView statusView;
private volatile Thread probeThread;
private Button allowBackgroundButton;
private TextView keepAliveView;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -108,6 +116,28 @@ public final class ServerControlActivity extends Activity {
boxParams.topMargin = dp(8);
contentLayout.addView(onScreenBox, boxParams);
// KEEPING THE SERVER ALIVE IS TWO PERMISSIONS THE USER GIVES, and both are invisible
// until something goes wrong: without the notification one (Android 13+) the service
// notification cannot be shown, 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. This section shows the state of
// both, and asks for whichever is missing.
keepAliveView = makeText("", 12, COLOR_INFO, false);
LinearLayout.LayoutParams keepParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
keepParams.topMargin = dp(12);
contentLayout.addView(keepAliveView, keepParams);
allowBackgroundButton = new Button(this);
allowBackgroundButton.setText("Allow background (notification + battery)");
allowBackgroundButton.setAllCaps(false);
allowBackgroundButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13);
allowBackgroundButton.setOnClickListener(v -> requestKeepAlive());
LinearLayout.LayoutParams allowParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
allowParams.topMargin = dp(6);
contentLayout.addView(allowBackgroundButton, allowParams);
LinearLayout buttons = new LinearLayout(this);
buttons.setOrientation(LinearLayout.HORIZONTAL);
LinearLayout.LayoutParams buttonsParams = new LinearLayout.LayoutParams(
@@ -514,6 +544,73 @@ public final class ServerControlActivity extends Activity {
statusView.setTextColor(color);
}
// ---- keeping the server alive ----------------------------------------------------------
private static final int NOTIFICATION_PERMISSION_REQUEST = 40614;
private boolean notificationsAllowed() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true;
return checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED;
}
private boolean batteryExempt() {
PowerManager power = getSystemService(PowerManager.class);
return power != null && power.isIgnoringBatteryOptimizations(getPackageName());
}
private void refreshKeepAlive() {
if (keepAliveView == null) return;
boolean notifications = notificationsAllowed();
boolean exempt = batteryExempt();
keepAliveView.setText("Keep alive: notification " + (notifications ? "allowed" : "NOT allowed")
+ ", battery exemption " + (exempt ? "granted" : "NOT granted"));
keepAliveView.setTextColor(notifications && exempt ? COLOR_PASS : COLOR_WARN);
if (allowBackgroundButton != null) allowBackgroundButton.setEnabled(!(notifications && exempt));
}
private void requestKeepAlive() {
// One at a time, and in this order: the notification prompt is a system dialog of ours,
// while the battery one leaves the app entirely - so it goes second, and coming back from
// it re-runs the other branch through onResume.
if (!notificationsAllowed() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissions(new String[] { Manifest.permission.POST_NOTIFICATIONS },
NOTIFICATION_PERMISSION_REQUEST);
return;
}
if (!batteryExempt()) {
try {
startActivity(new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:" + getPackageName())));
} catch (RuntimeException missingDialog) {
// Some builds ship without that dialog; the list is the honest fallback rather than
// a button that does nothing.
Log.w(TAG, "no battery-optimization dialog; opening the list", missingDialog);
startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
}
return;
}
setStatus("Already kept alive: the notification is allowed and the battery exemption is granted.",
COLOR_PASS);
}
@Override
protected void onResume() {
super.onResume();
refreshKeepAlive();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
super.onRequestPermissionsResult(requestCode, permissions, results);
if (requestCode != NOTIFICATION_PERMISSION_REQUEST) return;
refreshKeepAlive();
// The denial is worth saying out loud: the server still runs, but its notification is gone,
// and a service the system cannot show is a service it may stop.
if (!notificationsAllowed())
setStatus("Notifications are denied, so the server has no visible notification. "
+ "Android may stop it in the background.", COLOR_WARN);
}
private TextView makeText(String text, int sizeSp, int color, boolean bold) {
TextView view = new TextView(this);
view.setText(text);
@@ -0,0 +1,63 @@
# 容器当 client,手机当 server(上屏)
> 2026-10-01 在 Redmi 23117RK66C(Adreno 750,Android 16)与它的 Droidspaces Fedora 44
> 容器上实测通过。本文记的是那条链路上**真正要设的东西**,以及两个会让人白跑一次的坑。
## 为什么不需要 `adb forward`
Droidspaces 容器与 Android **共用网络命名空间**(实测:容器里 `127.0.0.1:22` 就是 Android 的
sshd,`ip addr` 列出的是手机的 `wlan0` / `rmnet_data5`)。所以服务端监听手机回环即可,
容器直接用 `tcp://127.0.0.1:<port>` 连上去,不需要 `adb forward`,也不需要局域网地址。
## 起服务端(手机侧,上屏形态)
```bash
TOKEN=<>=16 字节>
adb shell am start -n top.mobilegl.plugin/top.mobilegl.plugin.MobileGLDisplayActivity \
--es listen tcp://127.0.0.1:40613 --es token "$TOKEN"
# 就绪行:
# MG_Remote server: pid=… listening on tcp://127.0.0.1:40613 (in-process display server, display installed)
```
**不要**给这次启动加 `--es env MOBILEGL_IPC_REQUIRE_SAME_BUILD=1`,除非两端确实同 build:
按契约,`Dial == Connect` 时 build stamp 不同**只应警告**;加上它,服务端会因为 app 的戳(发布
APK 里是 0)与容器客户端的戳(源码树短哈希)不同而 `Refuse{BuildFingerprint}`——症状是客户端
在 `eglCreateWindowSurface` 之前就被 abort,日志里只有一条与真正原因无关的 caps 门。
## 容器侧(客户端)
```bash
export MOBILEGL_TRANSPORT=spawn # 两进程;endpoint 由下面这条给
export MOBILEGL_IPC_CONTROL=tcp://127.0.0.1:40613
export MOBILEGL_IPC_DATA=stream # 跨进程字节流;同机共享段是另一条轴
export MOBILEGL_IPC_SURFACE=server # 窗口是服务端的:客户端传 NULL 窗口
export MOBILEGL_IPC_TOKEN=<同一个>
export LD_LIBRARY_PATH=/root/build-cont # 容器里那份 split 构建
./onscreen_client # eglCreateWindowSurface(dpy, cfg, NULL, {EGL_WIDTH,EGL_HEIGHT})
```
客户端侧不需要 GPU、不需要 `/dev/dri`、不需要 Wayland:它只记录,画的是服务端。
## 实测结果
```
EGL 1.5 vendor=MobileGL version=1.5 MobileGL
surface created: 1440x3200 (asked 1440x3200)
GL_RENDERER=Espryt (MobileGL Core) (Adreno (TM) 750, OpenGL ES 3.2)
frame 0..11 swap=1
```
服务端逐帧:
```
P65ServerFrame frame=8..12 records=4 apply_ms=1.0..1.7
```
抓屏为整屏 `#72e3d8`,即客户端第 3 种清屏色 `(0.20, 0.90, 0.85)` 经 sRGB 转换后的值——
容器里那个没有 GPU 的进程要求画的东西,出现在手机屏幕上。
## 待办(本页不含)
- kwin:让 Plasma 的合成也走这条客户端(`LD_PRELOAD` 或直接链接容器里那份 `libEGL`),
窗口面仍是服务端形态;此时 Anland 只剩 Wayland 协议与输入。
- 两端同 build:容器里的源码树与 APK 从同一 revision 构建后,`REQUIRE_SAME_BUILD=1` 才有意义。
+122
View File
@@ -181,6 +181,128 @@ and the client reads both fills through the still-held pointer; after the last
round it unlocks and relocks once more (`after unlock+relock`), which separates
"GPU writes never landed" from "the held mapping kept stale lines".
## T4 — an image allocated outside Android
Can the render server draw into an image that was allocated **outside** Android —
a GBM buffer minted in a Linux container on the same kernel? That is the
`{Owner=Platform, Storage=OfferedImage}` cell an Anland-style Wayland host needs,
and nothing in MobileGL imports a dma-buf today. It is a per-device fact, not a
code fact.
### What the device answered
Redmi 23117RK66C, Adreno 750, Android 16, Droidspaces Fedora 44 container,
2026-09-30. The answer has two halves and they point in opposite directions.
| row | verdict | what it means |
|---|---|---|
| `T4-image-import` | `UNSUPPORTED` | This driver does not advertise `EGL_EXT_image_dma_buf_import`, so a container-minted dma-buf has no spelling as an EGLImage here. A capability answer, not a failure — and the reason the second row exists. |
| `T4-ahb-image` | `FAIL`, and the failure is the finding | **Android-side allocation works end to end.** `AHardwareBuffer_allocate` → `eglGetNativeClientBufferANDROID` → `eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID)` → `glEGLImageTargetRenderbufferStorageOES` (FBO `GL_FRAMEBUFFER_COMPLETE`) → clear, `glFinish`, and a waited `glFenceSync` — all clean — and `glEGLImageTargetTexture2DOES` reads **the same bytes back on both pixels**. The container then imports the same handle's fd with `gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER, …)` and **that** is where it breaks: the bytes it maps are not the bytes the GPU wrote, and its own writes do not arrive either. |
The reason is visible in the offer: the AHB's handle carries **two** descriptors,
while the import claims `DRM_FORMAT_MOD_LINEAR` over plane 0 alone. A multi-plane,
possibly compressed allocation imported as one linear plane is not a view of that
buffer, so a CPU mapping of it agrees with nothing — and no userspace API hands the
importer the modifier it should have used, because `AHardwareBuffer` does not expose
one. (`AHardwareBuffer_Desc::stride`, for its part, is in **pixels** while every
consumer on the wire wants bytes; that trap cost a device run and is now a named
constant beside the fourcc.)
### What this decides
The cell is reachable **only** when the image is allocated where the server can use
it — on the Android side — **and** its layout travels with the offer. That is a
requirement on the host that mints the scanout buffers, not a MobileGL feature:
Anland-style hosts that allocate in the container cannot be served by this driver,
and a host that publishes plane count, strides, offsets and modifier alongside the
fd can be. Note also that the container never needs a CPU view of the image in the
product: the compositor hands the buffer to its GL, which is MobileGL, and the
server renders into it. The CPU mapping in this leg is a *verification device* —
which is exactly why `FAIL` here means "a linear view is invalid", not "the driver
cannot render into an AHardwareBuffer".
This leg's topology differs from every other one: the peer is **not** a re-exec
child. It is a glibc process inside the container, because only that side can
allocate on the device Anland hands the container. So the probe **listens**
(`--only-t4[=@endpoint]`, default `@mgl-t4`) and the peer **dials** it. The
endpoint is an abstract socket name because a Droidspaces container runs
`net_mode=host`: the abstract namespace is common to both sides, while a
filesystem pathname would have to exist in the other side's root.
Both rows run on the one connection `--only-t4` opens, so the peer never waits for
a frame by name and a skipped row is not an error.
* **`T4-image-import`** — the container-minted direction. This side has no GBM
device, so it waits for an fd, asks the driver whether the announced
`(fourcc, modifier)` pair is one it even lists (`eglQueryDmaBufFormatsEXT` / `eglQueryDmaBufModifiersEXT`; a pair
the driver does not list is a capability answer, an import failure on a pair it
*does* list is a driver bug, and the two are never reported the same way),
imports the buffer as a texture **and** as a renderbuffer, reads two pixels on
each path — `(0,0)` and the far corner, so a wrong stride or offset cannot
pass as a working import — and then clears each path to a colour.
* **`T4-ahb-image`** — the Android-minted direction, and the one the measurement
says the architecture must use. This side allocates an `AHardwareBuffer`
(`R8G8B8A8_UNORM`, `GPU_SAMPLED_IMAGE|GPU_COLOR_OUTPUT|CPU_READ_OFTEN|CPU_WRITE_OFTEN`),
imports **the same buffer** into GL as an EGLImage, clears it through the
renderbuffer path behind a reported fence, hands its handle to the peer over a
socketpair (`AHardwareBuffer_sendHandleToUnixSocket`), and then reads the peer's
answer back through the **texture** path at `(0,0)` and at the far corner. One
decisive leg, `ahb[D]`: it needs the peer to have seen this side's clear colour
at both pixels *and* this side to have read the peer's pattern at both.
* **`peer/t4_gbm_peer.c`** — serves both rows. For the first it allocates with
`gbm_bo_create(…, GBM_FORMAT_ARGB8888,
GBM_BO_USE_RENDERING|GBM_BO_USE_LINEAR)`, fills the whole mapping with
`--seed=RRGGBBAA` in ARGB8888 memory order (B,G,R,A; a *solid* fill, so a
vertical flip cannot turn a working import into a reported failure), sends the
fd over `SCM_RIGHTS`, and after each of the probe's two writes takes a **fresh**
read mapping and answers with the bytes it actually saw. A driver that refuses
the allocation has answered the question: the refusal is printed verbatim and
the peer exits non-zero, with no silent fallback to another format. For the
second it takes the descriptors off the socketpair, imports the first with
`gbm_bo_import(GBM_BO_IMPORT_FD_MODIFIER, …)` and reports **how many** arrived —
the plane count is itself evidence — then compares both pixels against the
announced colour before writing a pattern of its own.
The peer **must run inside the container**: `/dev/dri/renderD128` there is the
Anland-backed `msm_drm` GBM device, and the Android side has no GBM device at all.
```sh
S=<serial>; DS="/data/local/Droidspaces/bin/droidspaces --name=Fedora"
# peer: into the container's own rootfs (adb push lands on the Android side),
# then built there with the container's own glibc gcc -- the documented line is
# gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm
base64 -w0 tools/spikes/extmem_probe/peer/t4_gbm_peer.c > /tmp/t4_gbm_peer.c.b64
adb -s $S shell "$DS run sh -c 'base64 -d > /root/t4_gbm_peer.c'" < /tmp/t4_gbm_peer.c.b64
adb -s $S shell "$DS run sh -c 'cd /root && gcc -O2 -o t4_gbm_peer t4_gbm_peer.c -lgbm'"
# probe: build and start it FIRST — it is the side that listens
ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 ./build_android.sh /tmp/extmem-build
adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/extmem_probe
adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe --only-t4" \
| tee out-t4-$S.txt
# then, with the probe listening, dial it from the container
adb -s $S shell "$DS run /root/t4_gbm_peer --endpoint=@mgl-t4"
```
Peer options: `--endpoint=@mgl-t4`, `--width=64`, `--height=64`,
`--seed=RRGGBBAA`, `--timeout=60`. The row is `T4-image-import`, with two
decisive legs `tex[D]` and `rb[D]` under the usual rule: `OK` needs each path to
have read the peer's seed bytes **and** to have had its own write observed by the
peer. The peer exits 0 only on that same two-way condition, and its last line
carries every raw value (stride, format, modifier, the fd's kernel name, the
exporter, both pixels of both paths) — read those, not the exit code.
Two things there will look like failures and are not. `gbm_bo_unmap()` of a
**WRITE** mapping segfaults on this container's mixed-version Mesa (distro
`libgbm`/`dri_gbm` 26.1.5 plus an unowned self-built
`libgallium-26.2.0-devel.so`), so the peer deliberately leaves its fill mapping
mapped instead. And the peer's fd name plus exporter (`/dmabuf:`,
`exp_name: system`) is the evidence that this is the same *kind* of object the
compositor scans out: kwin's own scanout allocations come from that same system
dma-heap.
## Route mode (P11 B2 step 1)
Does a client's AHB cross the *real* B1 routes into the server app? A spike APK built with
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff