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
swung0x48 648800a2da [Docs] (P8): close the phase - ruling ID-P8-19 (the exit device check and the P12 cross-host rerun), the P8 README and PLAN, the roadmap row and header, MEASUREMENTS row 19, the status page and the last debt 2026-09-30 08:25:38 -04:00
swung0x48 b4bee9dd38 [Docs] (P8): CONTRACT-P8 (the rules P8 landed, each with its code anchor and its test), rulings ID-P8-14..18 (MD + MF and the create-indirect fixture's alignment, SE, SV, the dev merge, the wave 2 integration gate) and the debts wave 2 hands on 2026-09-30 07:55:50 -04:00
swung0x48 494305381e [Merge] (P8): integrate wave 2 package MF - the push flush ladder gets dev's Adreno 830 queued-ring-copy guard (FlushPendingRangesFrom, create-instancing x Espryt x monolith 0.870 -> 0.99998 on the Redmi), and both G5 scripts pin the two reviewed ladder bodies under ID-P8-14 2026-09-30 07:12:50 -04:00
swung0x48 fb106a682e [Docs] (P8-MF): the package note - the push-ladder mirror of dev f973008c, create-instancing Espryt monolith 0.870 -> 0.99998 on the Redmi with spawn unchanged, why spawn was unaffected (the same two partial maps behind queued ring copies, the copy already submitted by the server's intervening whole-buffer maps), the red-onces and the gate 2026-09-30 07:09:28 -04:00
swung0x48 c8c4397475 [Merge] (P8): integrate wave 2 package SV - Magma's 3D native mip blit uses one layer, the depth/stencil mip decline gets its tally row, 1D depth mips decline by name, and the wire arm issues glDispatchComputeIndirect natively
# Conflicts:
#	MobileGL/MG_IntegrationTest/CMakeLists.txt
2026-09-30 06:11:04 -04:00
swung0x48 9c24a14d2b [CI] (scripts, P8-MF): pin G5's tenth row as well and re-pin both flush ladders on the reviewed post-fix bodies (ID-P8-14) - dev f973008c moved FlushPendingRangesNow past every base ref CI passes (the Adreno 830 queued ring copy) and ac9ab078 mirrors it into FlushPendingRangesFrom; both scripts' self-tests now run the pin-precedence and one-token controls on both ladders 2026-09-30 05:45:10 -04:00
swung0x48 ac9ab078cc [Fix] (Espryt, P8-MF): the push arm's flush ladder keeps a partial range-invalidating map behind an upload-ring copy still queued on the store, as dev f973008c's pull arm does - FlushPendingRangesFrom's ring tier stamps ringCopyRetireSerial and InvalidateFlushAccessFor refuses tier 1 for a partial range while that copy is unretired (the whole-buffer orphan-map stays exempt); Adreno 830 loses the queued copy and create-instancing on the monolith arm drew terrain sections empty 2026-09-30 05:43:55 -04:00
swung0x48 377d90d9f1 [Merge] (P8): integrate wave 2 package SE - the spawn/tcp default framebuffer publishes its surface to the client, the CPU mip arm declines on a driver-written base, D's case 3 runs on the Espryt split arms, the unmigrated-emulation name list is linted against its call sites, and the P8 notes' citations point at their content again 2026-09-30 05:24:11 -04:00
swung0x48 4cfb29a553 [Docs] (P8-SV): the note's gate table (G1 0/0, wire-declines 61/66, split_coverage, every lane, retrace 230/236) and the dirty-surface scan finding 2026-09-30 05:02:37 -04:00
swung0x48 51e95c9260 [Docs] (P8-SE): the note's gate and retrace readings 2026-09-30 04:47:27 -04:00
swung0x48 982958ebb1 [Test] (P8-SV): rename WireIndirectDispatchScenario's dispatch store member m_parameters -> m_groupCounts - gen_pipe_dirty_surface.py reads writes to shutter member names across all of MobileGL, and the name made NEW_PATCH_STATE's shutter look written outside its roots (self-test negative control 16 stopped tripping) 2026-09-30 04:42:06 -04:00
swung0x48 3717424229 [Merge] (P8): merge dev f973008c - the Espryt partial-flush fix for a queued ring copy on Adreno 830 (wave 2 MD), and dev's empty-binding uniform block fix deduplicated against P11's 7784d6bb (one code path), UnboundUniformBlockScenario registered on the split arms 2026-09-30 04:34:15 -04:00
swung0x48 f973008c4d [Fix] (Espryt): a partial buffer flush no longer takes the range-invalidating map while an upload-ring copy into the same store may still be queued - Adreno 830 loses the queued copy, and create-instancing drew whole Embeddium terrain sections empty 2026-09-30 04:07:41 -04:00
swung0x48 edcb9328e2 [Docs] (P8-SE): cite the integration CMakeLists by path - the bare basename resolves to the top-level file 2026-09-30 03:55:20 -04:00
swung0x48 e61060fbdf [Docs] (P8-SE): the package note, and the P8 package notes' line citations re-anchored by content at HEAD (A 14, B 29, C 10, D 12, E 18 plus the copy-image-shadow-mirror line, F 4) 2026-09-30 03:55:01 -04:00
swung0x48 3ce8ce3cb1 [Test] (P8-SE): the ForcedDs knob lanes run the default-framebuffer depth/stencil case again on all three arms (it was subtracted while red on spawn / tcp) 2026-09-30 03:55:01 -04:00
swung0x48 394d255487 [Docs] (P8-SV): the package note - the four items with their before/after, the design, the gates, ten red-once rows, the monolith observation and what was found but not fixed 2026-09-30 03:45:40 -04:00
swung0x48 0222852b94 [Fix] (Espryt, P8-SE): a spawn / tcp server publishes the default framebuffer's depth/stencil format (it has no frontend placeholder; the case leaves pending-fix for the three arms); the server CPU mip arm declines by name (generate-mipmap-store-declined) instead of filtering a store the driver wrote over, and E's refused-readback knob reaches it (CpuMipRefused. entries); WireIndirectDrawScenario case 3 runs on the Espryt split arms; copy-image-shadow-mirror goes and unmigrated_emulation_sites.py holds PipeCatalogueTest's list to the call sites 2026-09-30 03:44:29 -04:00
swung0x48 2edb2aa82a [Test] (P8-SV): WireMipShapeScenario (3D chain on both backends x every arm, 1D depth decline on Magma split+spawn, the 3D chain under the validation layer inproc) and WireIndirectDispatchScenario (both backends x every arm, Magma gated .IndirectDispatch.), the MipmapDepthStencilAspect tally in F1's depth/stencil decline case, WireDeclinePeek, and the two parity rows 2026-09-30 03:44:03 -04:00
swung0x48 fc415aee76 [Fix] (Magma, P8-SV): the wire arm's 3D native mip blit uses one layer with the depth in its z offsets, the depth/stencil mip decline gets its WireDeclineTally row (MipmapDepthStencilAspect), the depth shader mip refuses 1D/1D-array/3D view types before the chain grows, and glDispatchComputeIndirect is issued as vkCmdDispatchIndirect from the wire store (P8-D's barrier rule, wdsp counter, two named declines) 2026-09-30 03:44:03 -04:00
swung0x48 307267c188 [Docs] (P8): the monolith defect count is 12 (13 reports, one duplicate between C and D), 9 of them left to P13, not 14 and 11 2026-09-30 03:24:47 -04:00
swung0x48 cbbaf77f6d [Docs] (P8): wave 1 integrated - rulings ID-P8-5..13 (the survey, packages A-F, the integration gate, the wave 2 scope and the monolith defects left to P13), the survey note, open questions 9/11/15 answered, the debts and the P8 contract amendments, the P8 README table, PLAN-P8 wave 2 and the status page 2026-09-30 03:09:09 -04:00
swung0x48 39cd8fb73b [Merge] (P8): integrate package E - a texture readback the Espryt driver refuses is answered from the split server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline texture-readback-store-declined); CopyImageStoreReadbackScenario on both backends x every arm plus the extent-scoped StoreRead knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-30 02:17:51 -04:00
swung0x48 92eab9f3b7 [Merge] (P8): integrate package D - the Magma wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] natively from the wire store (vkCmdDraw*Indirect[Count]) instead of a CPU expansion behind a GPU-idle wait, with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters and WireIndirectDrawScenario on both backends x every arm 2026-09-30 01:28:20 -04:00
swung0x48 9441071567 [Merge] (P8): integrate package C - the Espryt split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it (storage/atomic/buffer-image marks, XFB captures into the copy; restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead); GpuWrittenDrawInputScenario (12 cases) on the Espryt split arms 2026-09-30 00:45:16 -04:00
swung0x48 2f530ad8a2 [Merge] (P8): integrate package B - Espryt glGenerateMipmap on the server: the record's generated window (BASE/MAX clip, B1) and RGB16F/RGB32F natively or on the server's CPU from the driver's level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-30 00:01:29 -04:00
swung0x48 c00f5eabae [Docs] (P8-E): the note's gate and retrace readings and two residuals (the extent-scoped tcp knob, the ERROR line before a store answer) 2026-09-29 23:51:49 -04:00
swung0x48 24b8aef616 [Fix] (P8 A): exempt F's Magma-only F1WireScenario.GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession on DirectGLES as single-backend - it landed with P8-F after A's exemption table was cut, skips on Espryt like its F1Wire decline siblings, and split_coverage.py named it missing on the merged head 2026-09-29 23:41:10 -04:00
swung0x48 8133cd52ae [Merge] (P8): integrate package A - register the monolith-only cases on split/spawn/tcp (P8Coverage.cmake, five server-knob tcp supervisors), gate them with split_coverage.py and its exemption table, pin the tcp supervisor's Vulkan env 2026-09-29 23:37:12 -04:00
swung0x48 06f28b0a1b [Docs] (P8-A): the note says what the census measured about the audit's 396 and DoublePrecision on tcp, and the knob-port comment names the split/verify one-port overlap 2026-09-29 23:18:08 -04:00
swung0x48 b9feb9d7be [Docs] (P8-B): the package note - the generated window and the server CPU arm, the coverage matrix, seven red-onces and monolith defects M1-M3 recorded for dev 2026-09-29 23:16:05 -04:00
swung0x48 2b4ae539da [Merge] (P8): integrate package F - reclassify what our client cannot send: the multi-draw latches and Magma client-vertex-array become ProtocolCorruption, Magma mip shader-shape becomes a pre-growth named decline, unpublished cap bits and the index-mirror sentinel marked reserved 2026-09-29 23:07:03 -04:00
swung0x48 e815ce8d33 [Docs] (P8-C): the package note - readers and writers of the server's staged copy, the twelve GPU-written draw-input cases, each red-once, the retrace subset and the five monolith defects recorded for dev 2026-09-29 22:53:18 -04:00
swung0x48 00d6c2480e [Docs] (P8-E): the package note - the split read never touched a shadow, the store route and FollowCopy, the red-once table, the monolith defect for dev 2026-09-29 22:43:02 -04:00
swung0x48 f823422bfa [Fix] (Espryt, P8-E): a texture readback the driver refuses is answered from the server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline); CopyImageStoreReadbackScenario on both backends x four arms plus the extent-scoped store-read knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-29 22:43:02 -04:00
swung0x48 e52a997f34 [Fix] (Espryt, P8-C): the split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it - storage/atomic/buffer-image writes mark the server twin, XFB captures land in the copy, the restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead; GpuWrittenDrawInputScenario (12 cases) on the split arms 2026-09-29 22:38:33 -04:00
swung0x48 c7a14ed790 [Test] (P8-F): give the mip shader-shape decline its own .ShaderMipDecline. tail - spawn_lane_parity.py holds each server-knob tail to exactly one case, and sharing .ShaderMip1. named two 2026-09-29 22:37:49 -04:00
swung0x48 d639e54616 [Perf] (P8-D, Magma): the wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] as vkCmdDraw*Indirect[Count] from the wire store instead of expanding them on the CPU behind a GPU-idle wait (create-indirect split: 321 waits / ~8 s -> 0), with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters, WireIndirectDrawScenario on both backends x every arm, notes/p8/D.md 2026-09-29 22:32:49 -04:00
swung0x48 32f222dbc2 [Docs] (P8-A): the package note - the 304-case census per arm, the server-knob lanes and their supervisors, the exemption classes, the red-once record, lane wall times and the findings 2026-09-29 22:30:20 -04:00
swung0x48 a2c81a2f64 [Fix] (P8-A): the tcp lane's shared supervisor carries the Vulkan ICD and the three lavapipe iterationRP repairs, which Magma reads in the server - DirectVulkan.Tcp IterationRPProgram203 missed its golden output by one texel without them 2026-09-29 22:30:20 -04:00
swung0x48 8dac1fd8a4 [Fix] (Espryt, P8-B): glGenerateMipmap on the server - the record's plan is the generated window (B1) and RGB16F/RGB32F generate natively or on the server's CPU from its own level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-29 22:22:55 -04:00
swung0x48 b71cb9cc2c [Docs] (P8-F): cite MG_Backend/Init.cpp by path - the bare basename is ambiguous 2026-09-29 22:15:45 -04:00
swung0x48 efd3533bd1 [Docs] (P8-F): the package note - the four items, their evidence, the red-once record and what was found but not fixed 2026-09-29 22:15:29 -04:00
swung0x48 67890f523a [Refactor] (P8-F): reclassify what our client cannot send - the multi-draw client-indices/instanced latches and Magma's client-vertex-array become ProtocolCorruption, Magma's mipmap-shader-format-or-shape becomes a named decline decided before the chain grows (with a knob-forced 3D case on split and spawn), and the three unpublished cap bits and the index-mirror sentinel are marked reserved 2026-09-29 22:15:29 -04:00
swung0x48 976fbe4cd4 [Test] (P8-A): register the monolith-only integration cases on the split, spawn and tcp arms (P8Coverage.cmake: 246 ambient cases, five server-knob lanes each on its own tcp supervisor), retire the ClientSideIndices and GuiBatch exclusions, and gate the rest with split_coverage.py and its exemption table 2026-09-29 22:14:31 -04:00
swung0x48 8d3e2317a6 [Docs] (P8): re-scope the phase - strike the original scope in the roadmap and the P8 README, add PLAN-P8 (six items closed, packages P8-0 and A-G, the rewritten gates) and rulings ID-P8-1..4, and mark P8 in progress on the status page 2026-09-29 21:31:15 -04:00
swung0x48 0b55b55d4d [Docs] (P11): the status page lists the decisions waiting on the user - the P8 start and the six debt rulings 2026-09-29 21:17:21 -04:00
swung0x48 642ac891af [Docs] (P11): ruling ID-P11-20 - the closing head's gate, lanes and retrace numbers 2026-09-29 21:13:12 -04:00
swung0x48 592558f5fc [Docs] (Debts): the debt review - drop the closed E1, DualBlock and GetCaps-blobref rows (open question 24 closed, the blobref rule now a CONTRACT-P5 row-18 invariant), move the unmeasured M2 note to MEASUREMENTS, rewrite HyperOS / P11 RSS / named refusals, split the P12 row, merge the two CI rows, add three B2 rows, and mark each destination as needing a ruling or ready 2026-09-29 21:12:28 -04:00
swung0x48 0a117243d5 [Docs] (P11): close the phase - rulings ID-P11-17..19 (B2 T0 zero-copy, the stale indexed binding and Magma empty-uniform fixes with the skip-hole, T0 on by default), B2-DEVICE, the P11 README, the roadmap row and header, the status page and the debts P11 hands on 2026-09-29 20:33:00 -04:00
swung0x48 344e2f8439 [Docs] (P11 B2): T0 is the default (the user, 2026-09-29) - CONTRACT-P11 B2's tier table, unset versus 0, the revision-5 fix-up; the knob rows in design/09 2026-09-29 20:15:29 -04:00
swung0x48 91900bdc5e [Test] (P11 B2): the unset knob on a stream and on a no-AHardwareBuffer shared-segment session - T2, no W/I fallback line, the fallback counted; the 0x80 ask accepted and 0x81 refused 2026-09-29 20:15:29 -04:00
swung0x48 b32140e614 [Feat] (MG_Remote, P11 B2): T0 is the default - an unset MOBILEGL_IPC_ADOPT_TIER asks T0 (ask 0x80) and falls back to T2 quietly (MGLOG_D, still counted: client T0Fallbacks, server T0Refusals), while an explicit 0 keeps every named line; revision 5's schema comment and pin fixed up 2026-09-29 20:15:29 -04:00
swung0x48 7030d307be [Test] (Magma): UnboundUniformBlockScenario - a dispatch and a draw whose program declares a uniform block at an empty binding point (never bound, or its buffer deleted while bound) run and produce their fallback, and a buffer bound there afterwards is read, on both backends 2026-09-29 19:58:41 -04:00
swung0x48 853c5f1264 [Fix] (Magma): a uniform block declared at an empty binding point reads a zeroed block instead of dereferencing a null buffer 2026-09-29 19:55:53 -04:00
swung0x48 e44d6ac7da [Docs] (P11 B2): CONTRACT-P11 - a buffer deleted while bound to an indexed point: the defect, the fix, Magma's behaviour before it, the tests with their red-once, and the G1 delta 2026-09-29 19:44:08 -04:00
swung0x48 8571e0c001 [Test] (P11 B2): DeletedBoundBufferScenario - a buffer deleted while bound to a storage, uniform, atomic-counter or transform-feedback point, its slot reused, then a dispatch and a draw, on monolith and Split/Spawn/Tcp for both backends; and the one-process arena pair as a regression 2026-09-29 19:32:19 -04:00
swung0x48 7784d6bbf7 [Fix] (Magma): a uniform block declared at an empty binding point reads a zeroed block on the monolith arm instead of dereferencing a null buffer, as the wire arm already does 2026-09-29 19:32:19 -04:00
swung0x48 4aec5621a8 [Fix] (MG_State, P11 B2): glDeleteBuffers moves the bind-point generation of every indexed target it unbinds, and a transform-feedback object swap moves it too - the server's window no longer names a deleted buffer's handle after its slot is reused 2026-09-29 19:32:19 -04:00
swung0x48 4c02279f22 [Docs] (P11 B2): CONTRACT-P11 B2 - the Espryt split/spawn itest entries now pin MOBILEGL_ITEST_REQUIRE_GPU themselves 2026-09-29 19:12:46 -04:00
swung0x48 e869d18ce8 [Test] (P11 B2): pin MOBILEGL_ITEST_REQUIRE_GPU=1 on every Espryt split/spawn itest environment, as the Magma and Espryt TCP lists do - a handshake-time Fatal fails there instead of skipping as a ctest pass 2026-09-29 18:57:04 -04:00
swung0x48 9d1e788597 [Docs] (P11 B2): CONTRACT-P11 B2 - M2 staying green on Espryt is consistent with the driver holding its own reference, not verified 2026-09-29 18:44:25 -04:00
swung0x48 ce5753c883 [Docs] (P11 B2): CONTRACT-P11 B2 - the tier table that replaces A1's shared-segment Fatal, revision 5, the Offer and its thread, POST, the staged-copy audit, lifetime, CPU reads, host and device gates with their red-once 2026-09-29 18:24:53 -04:00
swung0x48 5e65907d63 [Test] (P11 B2): RemoteClientAdoptT0.KnobTwoIsTodaysT2WithNoT0Line - the default asks 2 and neither side settles, imports or logs T0 even where the stand-in platform could run it 2026-09-29 18:20:25 -04:00
swung0x48 98bf1925aa [Test] (P11 B2): LargeArenaAdoptionScenario - a draw still queued when the arena is rewritten or respecified keeps the bytes it was issued against (no readback between the two draws, so an in-place write or an early free shows) 2026-09-29 18:06:48 -04:00
swung0x48 17f1c0b59b [Tool] (P11 B2): extmem probe --hold-ms - the T0S child keeps its AHardwareBuffer locked while the parent holds both imports, for the dmabuf memory calibration 2026-09-29 17:50:50 -04:00
swung0x48 2522f1803c [Test] (P11 B2): T0 protocol on the host - the Offer inbox (late, out of order, stale, missing) over both aux channels, the whole T0 path on a stand-in platform, A1's two handshake-Fatal cases rewritten as named fallbacks, revision 4 refused by name, AdoptTier0 lanes on the shared-segment arms and the allow switch through spawn 2026-09-29 17:50:50 -04:00
swung0x48 7cde55b1c1 [Feat] (Magma, P11 B2): import a T0 store through VK_ANDROID_external_memory_android_hardware_buffer as a wire buffer (external VkBufferObject), serial-gated release, done-no-bytes readback, and the sustained-lock POST on the renderer's device 2026-09-29 17:50:50 -04:00
swung0x48 2a69f61c88 [Feat] (Espryt, P11 B2): import a T0 store with glBufferStorageExternalEXT as the adopted twin - no staged copy, server writes queued GPU-ordered, CPU readers on the import's coherent map, fence-gated release, re-import after a context loss, and the sustained-lock POST in a throwaway context 2026-09-29 17:50:50 -04:00
swung0x48 590d74525d [Feat] (MG_Remote, P11 B2): T0 behind MOBILEGL_IPC_ADOPT_TIER - control revision 5 (LinkTerms.adoptTier ask), kCapAdoptT0 granted at the first native bind after the backend's POST, the AHardwareBuffer Offer on the aux socket matched to its map_persistent record by seq, named T2 fallbacks instead of A1's handshake Fatal, the server allow switch, done-no-bytes CPU reads 2026-09-29 17:40:56 -04:00
swung0x48 b57276b88a [Tool] (extmem_probe, P11 B2): route mode - the probe serves in the render server's place (-Pmobilegl.extmemProbeAsServer) and --route-client sends a held-lock AHB over B1's fd: pair, directly on aux and through a socketpair hop 2026-09-29 16:44:37 -04:00
swung0x48 ac25e3323a [Tool] (extmem_probe): build inside the API-26 trace APK - memfd_create through syscall() below API 30, the two Vulkan 1.1 calls through vkGetInstanceProcAddr below API 28 2026-09-29 16:31:52 -04:00
swung0x48 96881f639c [Build] (P11 A3): MOBILEGL_BUILD_EXTMEM_PROBE / -Pmobilegl.buildExtmemProbe packages tools/spikes/extmem_probe as libMobileGLExtmemProbe.so in the trace flavour 2026-09-29 16:31:52 -04:00
swung0x48 16ea23c927 [Tool] (extmem_probe): marker/args-file mode for the trace APK exec hook, --regions-at-end, and the T0S sustained-lock rows (Vulkan copy/fill + GL compute on a never-unlocked client AHB) 2026-09-29 16:31:52 -04:00
swung0x48 b9b14c4775 [Docs] (P11): status page - the two handed-on dev bugs are merged, the gate row points at the B1 and M2 rulings 2026-09-29 16:14:34 -04:00
swung0x48 37c7aede31 [Docs] (P11): ruling ID-P11-16 on B1 (fd: endpoint, token broker pairing at hand-off, app_process helper, per-peer apply affinity, the HyperOS battery requirement, the device pass), the B1 debts, the status page, the phase README and the roadmap row 2026-09-29 16:14:18 -04:00
swung0x48 64b5794bc9 [Docs] (P11 B1): CONTRACT-P11 B1 - under the EOF Hello wait a second client slower than 2 s sees a send failure rather than Refuse{Busy}, and the unix-reach argument is the device's 2026-09-29 16:10:45 -04:00
swung0x48 ce4dca2835 [Docs] (P11 B1): CONTRACT-P11 B1 - the rebased head's device pass: rd12 shm vs tcp on both routes, Magma openra re-measured clean, the 10 s slow start and the plugin flavour 2026-09-29 16:06:30 -04:00
swung0x48 df826f426f [Docs] (P11 B1): CONTRACT-P11 B1 - the broker pairs at hand-off, a paired unix session waits for its Hello until EOF (the decision and its file:line), the lone-prime guard, the supervisor's announced core as the one source, and the follow-up gates 2026-09-29 15:03:52 -04:00
swung0x48 7ccb920ad2 [Feat] (Plugin, P11 B1): the broker presents the PairBind pair at hand-off (PairBindFrames, the encoder's bytes) and answers v2 with that flag and the apply core its supervisor announced; the helper passes MOBILEGL_IPC_FD_PAIRED=1 on, and Java no longer re-derives the reserved core 2026-09-29 15:03:52 -04:00
swung0x48 582adb0dd6 [Test] (P11 B1): the broker's PairBind templates against the encoder, a pair presented at hand-off whose Hello comes 5 s later, a pair presented only at program start refused by name, and the reserved core on lone-prime, symmetric and unreadable topologies 2026-09-29 15:00:30 -04:00
swung0x48 578795c84d [Feat] (MG_Remote, P11 B1): pairing at hand-off - the fd: endpoint presents no PairBind when MOBILEGL_IPC_FD_PAIRED=1 says the broker did, names the server's refusal when its own PairBinds arrive too late, and a paired unix session waits for its Hello until EOF instead of 10 s 2026-09-29 15:00:30 -04:00
swung0x48 9af4c8aefd [Feat] (MG_Remote, P11 B1): the reserved apply core never takes a lone prime core - several prime cores give the lowest, one gives a big core beside it or none, and the log names which - and the supervisor announces the core it reserves on stdout at start-up, the one source the broker relays 2026-09-29 15:00:30 -04:00
swung0x48 5a97eb8d1b [Docs] (P11 B1): CONTRACT-P11 B1 - anchor the evidence paths to ~/w7/notes/p11/evidence/b1 and the report 2026-09-29 14:47:31 -04:00
swung0x48 fc427a14d6 [Docs] (P11 B1): CONTRACT-P11 - the external-client route (helper, token broker, fd: endpoint), the token rule and the named refusals, the apply-thread policy by peer and plane with its selection measurements, the keep-alive setting users must make, the ROM dependence and the gates 2026-09-29 14:47:31 -04:00
swung0x48 54e7c92cf4 [Feat] (Plugin, P11 B1): the broker hands the helper the core the server reserves for the session's apply thread and the helper execs the program through taskset on every other core (--keep-affinity opts out), so a client the scheduler would put there is not starved 2026-09-29 14:47:31 -04:00
swung0x48 1abddf0d00 [Test] (P11 B1): ApplyThreadPolicy - every row of the auto table by peer and plane, the Redmi topology, the shapes with no prime pair to split, explicit masks, spins and unrecognised text 2026-09-29 14:47:31 -04:00
swung0x48 0819f9d410 [Feat] (MG_Remote, P11 B1): the apply thread's auto affinity and spin chosen per session from the peer's dial mode and data plane - a dialled-in shared-segment client leaves the apply thread only the lowest prime core, stream, forked and in-process peers keep every big core 2026-09-29 14:47:31 -04:00
swung0x48 952a1ca4a7 [Tool] (P11 B1): trace flavour gate tooling - package the retrace CLI as libMobileGLRetrace.so (packageRetraceExe) and run an external client as a child of the app kept in front (mobilegl_exec_argv) 2026-09-29 14:47:31 -04:00
swung0x48 bd05e061fc [Feat] (Plugin, P11 B1): same-device external clients - the exported token-checked broker that connects twice to the server's own endpoint and answers every request, the app_process helper that execs the program on the pair, a private broker listener beside a TCP-only supervisor, and the copyable launch command on the server screen 2026-09-29 14:47:31 -04:00
swung0x48 23a9470a28 [Test] (P11 B1): the fd: endpoint end to end against a real server process after two bare connects, its PairBind pair read off the server side, and its named refusals that write nothing 2026-09-29 14:47:31 -04:00
swung0x48 53098bc57f [Feat] (MG_Remote, P11 B1): the fd:<control>,<aux> client endpoint - adopt two connected unix sockets a broker opened for this process, refuse by name anything that is not a distinct connected AF_UNIX stream pair, and present the client's own PairBind pair before the Hello, over shared segments 2026-09-29 14:47:31 -04:00
swung0x48 76f75f3a0c [Docs] (P11): ruling ID-P11-15 on the M2 dev merge - the scattered-upload fix completed on the split arms, the trim fence re-audited on the split server, the census and ratchet notes; DEBTS closes the two merged dev bugs and takes M2 leftovers 2026-09-29 13:46:37 -04:00
swung0x48 d587d394fe [Docs] (P11): zero-copy stays switchable (ruling ID-P11-14) - MOBILEGL_IPC_ADOPT_TIER 2 (default) keeps today shared-memory push, 0 takes the T0 import, and an unusable T0 falls back to T2 with a named line; the server gets its own allow switch 2026-09-29 13:45:57 -04:00
swung0x48 de996d539f [Test] (P11 M2): DescriptorPoolGrowthScenario waits for the inproc server to apply every record it emitted before each census read - under run-ahead a census right after the burst's swap read the pools before the burst, or a trim a frame late 2026-09-29 13:29:38 -04:00
swung0x48 b4b1825951 [Docs] (P11 M2): TextureEmit.h and the applier's accumulation name the one box-only record that is not a whole level - the oversize fallback's withheld cover, a partial box spanning gaps 2026-09-29 12:36:41 -04:00
swung0x48 ec5f6ead3f [Test] (P11 M2): TextureEmit.MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero keeps its name and pins the one collapse left - a footprint larger than a record carries falls back to the storage's rect list 2026-09-29 12:26:10 -04:00
swung0x48 f636a4cdae [Test] (P11 M2): the link ratchet records TextureObjectWithOneMipmap::GetStorageDirtyFootprint - dev 9524c688's new virtual on the placeholder vtable UniformManager already reaches (171 -> 172) 2026-09-29 12:26:10 -04:00
swung0x48 bd2e43ede3 [Test] (P11 M2): dev's two Magma scenarios on the split arms - both on Espryt's three arms, OversizedRecordingSplitScenario on Magma's three (and pinned to a 64-draw budget on split and spawn, where the server reads it), DescriptorPoolGrowthScenario's census cases on Magma's split arm only; the split-recording arming reads both role logs and dev's pinned lane holds its log's RESOURCE_LOCK, and the census cases count the sets the wire arm holds between its descriptor rewinds 2026-09-29 12:20:15 -04:00
swung0x48 e2333d98a8 [Fix] (Magma, P11 M2): DirectVulkan::GetDescriptorPoolCensus reads the server renderer's pools on the apply thread under inproc (the test probe, between drain batches) instead of racing its growth, rewinds and trims, and answers unavailable on spawn and tcp 2026-09-29 12:20:15 -04:00
swung0x48 5c9ad5b499 [Test] (P11 M2): RemoteGuards pins GetStorageDirtyFootprint on the apply thread as Fatal{RoleViolation, "texture-legacy-arm"}, beside the rest of the dirty model's rows 2026-09-29 12:16:55 -04:00
swung0x48 9145433890 [Fix] (P11 M2, Espryt, Magma): the texture emitter carries a level's exact dirty footprint as its regions (dev 9524c688) - a single partial box as one region, only a whole-level write as none - so both servers upload exactly the texels written over GPU content; the applier keeps it exact up to 4096 regions, and Espryt's handle arm applies the storage's box-or-rects cut-offs itself while its texture holds no GPU writes 2026-09-29 12:16:55 -04:00
swung0x48 c371599811 [Merge] (P11): dev@c14283ee - scattered sub-image writes upload exactly their texels, Magma submits a frame's recording every 16384 draws with backpressure, and Magma grows descriptor pools geometrically and trims a quiet slot whole; the handle arm notes driver-side writes by handle and the wire arm splits recordings and keeps the trim's epoch peak across descriptor rewinds 2026-09-29 12:11:37 -04:00
swung0x48 b34eed580d [Docs] (P11): HSPIKE - an app_process helper with no Context gets connected sockets to the server app over Binder under the shell uid and another app uid, token-checked; ruling ID-P11-13 on the single broker mechanism and the ROM caveat 2026-09-29 11:42:59 -04:00
swung0x48 c14283ee2d [Docs] (Magma): the trim threshold's comment states the bound its argument proves - a growth adds at most twice what the slot held, so the slot stays over a third full 2026-09-29 10:58:33 -04:00
swung0x48 9dccabf03c [Test] (Magma): descriptor pool growth and trim - the pure sizing policy adds O(log N) pools for an N-set frame, and DescriptorPoolGrowthScenario's 4096-distinct-set frame lands whole on both backends, grows at most 7 pools, and is given back after quiet frames and re-grown while they are still in flight 2026-09-29 10:34:16 -04:00
swung0x48 53fa16c2c2 [Fix] (Magma): a frame slot whose descriptor pools stay under a quarter full for 120 frames is handed back whole - its set cache cleared, grown pools destroyed, base pool reset and every memo holding a set handle dropped - so rd12's loading burst no longer lives for the whole process (live sets 15,042 -> 39, pools 65 -> 6); MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES (0 = never) and a test-visible pool census 2026-09-29 10:34:16 -04:00
swung0x48 c2de6576ad [Fix] (Magma): a frame slot's next descriptor pool doubles its LARGEST pool of the same update-after-bind flavour, capped at 4096 sets - doubling the first (oldest, smallest) one made every grown pool the same size, so rd12's 15,016-set loading frame grew one slot to 60 pools (1x128 + 59x256); now 9 (64 ... 4096, 4096, 4096) 2026-09-29 10:34:15 -04:00
swung0x48 35e1c861a3 [Docs] (P11): ruling ID-P11-12 on pairing by PairBind nonce and revision 3 -> 4; B0 F2 corrected - the product TCP path already paired by identity, the defect was unix endpoints and the one-shot TCP server; the TCP fixture comment no longer names AcceptPair 2026-09-29 10:20:53 -04:00
swung0x48 9ebcf5a73d [Docs] (P11 PAIR): CONTRACT-P11 - the PairBind wire change and revision 3 -> 4, the acceptor's rules, cross-revision behaviour, the gates and the red-once 2026-09-29 10:19:59 -04:00
swung0x48 05dc94fa38 [Test] (P11 PAIR): PairAcceptorTest - a stray before a client, four interleaved connections, an orphaned aux past the budget and a Hello-first client, each on unix and tcp - and the device's probe-then-client case end to end in ServerSpawnTest 2026-09-29 10:19:59 -04:00
swung0x48 abe29eae2d [Fix] (MG_Remote, P11 PAIR): a socket client's control and aux connections pair by the nonce each presents first, not by arrival order - a readiness probe or a second client can no longer take half a session, and a client that sends its Hello first is refused by name 2026-09-29 10:19:59 -04:00
swung0x48 0a6af63655 [Feat] (P11 PAIR): the PairBind control message - the client's nonce and which of its two connections this is, the first frame on each - with the control revision 3 -> 4 and its pin 2026-09-29 10:19:59 -04:00
swung0x48 32b81dae42 [Docs] (P11): the two dev bugs P11 handed on are fixed on dev (Espryt scattered uploads 9524c688; Magma rd12 command-buffer growth d20c2d01) and arrive with the next dev merge 2026-09-29 09:52:37 -04:00
swung0x48 9c32d9fa68 [Test] (Magma): OversizedRecordingSplitScenario carries state across the split - a depth occluder, a stencil mark, an additive blend destination, a 4x multisample target resolved after it, occlusion and primitives-generated queries and a transform-feedback capture left open across it, each checked on both backends and in the budget-64 lane 2026-09-29 09:44:58 -04:00
swung0x48 4d3d677a7f [Test] (Magma): OversizedRecordingSplitScenario - a 1024-draw frame, one pixel and one uniform value per draw, lands whole on both backends; the DirectVulkan.SplitRecording lane pins the draw budget to 64 and asserts the split fired 2026-09-29 09:44:58 -04:00
swung0x48 d20c2d01ae [Fix] (Magma): a frame's recording is submitted every 16384 draws/dispatches and continues on a fresh command buffer, with at most frames-in-flight + 1 submissions outstanding - rd12's 981k-draw loading frame no longer grows one command buffer until Adreno's 16 KiB driver mappings exhaust vm.max_map_count (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded) 2026-09-29 09:44:58 -04:00
swung0x48 2fcde6eb9b [Docs] (P11): B0-CROSS-APP - SELinux refuses a foreign context the connect to the server app unix endpoint, past a brokered connect the shared segments work unchanged, default placement makes shm slower than tcp; ruling ID-P11-11 on what B1 must build, and the HyperOS uid freeze as a debt 2026-09-29 09:34:26 -04:00
swung0x48 21c7e8cd63 [Test] (MG_IntegrationTest, MG_Test): scattered writes over a GPU clear on both backends - two distant 2x2 boxes, a hundred boxes, boxes filling 3/4 of their union and three quadrants spanning the level, the 3/4 and level-spanning cases again at the image-bindable re-mint, and a MipmapStorage footprint fuzz against a reference bitmap 2026-09-29 09:06:07 -04:00
swung0x48 9524c688ca [Fix] (Espryt, Magma): scattered glTexSubImage writes upload exactly their texels - MipmapStorage keeps an exact dirty footprint beside its rect list (a write log once a merge or a fold would widen the list past the writes), Magma always uploads it, Espryt uploads it box by box (through the unpack ring too) once the driver texture may hold GPU writes and merges it at the image-bindable re-mint; the union box, the 3/4 cut-off, the 96-rect fold and a level-spanning union no longer put the stale shadow over GPU-written texels (two 2x2 writes over a GPU clear: 41 of 64 texels red -> 0) 2026-09-29 09:06:07 -04:00
swung0x48 6481691d1e [Docs] (P11): the status page, the roadmap row and the phase README for P11 in progress, and ruling ID-P11-10 on absorbing origin@86dc38a0 2026-09-29 08:06:38 -04:00
swung0x48 9176dcff3d [Merge] (P11): origin feat/disaggregated@86dc38a0 - its eglSwapInterval port is dev c60639db (already merged) plus ForgetRequestedSwapInterval, so an ended Magma server session no longer hands its interval to the next 2026-09-29 08:05:57 -04:00
swung0x48 26de2b60fc [Docs] (P11): rulings ID-P11-1..9 with the integration gate, A-DEVICE (T0 is GO from the app domain on both backends; adoption buys RSS not frame time on the Adreno; split T2 costs RSS and p99), the debts the dev merge closes and the ones P11 hands on, and W2-REMINT marking the monolith re-mint fixed 2026-09-29 08:03:09 -04:00
swung0x48 82467823ef [Docs] (P11 C): the contract's A1/A2 sections, Config.h's GPU-ordered landing, Init.cpp's DirectVulkan resource table, and the stream refusal CONTRACT-P6 / design 06 / 07 claimed now cites its code 2026-09-29 07:23:41 -04:00
swung0x48 2a902cbd79 [Test] (P11 A1/A2): LargeArenaAdoptionScenario asserts the tier its arena landed in and reads the tcp AdoptTier0 refusal; registered on Split/Spawn/Tcp for both backends (Espryt through the split-arms macro, Magma's Arena. block and Buffers lane), the monolith lanes declare adopted 2026-09-29 07:23:41 -04:00
swung0x48 3c72e5c637 [Fix] (P11 A1): settle MOBILEGL_IPC_ADOPT_TIER at the handshake from the data plane - a stream refuses T0/T1 by name and runs T2 on either side, a client over shared segments dies by name before any record, the at-use check trusts the settlement; unit gates on a real tcp session and a real inproc handshake 2026-09-29 07:23:41 -04:00
swung0x48 1797db0070 [Docs] (P11): B0 drives the cross-app client first as a retrace executable run from adb shell, then re-checks the connection and fd passing from a retrace client inside an app, the domain a Termux-style client really runs in 2026-09-29 07:23:41 -04:00
swung0x48 01a33dfc58 [Docs] (P11): B0 tests the cross-app client as a retrace client in one app against a render server in another - no Termux route; the fallback channel is a bound Service in the server app handing over a socketpair end 2026-09-29 07:23:41 -04:00
swung0x48 93a72678ca [Docs] (P11): B is scheduled - the consumer is a GL program started from another app on the same device (Termux and the like); B0 probes a cross-app fd channel, B1 moves such a client onto shared-memory segments, B2 adds the T0 import 2026-09-29 07:23:41 -04:00
swung0x48 3cb3e32b87 [Test] (P11 M): ImageBindableRemintScenario pins a barrier before its imageLoad copy - the kept-allocation case read the pre-clear upload once on push-monolith under load, the ID-P9-11 shape 2026-09-29 06:47:11 -04:00
swung0x48 89f1e6cfba [Test] (P11 M): P4aFinalFixScenario expects no re-mint count on the monolith arm either - dev's 568090f0 keeps an immutable, unwidened allocation there as the server already did 2026-09-29 06:47:10 -04:00
swung0x48 2c7d99d5e8 [Test] (P11 M): the server half of a split eglSwapInterval - Magma's backend object carries the request into its table's SetSwapInterval (null before dev@c60639db) 2026-09-29 06:33:36 -04:00
swung0x48 9cde08064c [Test] (P11 M): dev's ImageBindableRemint and PartialUploadOverGpuContent scenarios on the Split/Spawn/Tcp arms of both backends, three scattered-upload cases for the handle arm's carried layout, and TextureRemintPullScenario's monolith Espryt skip retired 2026-09-29 06:30:54 -04:00
swung0x48 48a306025d [Fix] (Espryt, P11 M): the push builds' monolith re-mint merges the applier's pending boxes over the GPU readback - the client clears its dirty flags at emission there, so a pending glTexSubImage was read back over and lost 2026-09-29 06:30:54 -04:00
swung0x48 e0102bad8c [Merge] (P11): dev@c60639db - the Espryt image-bindable re-mint reads GPU levels back on the monolith arm too, partial writes into converted or reshaped levels upload only their region on every arm, Magma orders an upload after the recording's writes and queued clears, and Magma's eglSwapInterval reaches the swapchain 2026-09-29 06:15:56 -04:00
swung0x48 ffeed0da3a [Docs] (P11): the re-scoped plan - each roadmap item against the tree at 74ae652e; A makes the adopt tier honest (client-side verdict, the Stream refusal, a tier assertion, three arms) and re-runs open question 3 and the baseline on the device, B (T0) is conditional, C closes by docs 2026-09-29 05:52:51 -04:00
swung0x48 86dc38a07e [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes, and an ended server session forgets its interval (MC 26.3 inproc vsync on: flat 120) 2026-09-29 05:52:22 -04:00
swung0x48 c60639db1d [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes (MC 26.3 vsync on: 190-290 fps -> 120) 2026-09-29 05:51:18 -04:00
swung0x48 74ae652ecd [Docs] (P10): close the phase - CONTRACT-P10 sections 2-3, rulings ID-P10-1..14 with the integration gate, C-MEASUREMENTS (device fence bench, loopback credit 1/2/3, the retrace round-trip census), the roadmap row, the debts P10 hands on, design/09, the phase README and the status page 2026-09-29 05:29:00 -04:00
swung0x48 414dd09bda [Test] (P10): the fence bench comment - the replay corpus does carry timed fence waits (improved-transparency-minecraft-26.3), the bench isolates the shape 2026-09-29 05:29:00 -04:00
swung0x48 47bbf5e6fd [Docs] (P10 A): CONTRACT-P10 section 1 - the fence-report protocol, the present-time report, the gates and red-once - with the re-scoped plan and A-FENCE-POLL, the paired host bench and the llvmpipe finding 2026-09-29 04:34:16 -04:00
swung0x48 73089cf2b3 [Test] (P10 B): the caps mirror's parallel-shader-compile entry is the client's switch in both directions - the extension-list read-back case pins compilation inline, and a new case holds the reconcile (red once with it removed) 2026-09-29 04:34:02 -04:00
swung0x48 80d205c401 [Refactor] (Remote, P10 B): the emit table's SetSwapInterval slot is the server forwarder - SetSwapInterval_Unmigrated and the BackendObject_Remote override deleted, class C empty (69 emitted / 0 unmigrated), and a unit control that the slot crosses as one control frame 2026-09-29 04:34:02 -04:00
swung0x48 4373964443 [Test] (P10 B): AsyncCompileScenario (AsyncOn/AsyncOff/OptimisticShaderStatus), XfbPrimitiveQueryScenario and PrimitivesGeneratedNoXfbScenario on the split arms of both backends - Espryt Split/Spawn/Tcp, Magma Split/Spawn/Tcp gated, PrimGenReroute split+spawn only 2026-09-29 04:34:02 -04:00
swung0x48 2271f6c1ec [Fix] (Remote, P10 B): GL_KHR_parallel_shader_compile follows the client's compile switch - the caps mirror re-derives it on adoption, so a tcp server started with another MOBILEGL_ASYNC_SHADER_COMPILE no longer decides the client's string 2026-09-29 04:34:02 -04:00
swung0x48 8d0a1b6ca5 [Test] (P10 A): PacedFrameFenceWaitsBench - the per-frame timed wait on an older frame's fence as an opt-in measurement (MGITEST_FENCE_PACE_BENCH, _LAG) on both backends 2026-09-29 04:29:49 -04:00
swung0x48 625f4db47d [Feat] (MG_Remote, P10 A): a present reports the fences finished so far before it returns the credit, so the client the credit releases finds the fence of a frame ago already known 2026-09-29 04:29:49 -04:00
swung0x48 b1d60523e4 [Fix] (Magma): an upload into an image the open recording already wrote, or that still has a queued clear, is submitted after that work - a glTexSubImage after a clear or a draw into the level no longer ends up under it 2026-09-29 04:28:55 -04:00
swung0x48 8966d5c294 [Fix] (Espryt, Magma): a partial write into a level whose upload is converted (widened RGB formats, Magma's RGB->RGBA expansion) or reshaped (Espryt 1D arrays) uploads only its dirty region - the rest of the stale shadow no longer lands over GPU-drawn texels 2026-09-29 04:28:22 -04:00
swung0x48 bcff9b00cc [Feat] (MG_Remote): fence polls are answered from the server's reports - the apply thread posts kEventFenceSignaled as the GPU finishes each client fence (and submits and re-checks while idle), glClientWaitSync(0) / glGetSynciv answer locally, and only every MOBILEGL_IPC_POLL_ESCALATE-th unanswered poll crosses 2026-09-29 03:59:44 -04:00
swung0x48 de33d40173 [Docs] (P9): close the phase - the contract's remint section and invariants, rulings ID-P9-11..12 with the integration gate, the roadmap row, the debts P9 hands on, the phase README and the status page 2026-09-29 03:36:45 -04:00
swung0x48 7aa00761f8 [Docs] (P9 W2): W2-REMINT - no pull protocol, the census, the coverage matrix and the red-once record; design/04 8.4, the P5c/P5e contract amendments, open question 2 and the P7 census attribution 2026-09-29 03:24:03 -04:00
swung0x48 aa0c242b51 [Test] (P9 W2): TextureRemintPullScenario - fourteen re-mint cases on both backends x monolith/Split/Spawn/Tcp, and the forced-readback-failure fallback entries 2026-09-29 03:23:54 -04:00
swung0x48 6c2f8a560c [Fix] (Espryt, P9 W2): a texture re-mint never needs the client - read back only levels the driver holds, replay the staged level when the readback is refused, count trp on the server path, and retire the texture-remint-pull markers and OnTexturePullRequest (callbacks 9 -> 8) 2026-09-29 03:23:54 -04:00
swung0x48 568090f09f [Fix] (Espryt): the image-bindable re-mint reads every level back from the driver texture before replaying it, so GPU-written levels survive the first glBindImageTexture - iris-derivative SSIM 0.996196552 -> 0.996376198 2026-09-29 03:06:45 -04:00
swung0x48 738b29f3e4 [Docs] (P9): the contract's first three sections (pack-buffer readback, graded log forwarding, the glGetError observation point), both fault injections, the W1 note with its red-once evidence, and rulings ID-P9-1..10 2026-09-29 03:03:03 -04:00
swung0x48 a7a82ffee5 [Test] (MG_Remote): P9 W3 - LogForwardTest leaves no role logs in /tmp on a green run 2026-09-29 02:42:20 -04:00
swung0x48 24560ffe81 [Fix] (MG_Remote): P9 W3 - a FATAL forward waits once per stall rather than once per line (MGLOG_F also marks survivable conditions), and F2 proves the flood got through on a live connection, inside TCP_USER_TIMEOUT 2026-09-29 02:42:20 -04:00
swung0x48 6bcfb1fba1 [Feat] (MG_Remote): P9 W3 - the server's log forward carries each line's level and never blocks a logging thread (<=WARN dropped and counted, >=ERROR lossless and paced, FATAL a bounded flush), fault injection F2, and the producerless OnLog callback deleted (9 -> 8) 2026-09-29 02:42:20 -04:00
swung0x48 362daafeed [Fix] (MG_Remote): the pack-buffer readbacks check their whole shape before the read and refuse it at one latch site per sink, each with a raw-peer control, and a verb counts as statically barriered only when every op that stamps it waits 2026-09-29 02:21:36 -04:00
swung0x48 6485ae1dc0 [Feat] (MG_Remote): a read into a pixel pack buffer crosses as read_pixels_to_buffer / get_texture_image_to_buffer and waits for nothing - the server lands the rows through the buffer-write gate on both backends, the client marks the buffer GPU-written, and a device lost with the write still owed ends in Fatal{ReadbackDeclined, "buffer-writeback"} 2026-09-29 02:02:29 -04:00
swung0x48 ad705a7392 [Docs] (P9): the implementation hand-off - the roadmap row checked against the tree (half of it is done, one design premise changed), five work packages and two fault injections 2026-09-29 01:23:58 -04:00
swung0x48 acfd367852 [Docs] (P12): the FCL fork's keep-running setting is committed and pushed 2026-09-29 01:10:19 -04:00
swung0x48 743f4e8e6e [Docs] (P12): close the phase - the contract, the disposition of every leftover item, a G1 line that only compares same-machine builds, and the doc citation the docs reshuffle broke 2026-09-29 01:05:45 -04:00
swung0x48 6e35ea2c02 [Docs] (P12): the portrait-window clipping was the 0x0 window request, not Magma - root cause, fix and evidence 2026-09-29 00:54:46 -04:00
swung0x48 c932af7f3c [Fix] (EGLImpl): a server-owned window surface with no EGL_WIDTH/HEIGHT asks for the native window's size, so a landscape game is no longer clipped in a portrait server window 2026-09-29 00:54:46 -04:00
swung0x48 8a71ea5954 [Docs] (P12): exit gate (a) passes - FCL's Minecraft renders into the render server window on both backends and killing the server latches device-lost in tens of milliseconds 2026-09-29 00:27:29 -04:00
swung0x48 984e2b69f4 [Feat] (tools): the FCL exit-gate driver - Minecraft renders into the render server window, then the server is killed 2026-09-29 00:27:28 -04:00
swung0x48 f2ad2fae9b [Fix] (MG_Remote): once the device-lost latch has spoken, declined verbs no longer log one ERROR line each - an FCL game wrote 113,080 of them in 48 s 2026-09-29 00:27:28 -04:00
swung0x48 27cbc53241 [Docs] (P12): exit gate (b) passes - a WSL client replays the in-world trace over Wi-Fi TCP at ssim 0.999883; the load frame is bandwidth-bound, the steady state is latency-bound 2026-09-28 23:22:06 -04:00
swung0x48 b60da6729d [Feat] (tools): the cross-host acceptance driver and its report reducer, and a note that netbench's nc sink understates the link 2026-09-28 23:22:05 -04:00
swung0x48 b4f0c53426 [Docs] (Disaggregated): the cross-host eglMakeCurrent hang was the host's xray TUN proxy stalling its relay, not MobileGL; direct Wi-Fi runs pass and are bandwidth-bound 2026-09-28 22:57:20 -04:00
swung0x48 429b4472bb [Fix] (tools): cross-host runs first prove the WSL -> phone TCP path is end to end, and netbench's pkill no longer kills its own shell 2026-09-28 22:57:20 -04:00
swung0x48 221a8144c2 [Docs] (Disaggregated): sync stage progress and roadmap with the five-arm perf comparison and the cross-host hang blocker 2026-09-28 22:33:53 -04:00
swung0x48 1987bdff5c [Feat] (Android): the render server screen and binary ship in the plugin APK too, not just trace 2026-09-28 22:10:56 -04:00
swung0x48 2fa58940e4 [Fix] (ci): the trace APK is a launchable app; assert exactly one launcher activity
The "Trace APK must not expose a launcher activity" check predates the render-server
screen: 1f72597dd gave the trace APK a launcher on purpose and 884f6aac9 moved the entry
icon back to PostActivity with a bottom navigation bar between the two screens, so the
apk.yml build job has been red ever since - taking the whole retrace matrix down with it
(skipped, not failed), which hid the distinction.

What is left of the old isolation contract is the count: exactly one
android.intent.action.MAIN in the merged manifest (a second one means a screen grew a
launcher entry by accident), and it belongs to PostActivity. The fclPlugin metadata ban is
unchanged.
2026-09-28 21:54:16 -04:00
swung0x48 35dddc0c89 [Fix] (MG_Remote): the socket-io stats are LinkMetrics free functions, not StreamLink members
6bccf98d7 put TakeSendStats/TakeReadStats on the concrete StreamLink class, so
ClientSession.cpp and ServerLoop.cpp had to include <MG_Remote/Transport/StreamLink.h> -
which the P6.5 seam gate (scripts/ci/link_seam_purity.py, "concrete link included above
Transport") rejects, and the MGPipe generators and hygiene gates job has been red since.

The counters stay in StreamLink.cpp beside the recv/sendmsg loops that feed them; only the
struct and the two Take functions move, to LinkMetrics.h as LinkIoStats +
LinkMetricsTakeReadStats/TakeSendStats. The readers drop the concrete include; the hot
loops are untouched.
2026-09-28 21:54:08 -04:00
swung0x48 3bdd565dc6 [Chore] (docs): Remove orphaned CS_Refactor notes accidentally committed in 4e48702f2 2026-09-28 21:16:35 -04:00
swung0x48 884f6aac96 [Feat] (Android): trace APK boots to POST; bottom nav bar switches to the render server
The trace flavor manifest removed PostActivity and made ServerControlActivity
the launcher, so the APK opened straight into the render server screen. Keep
both screens: PostActivity is the launcher again, and a bottom navigation bar
(NavBar, plugin flavor unaffected) switches between them via
FLAG_ACTIVITY_REORDER_TO_FRONT with transitions suppressed, so each screen
keeps its state. ServerControlActivity is singleTop now and handles extras in
onNewIntent so adb-driven starts still work. The POST screen also shows an
indeterminate progress bar while the self-test runs.
2026-09-28 21:06:44 -04:00
swung0x48 b9a89967b0 [Docs] (Disaggregated): five-arm transport perf comparison and bottleneck attribution
rd12/openra on-device matrix (monolith/inproc/spawn+shm/spawn+tcp),
tcp-localhost transport_wait decomposition (present credit=1 ~74% +
futex wakeup tax; SPIN_US=2000 alone 2.9x), cross-host control-plane
hang evidence chain blocking P12 acceptance B, Wi-Fi bandwidth ceiling,
and the prioritized knob/code optimization list.
2026-09-28 20:55:45 -04:00
swung0x48 141227e8ba [Feat] (tools): add the disaggregated-mode device perf matrix and profiling harness
run_matrix.sh drives interleaved monolith/inproc/spawn+shm/spawn+tcp
benchmark rounds on an adb device with per-run arm-marker verification;
profile_matrix.sh layers simpleperf, ftrace wakeup capture and a /proc
sampler on top; make_report.py aggregates runs into REPORT.md. The wifi
kit carries the cross-host control-plane hang repro and phone-side
strace/fd-reconcile helpers. .trace-work/ (run output) is now ignored.
2026-09-28 19:36:30 -04:00
swung0x48 930c29e9e0 [Feat] (Android): the render server screen takes intent extras for prefill and autostart
listen/token/env/onscreen prefill the form, --ez start true starts the
server, --ez stop true stops it - adb drives the screen without the IME,
which is how the on-screen unix path (@name + shm, ServerOwned window)
was verified end to end from the UI.
2026-09-27 08:36:57 -04:00
swung0x48 c16fb5a8ac [Feat] (MG_Remote): the in-process display server serves unix endpoints too
mobilegl_server_serve_inprocess refused anything but tcp:// because its
only supervisor was the TCP one's thread hand-off. A unix endpoint
(@abstract name or filesystem path) now runs a supervisor of the same
shape minus the TCP exposure machinery: AcceptPair pairs a client's
control and SCM_RIGHTS connections by arrival order, there is no
DataBind to route and no pre-auth gate, sessions run on threads one at
a time with Busy for a second client, the latch resets between sessions
and g_inProcessStop ends it. That gives the on-screen server the
on-device fast path: ServerOwned windows over a unix socket with shm
data, no TCP copies.

The control screen's tcp-only guard for the on-screen mode goes with
the refusal it mirrored.
2026-09-27 07:43:56 -04:00
swung0x48 e3e890ed5d [Feat] (Trace): the replay runner builds as an adb-shell executable for Android, and unix endpoints carry a headless window
MOBILEGL_BUILD_TRACE_REPLAY now also produces mobilegl_trace_replay on
ANDROID: the same CLI as desktop, linked against the APK runner's
glws/glproc glue. On the device it dials a render server with no
Activity at all - MOBILEGL_IPC_CONTROL=unix:@name with shm data to the
offscreen supervisor (a cross-uid abstract connect needs root or an
SELinux waiver; loopback TCP works from plain adb shell), or
tcp:// with MOBILEGL_IPC_SURFACE=server to the on-screen display server.

glws_android grows the desktop glws_egl's server-owned null-window
createWindowSurface, gated on MOBILEGL_TRACE_SURFACE=window so the APK's
pbuffer mode keeps its old answer, and the config carries EGL_WINDOW_BIT
for it. The control screen refuses unix endpoints in on-screen mode:
the in-process display server serves tcp:// only.

Verified on-device with an OpenRA retrace, ssim=1.0 both ways:
offscreen service on @mgl-cli with the CLI as root, and the display
Activity on tcp://127.0.0.1:40613 with the replay's frames on the
phone's screen.
2026-09-27 06:28:00 -04:00
swung0x48 bf6feebb22 [Perf] (MG_Remote): The producer's transport park is timed as its own per-frame number, and a reducer pairs the two role logs into a frame-time breakdown
transport_wait_ns measures wall time in Ring::Wait's flush/spin/sleep, so
the client's parked time is read directly instead of inferred by
subtracting CPU clocks that run concurrently with the server.
frame_stats.py matches client and server present serials into per-frame
avg/p50/p99 splits; the README documents the MOBILEGL_PIPE_STATS=1
workflow.
2026-09-27 04:45:07 -04:00
swung0x48 f10db410e8 [Feat] (Android): the render server screen also starts the on-screen display server
An On-screen window checkbox switches Start from the offscreen service
to MobileGLDisplayActivity (process :mglwin), which serves in its own
process and renders a client's window surface onto its SurfaceView.
Stop ends either shape: stopService for the supervisor, killProcess for
:mglwin, and the running check covers both. Verified end to end with an
OpenRA retrace: the client dialed tcp://127.0.0.1:40613 with
MOBILEGL_IPC_SURFACE=server from a freeform window while the display
Activity held the screen, frames landed on the SurfaceView and the run
passed with ssim=1.0.
2026-09-27 04:39:01 -04:00
swung0x48 aaa50c88e1 [Feat] (Android): the render server screen takes unix endpoints, and is titled for what it serves
The endpoint field accepts @abstract names and filesystem paths beside
tcp://, matching what mobilegl_server_main's Listen already binds - on
Android the abstract namespace is the on-device rendezvous that always
works, and a relative path lands in the app's files dir, the service's
working directory. Unix endpoints skip the token rule (same-device
reach by construction) and are probed with LocalSocket instead of a
TCP connect. The screen is now titled MobileGL Render Server.
2026-09-27 04:39:00 -04:00
swung0x48 1f72597dd8 [Feat] (Android): a launcher screen starts and stops the offscreen TCP server
ServerControlActivity (trace flavor) collects the listen endpoint, auth
token and extra env, then drives MobileGLServerService with the same
extras adb used. Non-loopback listens are held to the 16-byte token the
server enforces, and a connect probe confirms the supervisor bound its
socket. The screen is the APK's launcher icon, which the trace flavor
lacked since PostActivity is removed there.
2026-09-27 04:39:00 -04:00
350 changed files with 43021 additions and 2078 deletions
+27 -2
View File
@@ -61,15 +61,40 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
exit 1
fi
# The render server is not trace-only: both flavors carry the control screen, the offscreen
# service, the on-screen display activity, and (split native builds) the supervisor executable.
# The plugin APK ships to users, so its build keeps the three components private (manifest
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
require "$component" "$plugin_manifest" "plugin render server component ($component)"
require "$component" "$trace_manifest" "trace render server component ($component)"
done
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
trace_contents=$(unzip -Z1 "$trace_apk")
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
exit 1
fi
done
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
# entry icon and the bottom navigation bar switches to the render server screen. What is
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
# reds here instead of shipping two icons.
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
if [[ "$main_count" -ne 1 ]]; then
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
exit 1
fi
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+29
View File
@@ -1461,6 +1461,22 @@ jobs:
- name: Spawn/split lane parity (9.2)
run: python3 scripts/ci/spawn_lane_parity.py build-split
# P8-A (ID-P8-2): every monolith case, tails normalised, is on a gated split arm or is named
# with a reason class in scripts/data/split_coverage_exemptions.txt. The same `ctest -N`
# cost as the parity step above, and the question it cannot answer: parity compares the
# split arms with EACH OTHER, so a case missing from all three passes it.
- name: Split coverage against monolith (P8-A)
run: |
python3 scripts/ci/split_coverage.py --self-test
python3 scripts/ci/split_coverage.py build-split
# P8-SE: PipeCatalogueTest's unmigrated-emulation list equals the MGPipeUnmigratedEmulation
# call sites in the tree, one site per name. A source lint: no build needed.
- name: Unmigrated-emulation list against its call sites (P8-SE)
run: |
python3 scripts/ci/unmigrated_emulation_sites.py --self-test
python3 scripts/ci/unmigrated_emulation_sites.py
- name: Unit tests on the split runtime
working-directory: build-split
env:
@@ -2042,6 +2058,13 @@ jobs:
"PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding",
# P11 A2: the >= 16 MiB arena and its tier assertion (emulated).
"LargeArenaAdoptionScenario.SubDataAfterAnInFlightDrawReachesTheNextDraw",
"LargeArenaAdoptionScenario.RespecifiedVertexArenaKeepsVaoBindings",
"LargeArenaAdoptionScenario.RespecifiedIndexArenaKeepsVaoBinding",
"LargeArenaAdoptionScenario.ReadbackSeesTheLatestCpuWrite",
"LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack",
"LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares",
]
slp.require_green(json.load(open(sys.argv[2])), sys.argv[3],
["DirectVulkan.Split.Buffers." + name for name in cases])
@@ -2104,6 +2127,12 @@ jobs:
"PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding",
"LargeArenaAdoptionScenario.SubDataAfterAnInFlightDrawReachesTheNextDraw",
"LargeArenaAdoptionScenario.RespecifiedVertexArenaKeepsVaoBindings",
"LargeArenaAdoptionScenario.RespecifiedIndexArenaKeepsVaoBinding",
"LargeArenaAdoptionScenario.ReadbackSeesTheLatestCpuWrite",
"LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack",
"LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares",
]
required = [sys.argv[4] + name for name in cases]
if sys.argv[5] == "tcp":
+1
View File
@@ -28,3 +28,4 @@ tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
/.trace-work/
+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 */
+171 -1
View File
@@ -29,6 +29,8 @@ option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-pr
# only decides whether the shims are compiled in.
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
option(MOBILEGL_BUILD_EXTMEM_PROBE "Package tools/spikes/extmem_probe as libMobileGLExtmemProbe.so for the trace APK's exec hook (P11 A3, Android only)" OFF)
option(MOBILEGL_EXTMEM_PROBE_AS_SERVER "Spike only (P11 B2 step 1): also package the probe as libMobileGLServer.so, the real server as libMobileGLServerReal.so" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
@@ -401,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
@@ -432,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
@@ -607,9 +616,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
MobileGL/MG_Remote/Transport/SocketTransport.cpp
MobileGL/MG_Remote/Transport/ControlInbox.cpp
# P9 W3: the server's log forward - a queue and a sender thread, graded by severity
# (<= WARN lossy, >= ERROR lossless and paced), so no logging thread writes the socket.
MobileGL/MG_Remote/Transport/LogForward.cpp
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
# P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by
# both roles (so SERVER-partition code in the link ratchet, not the client's tracker).
MobileGL/MG_Remote/Transport/AdoptTier.cpp
# P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles).
MobileGL/MG_Remote/Transport/AdoptT0.cpp
# ---- P5: the three new directories ------------------------------
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
# var-tails in and out of SEG_STAGE. [w1]
@@ -630,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
@@ -650,6 +668,11 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
MobileGL/MG_Remote/Server/PipeApplier.cpp
MobileGL/MG_Remote/Server/ServerLoop.cpp
MobileGL/MG_Remote/Server/ServerMain.cpp
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
MobileGL/MG_Remote/Server/PairAcceptor.cpp
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
MobileGL/MG_Remote/Server/AdoptInbox.cpp
MobileGL/MG_Remote/Server/ServerSpawn.cpp
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
@@ -682,7 +705,7 @@ if (ANDROID)
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
# P12 (on-screen server window): the APK's MobileGLDisplayActivity - the in-process
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
list(APPEND SOURCE_FILES
@@ -763,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
@@ -800,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}
@@ -866,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)
@@ -873,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
@@ -982,6 +1094,12 @@ if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# The replay runner also builds for Android: an adb-shell executable that can
# dial a render server on the device (unix or TCP) without any Activity.
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
endif()
# ---------------------------------------------------------------------------
# P0 spike A: the Android delivery chain for a second native executable.
#
@@ -1090,3 +1208,55 @@ if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
# P11 A3 (open question 3): the spike-B external-memory probe, exec'd from the trace
# app's own process by the spike hook (`--es mobilegl_spike_spawn
# libMobileGLExtmemProbe.so`), so its rows answer for the untrusted_app domain. The
# same PIE-executable-named-like-a-library shape as MobileGLServerSpike above; it
# links nothing from MobileGL.
add_executable(MobileGLExtmemProbe
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbe PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLExtmemProbe"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbe PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbe PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbe PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbe PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
if (MOBILEGL_EXTMEM_PROBE_AS_SERVER)
# P11 B2 step 1, spike APKs only: MobileGLServerService execs `libMobileGLServer.so
# <endpoint> --serve` and B1's broker connects to whatever that listens on, so the probe
# takes the server's file name and answers "does a client's AHardwareBuffer cross this
# route into the server app" on the real route (tools/spikes/extmem_probe, route mode).
if (TARGET MobileGLServer)
set_target_properties(MobileGLServer PROPERTIES OUTPUT_NAME "MobileGLServerReal")
endif()
add_executable(MobileGLExtmemProbeAsServer
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServer"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbeAsServer PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbeAsServer PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbeAsServer PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
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()
+58 -8
View File
@@ -132,6 +132,18 @@ namespace MobileGL::MG_Config {
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES: how many consecutive frames (strictly, BeginFrame
// epochs) a frame slot's descriptor pools must stay under a quarter full before Magma
// hands the slot's whole set cache back and lets it re-grow. 0 = never trim. A loading
// frame otherwise keeps its burst of sets (rd12: ~15k, where steady frames use a few dozen)
// for the life of the process. See UniformManager::TrimFrameDescriptorPools.
Uint32 MagmaDescriptorTrimFrames = 120;
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool EsprytAvoidSamplerMipmapMinFilter = false;
@@ -202,9 +214,12 @@ namespace MobileGL::MG_Config {
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
// tore the frames still reading the old bytes) - still no driver-mediated write
// into the busy mutable store, which on this class of Mali driver stalls the
// thread or ghost-copies the whole arena, and the arena stops costing its size
// again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
@@ -484,7 +499,11 @@ 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.
inline constexpr Uint32 kAdoptTierUnset = 3;
struct IpcTable {
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
@@ -595,11 +614,33 @@ namespace MobileGL::MG_Config {
// flag keyed on wantReply would disable the very mechanism it exists to enable
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
Uint32 CreateWindow = 10;
// MOBILEGL_IPC_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
// negative control has a spelling the day P11 writes it.
Uint32 AdoptTier = 2;
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
// nothing, and marks the buffer GPU-written. The 1 arm is the A/B control and the
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
// wait per read.
Uint32 PboReadbackSync = 0;
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
// arm and the red-once of the no-reply gate (FencePollScenario).
Uint32 PollEscalate = 64;
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
Uint32 AdoptTier = kAdoptTierUnset;
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
Uint32 AllowAdoptT0 = 1;
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
@@ -689,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
+29 -13
View File
@@ -230,6 +230,9 @@ namespace MobileGL::MG_ConfigLoader {
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
@@ -421,14 +424,21 @@ namespace MobileGL::MG_ConfigLoader {
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
// StreamLink's crossing log are the checks on this number.
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
// shadow comparer is armed.
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
// point of use, which is where the "P11" in the message belongs.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
@@ -484,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");
@@ -495,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;
}
@@ -505,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");
}
}
@@ -522,11 +536,13 @@ namespace MobileGL::MG_ConfigLoader {
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB create-window=%u "
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.AdoptTier,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
ipc.AllowAdoptT0,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
+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;
+535 -145
View File
@@ -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;
@@ -483,11 +487,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
if (!MG_Pipe::MGPipeHandleIsNull(handle)) {
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
const Uint8* const base = resource ? resource->hostBytes : nullptr;
const Uint8* base = resource ? BufferImpl::SplitHostBytes(*resource) : nullptr;
// P8-C: a store nothing ever staged but a shader wrote (the GPU-culling shape)
// takes its first copy from this server's own GL buffer. A store that HAS a copy
// is not refreshed here: most indirect draws execute natively and read no field
// on the CPU, so ExecuteIndexed/ArraysIndirectCommands refresh the fields only
// when they consume them.
if (base == nullptr && resource != nullptr) {
base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_bytes");
}
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (base == nullptr) {
MGLOG_E_ONCE("%s skipped: the verb's indirect buffer {%u, %u} has no staged "
"shadow on this side (GPU-written indirect commands are P8's)",
"shadow on this side and no GPU write to take one from",
label, handle.Slot, handle.Gen);
return nullptr;
}
@@ -769,6 +781,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
entry.Res.Gen);
continue;
}
// P8-C: the SERVER's half of the mark MarkShaderStorageBuffersGpuWritten no longer
// makes under a transport - on this side's twin, for this side's CPU readers
// (SplitHostBytesForCpuRead). Every bound storage point, which is the client's
// own conservative set (ShaderBufferEmit.h marks every one writable); a uniform
// point is read-only by definition.
if (shaderBufferClass != MG_Pipe::kMGPipeShaderBufferClassUniform) {
backendResource->serverGpuWritten = true;
}
const auto backendBufferId = backendResource->id;
if (entry.Offset == 0 && entry.Size == MG_Pipe::kMGPipeWholeBuffer) {
BindBufferBaseCached(glTarget, static_cast<GLuint>(i), backendBufferId);
@@ -997,6 +1017,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// from MarkGpuWritesForDraw, WIDER on purpose - it marks every touched
// counter point rather than only the ones this program declares, which is
// the safe direction for an over-approximate set.
//
// P8-C: the server's own twin is marked instead, for the server's own CPU
// readers (SplitHostBytesForCpuRead) - a counter buffer later bound as a
// parameter or element buffer is read on this side.
backendResource->serverGpuWritten = true;
}
return;
}
@@ -1284,6 +1309,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
ProcessDeferredT0Retires(); // P11 B2
#endif
// All buffers we need are:
// 1.VBO 2.IBO (if needed) 3.UBO 4.IndirectBuffer (if needed)
@@ -1531,6 +1559,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
ProcessDeferredT0Retires(); // P11 B2
#endif
// THE COMPUTE PATH IS THE ONE THAT NEEDS THE FRONTEND-INDEXED UNIFORM PASS, and it
// is why the uniform class is emitted at all: compute does NOT go through the
// per-program block remap in BindCurrentProgramWithResources, so these are the
@@ -1797,7 +1828,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const MG_Pipe::MGPipeHandle res = target.handle;
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
if (resource == nullptr || resource->persistentMapped) continue;
if (resource == nullptr) continue;
if (resource->persistentMapped) {
// P8-C: nothing to copy - the capture reaches a coherent map -
// but this side's CPU readers must finish before reading it.
resource->serverGpuWritten = true;
continue;
}
const SizeT size = target.end - target.start;
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, target.backendId);
void* mapped = g_GLESFuncs.glMapBufferRange(BufferImpl::TempBufferTarget,
@@ -1838,6 +1875,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
"captured bytes of buffer {%u,%u} cannot reach the client shadow",
res.Slot, res.Gen);
}
// P8-C: the server's own staged copy takes the same bytes, from the
// same mapping, so this side's CPU readers see the capture too.
BufferImpl::StageServerWrittenRange(*resource, res, mapped, target.start, size);
g_GLESFuncs.glUnmapBuffer(BufferImpl::TempBufferTarget);
// Ops_H_Readback's order: writeback, unmap, THEN the serial
// stamp, so the next draw does not re-upload the server's stale
@@ -1998,7 +2038,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
splitRes.Slot, splitRes.Gen);
continue;
}
const Uint8* hostBytes = splitResource->hostBytes;
// P8-C: made current first - a store a shader wrote here is refreshed.
const Uint8* hostBytes = BufferImpl::SplitHostBytesForCpuRead(*splitResource, splitRes,
"xfb_scatter_pre_capture");
// A NULL SHADOW IS NOT A LOST CAPTURE. It is the ORPHANED store
// (glBufferData(size, NULL) with no resource_subdata since), which
// M-3 declares legal: every byte the application has not staged is
@@ -2063,9 +2105,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
"scattered capture of buffer {%u,%u} cannot reach the client shadow",
splitRes.Slot, splitRes.Gen);
}
// Ops_H_Readback's stamp, for its reason: the GL store now holds
// bytes the server's staged shadow does not, and the next draw must
// not re-upload the stale shadow over the capture.
// P8-C: the staged copy takes the composed range too, so this side's CPU
// readers see the capture (before P8 it kept the pre-capture bytes).
BufferImpl::StageServerWrittenRange(*splitResource, splitRes, staged.data(), target.start,
rangeBytes);
// Ops_H_Readback's stamp, for its reason: the GL store now holds the
// capture, and the next draw must not re-upload an older range over it.
splitResource->syncedChangeSerial = BufferImpl::ResourceSerialForHandle(splitRes);
} else
#endif
@@ -4345,12 +4390,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
const auto textureFormat = static_cast<TextureInternalFormat>(resource->Desc.InternalFormat);
const TextureTarget textureTarget = PipeTextureTargetForHandle(view.Res);
const GLenum access = GLAccessForImageView(view);
// Before the sync, for the frontend arm's reason: the note may adopt a twin, and
// `backendTexture` below is a reference into the same registry.
if (access != GL_READ_ONLY) {
NoteDriverSideTextureWriteByHandle(view.Res);
}
auto& backendTexture = SyncTextureToBackendByHandle(view.Res, /*imageBindableStorageRequired=*/true);
if (!backendTexture) return;
IssueImageTextureBind(unit, *backendTexture, textureTarget, textureFormat,
static_cast<GLenum>(view.InternalFormat), static_cast<GLint>(view.Level),
view.Layered != 0, static_cast<GLint>(view.Layer),
GLAccessForImageView(view));
view.Layered != 0, static_cast<GLint>(view.Layer), access);
}
#endif // MOBILEGL_PIPE_PUSH
@@ -4375,6 +4425,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
// Before the sync: `backendTexture` below is a reference into the registry, which the
// lookup inside the note may reshuffle.
if (imageBinding.Access != GL_READ_ONLY) {
NoteDriverSideTextureWrite(imageBinding.Texture);
}
auto& backendTexture = SyncTextureObjectToBackend(imageBinding.Texture, true);
if (!backendTexture) return;
IssueImageTextureBind(unit, *backendTexture, imageBinding.Texture->GetTarget(),
@@ -4409,7 +4464,26 @@ namespace MobileGL::MG_Backend::DirectGLES {
// reach of an apply thread entirely.
void MarkWritableImageBufferTexturesGpuWritten() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// P8-C: the SERVER's twin of the buffer behind each writable buffer image, for
// this side's CPU readers (SplitHostBytesForCpuRead). Every read is a record:
// the unit set SyncImageTextureBinding(view) tracked, the applier's image view,
// and the texture descriptor's backing-store handle.
if (g_writableImageBufferUnitCount == 0) return;
const auto& st = MG_Pipe::MGPipeApplier();
for (Uint unit = 0; unit < g_writableImageBufferUnits.size() && unit < st.BoundShaderImages.size();
++unit) {
if (!g_writableImageBufferUnits[unit]) continue;
const MG_Pipe::MGPImageView& view = st.BoundShaderImages[unit];
if (!IsWritableImageBufferView(view)) continue;
const auto* record = PipeTextureRecordForHandle(view.Res);
if (record == nullptr) continue;
if (auto* buffer = BufferImpl::FindBufferResourceForHandle(record->Desc.BufferForTexBuffer)) {
buffer->serverGpuWritten = true;
}
}
return;
}
#endif
if (g_writableImageBufferUnitCount == 0) return;
for (Uint unit = 0; unit < g_writableImageBufferUnits.size(); ++unit) {
@@ -6445,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;
}
@@ -6514,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);
}
}
@@ -6534,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;
}
@@ -6575,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;
}
@@ -8475,6 +8549,30 @@ namespace MobileGL::MG_Backend::DirectGLES {
return g_GLESCapabilities.SupportsDrawIndirect;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-C: the command bytes an indirect executor is about to CONSUME on the CPU, made current.
// Consumed means: every field on the CPU path (no native indirect), or on the native path the
// uniforms fed from the command - gl_BaseVertex whenever the program reads it, and
// gl_BaseInstance when the program reads it without the mg_IndirectParams storage view that
// would give it the GPU's word. A native draw that feeds neither reads nothing and keeps its
// pointer, so a compute-culled indirect buffer drawn every frame costs no readback.
static const Uint8* CurrentIndirectCommandBytes(MG_Pipe::MGPipeHandle handle, const Uint8* commandBytes,
SizeT commandOffset, Bool useNative, Bool indexed) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return commandBytes;
if (useNative) {
const auto program = GetCurrentBackendProgram();
if (program == nullptr) return commandBytes;
const Bool baseVertexFromCpu = indexed && program->ReadsBaseVertex();
const Bool baseInstanceFromCpu = program->GetIndirectParamsBinding() < 0 && program->ReadsBaseInstance();
if (!baseVertexFromCpu && !baseInstanceFromCpu) return commandBytes;
}
auto* resource = BufferImpl::FindBufferResourceForHandle(handle);
if (resource == nullptr) return commandBytes;
const Uint8* base = BufferImpl::SplitHostBytesForCpuRead(*resource, handle, "indirect_command_fields");
return base != nullptr ? base + commandOffset : commandBytes;
}
#endif
// Runs an (indexed) indirect multi-draw. When a GL_DRAW_INDIRECT_BUFFER is bound the draws
// execute natively on the GPU so commands written by compute shaders (e.g. Flywheel's
// culling pipeline updating instanceCount) are honored; the CPU shadow is still consulted
@@ -8521,6 +8619,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
:
#endif
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
#if MOBILEGL_BUILD_DISAGGREGATED
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
/*indexed=*/true);
#endif
if (useNative) {
// gl_BaseInstance must observe GPU-written command fields; expose the indirect
// buffer to the program's mg_IndirectParams SSBO view and address it per draw.
@@ -8619,6 +8721,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
:
#endif
drawIndirectBuffer != nullptr && SupportsNativeIndirectDraws();
#if MOBILEGL_BUILD_DISAGGREGATED
commandBytes = CurrentIndirectCommandBytes(verbBufferHandle, commandBytes, commandOffset, useNative,
/*indexed=*/false);
#endif
if (useNative) {
const auto backendProgram = GetCurrentBackendProgram();
const Int paramsBinding = backendProgram ? backendProgram->GetIndirectParamsBinding() : -1;
@@ -9203,9 +9309,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
return;
}
// The staged shadow is the source of truth for CPU reads on this side: a
// persistent map's blocks and a shader write's writeback were consumed by the
// applier before this verb ran.
const Uint8* hostBytes = elementResource ? elementResource->hostBytes : nullptr;
// persistent map's blocks were consumed by the applier before this verb ran, and
// (P8-C) a store a shader wrote on this side is read back from this server's own
// GL buffer first - no writeback ever reaches the server's copy.
const Uint8* hostBytes =
elementResource
? BufferImpl::SplitHostBytesForCpuRead(*elementResource, elements,
"primitive_restart_substitution")
: nullptr;
if (hostBytes == nullptr) {
MGLOG_E_ONCE("Draw skipped: GL_PRIMITIVE_RESTART with restart index %u needs a CPU-readable copy of "
"the bound element array buffer and none is available.",
@@ -9522,8 +9633,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
auto* paramResource =
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
// (a store a shader wrote on this side is read back); the command buffer only gets a
// first copy when it has none - its fields are made current by the executor, and
// only when it consumes them (CurrentIndirectCommandBytes).
const Uint8* const parameterBytes =
paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource,
applierState.VerbIndirectParameterBuffer,
"indirect_count_parameter")
: nullptr;
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
if (drawBytes == nullptr && drawResource != nullptr) {
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
"indirect_count_commands");
}
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
MGLOG_E_ONCE("MultiDrawElementsIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
@@ -9676,8 +9799,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* drawResource = BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectBuffer);
auto* paramResource =
BufferImpl::FindBufferResourceForHandle(applierState.VerbIndirectParameterBuffer);
const Uint8* const drawBytes = drawResource ? drawResource->hostBytes : nullptr;
const Uint8* const parameterBytes = paramResource ? paramResource->hostBytes : nullptr;
// P8-C: the count is CONSUMED here, so the parameter buffer is made current first
// (a store a shader wrote on this side is read back); the command buffer only gets a
// first copy when it has none - its fields are made current by the executor, and
// only when it consumes them (CurrentIndirectCommandBytes).
const Uint8* const parameterBytes =
paramResource ? BufferImpl::SplitHostBytesForCpuRead(*paramResource,
applierState.VerbIndirectParameterBuffer,
"indirect_count_parameter")
: nullptr;
const Uint8* drawBytes = drawResource ? BufferImpl::SplitHostBytes(*drawResource) : nullptr;
if (drawBytes == nullptr && drawResource != nullptr) {
drawBytes = BufferImpl::SplitHostBytesForCpuRead(*drawResource, applierState.VerbIndirectBuffer,
"indirect_count_commands");
}
if (commandBytes > BufferImpl::ResourceWidthForHandle(applierState.VerbIndirectBuffer)) {
MGLOG_E_ONCE("MultiDrawArraysIndirectCount skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range");
return;
@@ -10929,6 +11064,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
continue;
}
TextureImpl::NoteDriverSideTextureWrite(destinationTexture);
auto backendSource = TextureImpl::SyncTextureObjectToBackend(sourceTexture);
auto backendDestination = TextureImpl::SyncTextureObjectToBackend(destinationTexture);
if (!backendSource || !backendDestination) continue;
@@ -11608,51 +11744,49 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// P5c (hd), P5e (tx2): the verification half, with the RECORD passed in instead of re-read
// from the verb stash - the caller has it and the two must not disagree about which texture
// is being verified.
static void EnsureGenerateMipmapStorageDescribed(const MG_Pipe::MGPipeResourceRecord& record) {
// P8-B1: THE LEVELS A generate_mipmap RECORD GENERATES, end-exclusive. GL generates levels
// BASE_LEVEL+1 .. q, q = min(p, MAX_LEVEL) with p the last level the base image's own chain
// reaches, and an immutable texture's storage bounds the chain too (GL 4.6 core 8.14.4). The
// client already computed that window (ComputeMipmapGenerationRange) and defined its levels
// before emitting the verb, so the record's plan is the window and the descriptor is what
// holds it: Base = VerbMipBaseLevel, End = min(VerbMipLevelCount, Desc.Levels, MaxLevel + 1,
// Base + the base extent's chain) - the same clip Magma's GenerateWireMipmap makes.
//
// What stood here instead demanded the full chain from LEVEL 0 and died with
// Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the descriptor held less: a
// MAX_LEVEL below the chain, or glTexStorage2D with fewer levels, both legal GL.
struct RecordMipWindow {
Uint32 Base = 0;
Uint32 End = 0; // end-exclusive; End <= Base + 1 generates nothing
};
static RecordMipWindow GenerateMipmapWindowByRecord(const MG_Pipe::MGPipeResourceRecord& record) {
const auto& applier = MG_Pipe::MGPipeApplier();
const auto& desc = record.Desc;
if (desc.Width == 0 || desc.Levels == 0) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
}
Uint maxDimension = desc.Width;
const auto target = static_cast<MG_Pipe::MGPipeResourceTarget>(desc.Target);
if (target != MG_Pipe::MGPipeResourceTarget::Tex1D &&
target != MG_Pipe::MGPipeResourceTarget::Tex1DArray) {
maxDimension = std::max(maxDimension, desc.Height);
}
if (target == MG_Pipe::MGPipeResourceTarget::Tex3D) {
maxDimension = std::max(maxDimension, desc.Depth);
}
Uint requiredLevels = 1;
while (maxDimension > 1) {
maxDimension /= 2;
++requiredLevels;
}
if (desc.Levels < requiredLevels) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
RecordMipWindow window{applier.VerbMipBaseLevel, applier.VerbMipBaseLevel};
if (desc.Width == 0 || desc.Levels == 0 || window.Base >= desc.Levels) return window;
Uint32 end = std::min<Uint32>(applier.VerbMipLevelCount, desc.Levels);
if (record.ParamsSerial) end = std::min<Uint32>(end, static_cast<Uint32>(record.Params.MaxLevel) + 1u);
const IntVec3 baseExtent = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, window.Base);
const Int shrinking = MG_Remote::Server::StagedTextureShrinkingAxisCount(desc.Target);
Int largest = 1;
for (Int axis = 0; axis < shrinking && axis < 3; ++axis) largest = std::max<Int>(largest, baseExtent[axis]);
Uint32 chain = 1;
while (largest > 1) {
largest /= 2;
++chain;
}
window.End = std::max(window.Base, std::min(end, window.Base + chain));
return window;
}
#endif
static Bool EnsureGenerateMipmapStorageAllocated(const SharedPtr<MG_State::GLState::ITextureObject>& texture) {
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
// The frontend has already defined the generated chain before emitting this verb.
// Its resource_respecify carries that shape, so the server only verifies the
// descriptor; it must not allocate or dirty the client's level shadows again.
// P5c (hd): identity is the handle the generate_mipmap record carried
// (MGPMipPlan::Res, the verb stash) - the client allocator is never probed (T2).
const auto handle = MG_Pipe::MGPipeApplier().VerbMipRes;
const auto* record = PipeTextureRecordForHandle(handle);
if (record == nullptr) {
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-storage");
}
EnsureGenerateMipmapStorageDescribed(*record);
return false; // No server-side shadow allocation was necessary.
}
#endif
// P8-B1: the transport arm that stood here (a descriptor check under the same
// `generate-mipmap-storage` name) was unreachable - GenerateMipmap returns through
// GenerateMipmapByRecord before this call on every split arm - and the record arm's
// window above replaces the check it made.
auto* mipmapTexture = dynamic_cast<MG_State::GLState::TextureObjectMipmap*>(texture.get());
MOBILEGL_ASSERT(mipmapTexture != nullptr, "GenerateMipmap requires mipmap texture storage.");
Bool allocatedStorage = false;
@@ -12386,11 +12520,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// re-derived its handle with a scoped `HandleOf` probe (the two sites §4.4 listed as
// barriered), and the handle is now simply passed in - the extent derivation, the per-level
// blits and the format question are §1's descriptor reads exactly as P5c wrote them.
//
// P8-B1: both chains run over the record's WINDOW (GenerateMipmapWindowByRecord), not from
// level 1 to the end of the descriptor: a BASE_LEVEL above 0 is the source and a level past
// MAX_LEVEL is left as it is, which is what the monolith twins below still get wrong.
static void GenerateDepthTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
const RecordMipWindow& window) {
const auto& desc = record.Desc;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
for (Uint32 level = window.Base + 1; level < window.End; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
@@ -12403,12 +12542,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
static void GenerateColorTexture2DMipmapByRecord(const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
const RecordMipWindow& window) {
const auto& desc = record.Desc;
const GLenum filter =
IsIntegerColorFormat(static_cast<TextureInternalFormat>(desc.InternalFormat)) ? GL_NEAREST : GL_LINEAR;
const GLuint textureId = backendTexture->GetBackendTextureId();
for (Uint32 level = 1; level < desc.Levels; ++level) {
for (Uint32 level = window.Base + 1; level < window.End; ++level) {
const IntVec3 srcSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, level - 1);
const IntVec3 dstSize = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
@@ -12591,6 +12731,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// touched unit happened to sync it as a side effect. The named narrowing (P5e-5) says
// the union that still covers every texture includes "the waited texture ops", and
// this is one of them: it syncs its own endpoint by handle.
TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle);
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle);
if (!backendTexture || dstRecord == nullptr) {
MGLOG_E_ONCE("CopyTexImage2D: the verb's destination texture {%u, %u} has no twin "
@@ -12606,6 +12747,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& textureObject = MGB_CTX->GetTextureUnitObject((Int)activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
if (textureObject) TextureImpl::NoteDriverSideTextureWrite(textureObject);
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
@@ -12708,6 +12850,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_Pipe::MGPipeHandle dstHandle = MG_Pipe::MGPipeApplier().VerbCopyTexDst;
// P5e (tx2): synced by handle - see CopyTexImage2D's note. A copy destination is not
// in the sampler-view window, so nothing else on this path would build its twin.
TextureImpl::NoteDriverSideTextureWriteByHandle(dstHandle);
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(dstHandle);
if (!backendTexture) {
MGLOG_E_ONCE("CopyTexSubImage2D: the verb's destination texture {%u, %u} has no "
@@ -12722,6 +12865,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
const auto& textureObject = MGB_CTX->GetTextureUnitObject(activeTextureUnit)
.GetBindingSlot(MG_Util::ConvertGLEnumToTextureTarget(target))
.GetBoundObject();
if (textureObject) TextureImpl::NoteDriverSideTextureWrite(textureObject);
auto* backendTextureSlot = TextureImpl::g_backendTextureObjects.Find(textureObject.get());
if (!backendTextureSlot || !*backendTextureSlot) {
MGLOG_E_ONCE("CopyTexSubImage2D: No backend texture found for texture %u.",
@@ -12858,65 +13002,268 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-B2: RGB16F / RGB32F MIP GENERATION ON THE SERVER, FROM THE SERVER'S OWN LEVEL.
//
// ES generates a chain only for a colour-renderable, filterable format, and whether the
// driver's storage for these two is one depends on the driver: Espryt stores them natively
// where the driver renders the three-channel form and widens them to RGBA16F / RGBA32F where
// it does not (TextureImpl::BackendTextureFormatAddsAlpha), and a widened RGBA32F still needs
// OES_texture_float_linear to be filterable. So GenerateMipmapByRecord asks the driver first
// and comes here only when it refuses - or when MGITEST_ESPRYT_FORCE_CPU_MIPMAP says to, which
// is the only way a host whose driver never refuses reaches this arm.
//
// THE SOURCE IS THE SERVER'S LEVEL, NEVER THE CLIENT'S. The base level is read back from the
// driver (ReadTextureLevelTight), because a render target's texels exist only there - the
// monolith arm (GenerateThreeChannelFloatMipmapOnCpu) filters the frontend's level shadow
// instead and reads a stale base after a draw. Only when the driver cannot read the level back
// (storage it cannot attach) does the staged store answer, and a store that does not cover the
// level declines by name rather than inventing texels.
static Bool ForcedServerCpuMipmap() {
// The R-16 control. Read by the SERVER, like the MGITEST_MAGMA_FORCE_* knobs.
static const Bool s_forced = std::getenv("MGITEST_ESPRYT_FORCE_CPU_MIPMAP") != nullptr;
return s_forced;
}
// WireTextureReadback.inc (included further down) defines it: E's extent-scoped stand-in for a
// driver that refuses to read a colour level back, asked here too so a host reaches the store
// route below (P8-SE).
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent);
// The base level as tight RGB floats: the driver's texels first, the staged store second.
//
// P8-SE: THE STORE ANSWERS ONLY FOR A TEXTURE THE DRIVER NEVER WROTE (P8-E's sticky mark). A
// render target's level 0 lives on the driver; the store still holds whatever crossed before
// the draw, and filtering it would hand the chain stale texels. `driverWrittenBefore` is the
// mark as it stood BEFORE this generation noted its own write (GenerateMipmap's split arm),
// because that note is unconditional and would otherwise decline every store read. Anything
// the store cannot stand for is a named decline, `declined` says why.
static Bool ReadThreeChannelFloatLevelOnServer(MG_Pipe::MGPipeHandle mipRes,
const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
TextureTarget textureTarget, TextureUploadTarget face, Uint32 level,
const IntVec3& extent, Bool driverWrittenBefore,
Vector<Float>& rgb, const char*& declined) {
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
const SizeT texels = static_cast<SizeT>(extent.x()) * static_cast<SizeT>(extent.y());
Vector<Uint8> bytes;
if (!WireTextureReadbackRefusedForTesting(extent) &&
ReadTextureLevelTight(backendTexture->GetBackendTextureId(), textureTarget, face, format,
static_cast<GLint>(level), extent, backendTexture->RequiresImageBindableStorage(),
GL_RGB, GL_FLOAT, bytes) &&
bytes.size() == texels * 3 * sizeof(Float)) {
rgb.resize(texels * 3);
Memcpy(rgb.data(), bytes.data(), bytes.size());
return true;
}
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(mipRes);
const auto faceCode = static_cast<Uint16>(face);
const auto levelCode = static_cast<Uint16>(level);
const Bool half = format == TextureInternalFormat::RGB16F;
const SizeT componentBytes = half ? sizeof(Uint16) : sizeof(Float);
// A view's staged bytes live under its storage owner's key, in the owner's levels; a view
// whose level the driver cannot read has no answer here.
if (!MG_Pipe::MGPipeHandleIsNull(record.Desc.ViewOf)) {
declined = "a view has no staged level of its own";
} else if (!store.CopiesIntoServerStorage()) {
declined = "this arm keeps no staged store";
} else if (driverWrittenBefore) {
declined = "the driver has written the texture since its bytes crossed";
} else if (!store.IsCovered(key, faceCode, levelCode)) {
declined = "not every byte of the level crossed";
} else if (store.LevelByteSize(key, faceCode, levelCode) != texels * 3 * componentBytes) {
declined = "the staged level is not the size of the level";
}
if (declined != nullptr) return false;
const Uint8* staged = store.RequireLevelBytes(key, faceCode, levelCode, "generate-mipmap-server-cpu");
rgb.resize(texels * 3);
for (SizeT i = 0; i < texels * 3; ++i) {
if (half) {
Uint16 bits = 0;
Memcpy(&bits, staged + i * componentBytes, sizeof(bits));
rgb[i] = MG_Util::DecodeHalfBitsToFloat(bits);
} else {
Memcpy(&rgb[i], staged + i * componentBytes, sizeof(Float));
}
}
MGLOG_W_ONCE("MGPipe: generate-mipmap-server-cpu: texture {%u, %u} level %u could not be read back from "
"the driver; filtering the staged store's copy of it instead",
mipRes.Slot, mipRes.Gen, level);
return true;
}
static void GenerateThreeChannelFloatMipmapOnServer(MG_Pipe::MGPipeHandle mipRes,
const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
Bool driverWrittenBefore, const char* why) {
const auto& desc = record.Desc;
const auto format = static_cast<TextureInternalFormat>(desc.InternalFormat);
const auto textureTarget = TextureTargetForPipeResourceTarget(desc.Target);
const RecordMipWindow window = GenerateMipmapWindowByRecord(record);
if (window.End <= window.Base + 1) return;
Vector<TextureUploadTarget> faces;
if (textureTarget == TextureTarget::Texture2D) {
faces.push_back(TextureUploadTarget::Texture2D);
} else if (textureTarget == TextureTarget::TextureCubeMap) {
for (Int face = 0; face < 6; ++face)
faces.push_back(static_cast<TextureUploadTarget>(
static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face));
}
// The upload spelling the driver's storage takes: GL_FLOAT is legal for all four float
// internal formats (ES 3.2 table 8.2), and the channel count follows the storage.
GLenum esInternal = GL_NONE, esFormat = GL_NONE, esType = GL_NONE;
TextureImpl::GenerateTextureFormatInfo(format, &esInternal, &esFormat, &esType, textureTarget);
const Bool fourChannel = esInternal == GL_RGBA16F || esInternal == GL_RGBA32F;
if (faces.empty() || (!fourChannel && esInternal != GL_RGB16F && esInternal != GL_RGB32F)) {
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: target %s stored as %s has "
"no server CPU arm (%s); levels %u..%u are left as they are",
mipRes.Slot, mipRes.Gen, MG_Util::ConvertTextureTargetToString(textureTarget).c_str(),
MG_Util::ConvertGLEnumToString(esInternal).c_str(), why, window.Base + 1, window.End - 1);
return;
}
MGLOG_I_ONCE("MGPipe: generate-mipmap-server-cpu: %s levels filtered on the server's CPU from its own "
"level (%s)", MG_Util::ConvertTextureInternalFormatToString(format).c_str(), why);
const GLuint textureId = backendTexture->GetBackendTextureId();
const GLenum bindTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(textureTarget);
for (const TextureUploadTarget face : faces) {
IntVec3 extent = MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height,
desc.Depth, window.Base);
Vector<Float> level;
const char* declined = nullptr;
if (!ReadThreeChannelFloatLevelOnServer(mipRes, record, backendTexture, textureTarget, face, window.Base,
extent, driverWrittenBefore, level, declined)) {
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu declines texture {%u, %u}: base level %u could not "
"be read back from the driver and the staged store cannot stand for it (%s); levels "
"%u..%u are left as they are - DECLINED (generate-mipmap-store-declined)",
mipRes.Slot, mipRes.Gen, window.Base, declined != nullptr ? declined : "?",
window.Base + 1, window.End - 1);
return;
}
for (Uint32 dst = window.Base + 1; dst < window.End; ++dst) {
const IntVec3 dstExtent =
MG_Remote::Server::StagedTextureMipExtent(desc.Target, desc.Width, desc.Height, desc.Depth, dst);
// The monolith's box filter: the 2x2 source footprint, clamped where a dimension
// is already 1 (GL 4.6 core 8.14.4 leaves the filter to the implementation).
Vector<Float> next(static_cast<SizeT>(dstExtent.x()) * static_cast<SizeT>(dstExtent.y()) * 3);
const auto at = [&](Int x, Int y, Int c) {
return level[(static_cast<SizeT>(y) * static_cast<SizeT>(extent.x()) + static_cast<SizeT>(x)) * 3 +
static_cast<SizeT>(c)];
};
for (Int y = 0; y < dstExtent.y(); ++y) {
for (Int x = 0; x < dstExtent.x(); ++x) {
const Int x0 = std::min(x * 2, extent.x() - 1), x1 = std::min(x * 2 + 1, extent.x() - 1);
const Int y0 = std::min(y * 2, extent.y() - 1), y1 = std::min(y * 2 + 1, extent.y() - 1);
for (Int c = 0; c < 3; ++c)
next[(static_cast<SizeT>(y) * static_cast<SizeT>(dstExtent.x()) + static_cast<SizeT>(x)) *
3 + static_cast<SizeT>(c)] =
0.25f * (at(x0, y0, c) + at(x1, y0, c) + at(x0, y1, c) + at(x1, y1, c));
}
}
Vector<Float> upload;
if (fourChannel) {
const SizeT count = next.size() / 3;
upload.resize(count * 4);
for (SizeT i = 0; i < count; ++i) {
upload[i * 4] = next[i * 3];
upload[i * 4 + 1] = next[i * 3 + 1];
upload[i * 4 + 2] = next[i * 3 + 2];
upload[i * 4 + 3] = 1.0f;
}
}
// Tight float rows are a multiple of four bytes, which the resting UNPACK state
// (4/0/0/0, and the unpack-PBO binding at 0) parses as they are - CopyR32FTexture2D's
// discipline, including the re-bind that keeps the binding cache truthful.
ClearGLErrors();
BufferImpl::BindPixelUnpackBufferId(0);
TextureImpl::ActivateTextureUnit(TextureImpl::TempTextureUnit);
g_GLESFuncs.glBindTexture(bindTarget, textureId);
g_GLESFuncs.glTexSubImage2D(TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(face),
static_cast<GLint>(dst), 0, 0, static_cast<GLsizei>(dstExtent.x()),
static_cast<GLsizei>(dstExtent.y()), fourChannel ? GL_RGBA : GL_RGB,
GL_FLOAT, fourChannel ? upload.data() : next.data());
const GLenum err = g_GLESFuncs.glGetError();
auto* cachedBound = TextureImpl::g_boundTexturesCache[TextureImpl::TempTextureUnit]
[static_cast<SizeT>(textureTarget)];
g_GLESFuncs.glBindTexture(bindTarget, cachedBound ? cachedBound->GetBackendTextureId() : 0);
if (err != GL_NO_ERROR) {
MGLOG_E_ONCE("MGPipe: generate-mipmap-server-cpu: uploading level %u of texture {%u, %u} "
"failed: %s", dst, mipRes.Slot, mipRes.Gen,
MG_Util::ConvertGLEnumToString(err).c_str());
return;
}
level = Move(next);
extent = dstExtent;
}
}
}
// P5e (tx2), CONTRACT-P5E §5.2: glGenerateMipmap's body once the texture came from
// VerbMipRes. Same four arms as the frontend body, with every shape question answered from
// the descriptor.
//
// RULING 14, AND WHY THE ROW IS NOT FLIPPED HERE. The unit read this arm retires is what the
// wait existed for, but ONE frontend contact is left on the path and it is not tx2's to
// retire: the RGB16F/RGB32F CPU filter reaches into the client's level shadows
// (GenerateThreeChannelFloatMipmapOnCpu, kimi audit row 61) and P8/P9 own the pull. So the
// brief's escape applies - the arm is not complete, the row stays kWaitApplied and the flip
// is listed as trailing - and the record stays barriered, which is what keeps the CPU arm's
// refusal a refusal rather than a wrong picture.
// RULING 14's TRAILING FLIP. tx2 kept this verb's PipeCalls.def row at kWaitApplied because
// one frontend contact was left on the path: the RGB16F/RGB32F CPU filter reached into the
// client's level shadows. P8-B2 retired it (GenerateThreeChannelFloatMipmapOnServer reads the
// server's own level), so no arm here reads client memory any more; the WaitClass flip itself
// is a protocol change and is left to the integrator.
//
// `driverWrittenBefore`: P8-E's driver-written mark on the texture's staged store as it stood
// before this call noted its own write - the CPU arm's store route reads it (P8-SE).
static void GenerateMipmapByRecord(GLenum target, MG_Pipe::MGPipeHandle mipRes,
const MG_Pipe::MGPipeResourceRecord& record,
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture) {
const SharedPtr<TextureImpl::BackendTextureObject>& backendTexture,
Bool driverWrittenBefore) {
const auto format = static_cast<TextureInternalFormat>(record.Desc.InternalFormat);
const auto textureTarget = TextureTargetForPipeResourceTarget(record.Desc.Target);
const Bool threeChannelFloat =
format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F;
if (format == TextureInternalFormat::R11FG11FB10F || IsDepthOnlyFormat(format) ||
format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) {
// The storage verification EnsureGenerateMipmapStorageAllocated's disaggregated arm
// already makes, with the record passed in rather than re-read from VerbMipRes: the
// frontend defined the generated chain before emitting this verb, so the server only
// checks that the descriptor it was given can carry it.
EnsureGenerateMipmapStorageDescribed(record);
}
if (format == TextureInternalFormat::RGB16F || format == TextureInternalFormat::RGB32F) {
// The CPU filter reads and writes the CLIENT's level shadows. It already aborts
// three frames deep at the MipmapStorage guard under a transport; naming it here
// says which emulation, which is what the guard cannot.
MG_Pipe::MGPipeUnmigratedEmulation("generate-mipmap-cpu-filter");
}
// A view's layer window is the one bound the native arm cannot express, so it is asked
// for before the format arms - those two are 2D-only emulations of the same generation
// and a view reaches them too.
if (GenerateMipmapThroughViewWindowByRecord(mipRes, record, format)) return;
if (IsDepthOnlyFormat(format)) {
GenerateDepthTexture2DMipmapByRecord(record, backendTexture);
GenerateDepthTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record));
return;
}
if (format == TextureInternalFormat::R11FG11FB10F && textureTarget == TextureTarget::Texture2D) {
GenerateColorTexture2DMipmapByRecord(record, backendTexture);
GenerateColorTexture2DMipmapByRecord(record, backendTexture, GenerateMipmapWindowByRecord(record));
return;
}
if (threeChannelFloat && ForcedServerCpuMipmap()) {
GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore,
"forced by MGITEST_ESPRYT_FORCE_CPU_MIPMAP");
return;
}
const GLenum backendTarget =
TextureImpl::ConvertTextureTargetToBackendGLEnum(MG_Util::ConvertGLEnumToTextureTarget(target));
// TempTextureUnit, not the active unit: the scratch bind is the backend's own and
// BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit
// was never anything but the frontend's way of naming the texture, and the record names
// it now.
backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit);
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the
// source texture is detached from every framebuffer that attaches it - by handle, over
// the applier's own framebuffer records.
ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes);
ScopedCompleteFramebufferBinding completeFramebuffer;
ClearGLErrors();
g_GLESFuncs.glGenerateMipmap(backendTarget);
RecordGLError("glGenerateMipmap", backendTarget, format);
GLenum refused = GL_NO_ERROR;
{
// TempTextureUnit, not the active unit: the scratch bind is the backend's own and
// BindCurrentTextures re-establishes the sampling bindings regardless. The ACTIVE unit
// was never anything but the frontend's way of naming the texture, and the record
// names it now.
backendTexture->Bind(backendTarget, TextureImpl::TempTextureUnit);
// ANGLE/Mesa may validate the currently bound FBO while generating mipmaps, so the
// source texture is detached from every framebuffer that attaches it - by handle,
// over the applier's own framebuffer records.
ScopedDetachedTextureFramebufferAttachments detachedAttachments(mipRes);
ScopedCompleteFramebufferBinding completeFramebuffer;
ClearGLErrors();
g_GLESFuncs.glGenerateMipmap(backendTarget);
if (!threeChannelFloat) {
RecordGLError("glGenerateMipmap", backendTarget, format);
return;
}
// The driver's refusal of a three-channel float chain is not the application's
// error: the CPU arm below answers it.
refused = g_GLESFuncs.glGetError();
}
if (refused == GL_NO_ERROR) return;
GenerateThreeChannelFloatMipmapOnServer(mipRes, record, backendTexture, driverWrittenBefore,
"the driver refused glGenerateMipmap");
}
#endif
@@ -12943,6 +13290,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// for GenerateMipmap move to kWaitNone.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
const auto mipRes = MG_Pipe::MGPipeApplier().VerbMipRes;
// P8-SE: whether the staged store still stands for the texture is read BEFORE the note
// below sets the mark, or the CPU arm's store route could never answer at all.
const Bool driverWrittenBefore = MG_Remote::Server::ServerStagedTexture().IsDriverWritten(
MG_Remote::Server::StagedTextureStore::KeyForHandle(mipRes));
// Every arm GenerateMipmapByRecord takes writes the levels above the base on the
// driver only (P8-B2's CPU arm uploads its levels there too), so the note is
// unconditional.
TextureImpl::NoteDriverSideTextureWriteByHandle(mipRes);
auto& backendTexture = TextureImpl::SyncTextureToBackendByHandle(mipRes);
const auto* record = PipeTextureRecordForHandle(mipRes);
if (!backendTexture || record == nullptr) {
@@ -12951,7 +13306,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
mipRes.Slot, mipRes.Gen);
return;
}
GenerateMipmapByRecord(target, mipRes, *record, backendTexture);
GenerateMipmapByRecord(target, mipRes, *record, backendTexture, driverWrittenBefore);
return;
}
#endif
@@ -12970,6 +13325,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
TextureImpl::SyncTextureObjectToBackend(texture);
return;
}
// Every path below writes the levels above the base on the driver only.
TextureImpl::NoteDriverSideTextureWrite(texture);
auto& backendTexture = TextureImpl::SyncTextureObjectToBackend(texture);
// Asked for before the format arms below, which are 2D-only emulations of the same
@@ -13083,25 +13440,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// The renderbuffer twin of TextureImpl::SyncTextureObjectToBackend: the same
// find-or-create-then-sync the framebuffer attachment walk does (see SyncAttachmentObject),
// reachable from a path that has a renderbuffer but no framebuffer.
// reachable from a path that has a renderbuffer but no framebuffer. The MONOLITH arm's only:
// under a transport MakeGLESCopyImageEndpoint resolves a renderbuffer endpoint by handle and
// never calls this. (P8-E removed the `CopyImageSubData+RENDERBUFFER` latch that stood here -
// its transport condition could not hold at the only call site - and
// CopyImageStoreReadbackScenario's renderbuffer case runs the handle arm on every transport.)
static SharedPtr<RenderbufferImpl::BackendRenderbufferObject> SyncRenderbufferObjectToBackend(
const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbufferObject) {
if (!renderbufferObject) return nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5e (fb, CONTRACT-P5E.md §4.4): the scope at this site is DELETED, and with it the
// arm that needed it. The only caller is the glCopyImageSubData endpoint builder, and a
// RENDERBUFFER endpoint is refused on the wire before it is emitted and again at the
// sink (PipeApplier.cpp's ServerUnmigratedVerbFatal("CopyImageSubData+RENDERBUFFER")),
// so under a transport nothing can reach here with one. Saying so by name is better
// than a find-or-MINT against the frontend address that the refusal already proved
// unreachable - if it ever becomes reachable, the name is where to start.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
MGLOG_F("MGPipe: Fatal{UnmigratedVerb, \"CopyImageSubData+RENDERBUFFER\"} a renderbuffer "
"endpoint reached the backend under an active transport, where both the client "
"emitter and the sink refuse one");
std::abort();
}
#endif
SharedPtr<RenderbufferImpl::BackendRenderbufferObject> backendRenderbufferObject;
if (auto* slot = RenderbufferImpl::g_backendRenderbufferObjects.Find(renderbufferObject.get())) {
backendRenderbufferObject = *slot;
@@ -13222,19 +13568,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
auto* dstMipmap = MG_State::GLState::AsMipmapTexture(dstEndpoint.Texture.get());
if (!srcMipmap || !dstMipmap) return;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5b package i1's ONE backend edit, and the contract names this site
// (MG_Remote/CONTRACT-P5B.md §2 i1 "the copy-image-shadow-mirror emulation", ruling
// §6.7). Migrating glCopyImageSubData moves the first blocker off the client's
// Fatal{UnmigratedVerb} and onto the Fatal below, on every Espryt copy between two
// textures with CPU shadows - so the ruling is: UNDER A REAL TRANSPORT THE SERVER SKIPS
// THE MIRROR, and the client-side mirror ROADMAP P8 names ("CopyImage 镜像搬到 client")
// stays P8's.
//
// WHAT THE SKIP LOSES IS BOUNDED BY TWO FATALS, which is the whole reason it is allowed
// to be a skip rather than a port: a later glGetTexImage of the destination served from
// the shadow is class C wave 3 (Fatal{UnmigratedVerb, "GetTexImage"}, P9) and a texture
// re-mint that re-uploads the level is Fatal{UnmigratedEmulation, "texture-remint-pull"}
// (Managers.cpp:5634). Neither can silently read the un-mirrored shadow.
// UNDER A REAL TRANSPORT THE SERVER HAS NO FRONTEND SHADOW TO MIRROR INTO (P5b i1, ruling
// §6.7), and P8-E settled where the move lives instead - not on the client (ROADMAP P8's
// original row) but in the server's staged store: CopyImageSubData's handle arm makes the
// same texel-block move there (TextureImpl::FollowCopyImageInStagedStore) for EVERY
// format, not only the redundant-encoding ones this mirror serves, and marks the store
// driver-written where it cannot. The store is what answers a split glGetTexImage the
// driver refuses to read back (WireTextureReadback.inc's store route); every other split
// read of the destination reads the GPU level itself (notes/p8/E.md).
//
// BEHIND #if MOBILEGL_BUILD_DISAGGREGATED so the pull build's code does not move (G1),
// and the arm is the TRANSPORT and not the build - build-split runs its unit and
@@ -13242,14 +13583,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ordinary correct path and must still run.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;
#endif
#if MOBILEGL_PIPE_PUSH
// P4a (D-M). glCopyImageSubData's CPU-shadow mirror copies the source level's shadow
// rows into the DESTINATION's shadow so a later readback of the destination sees what
// the GPU copy put there. Both shadows are the client's, so this is the clearest case
// in the family of an emulation that cannot survive a split; ROADMAP puts the move
// itself in P8 and this names the site until then.
MG_Pipe::MGPipeUnmigratedEmulation("copy-image-shadow-mirror");
#endif
// P4a (D-M) named this site `copy-image-shadow-mirror` with MGPipeUnmigratedEmulation.
// P8-SE removed the call: the return above means no transport reaches it, and on a
// monolith arm the call was a no-op (PipeCatalogueTest's list and the lint
// scripts/ci/unmigrated_emulation_sites.py follow).
const auto srcUploadTarget = srcEndpoint.Texture->GetUploadTargets()[0];
const auto dstUploadTarget = dstEndpoint.Texture->GetUploadTargets()[0];
@@ -13316,11 +13653,43 @@ namespace MobileGL::MG_Backend::DirectGLES {
// dereference. The frontend validator is what keeps this unreachable and what reports
// the error the application is owed; declining is only how a future gap up there stops
// being a crash. See the level guard in VulkanRenderer::CopyImageSubData.
if (!dstEndpoint.IsRenderbuffer() && dstEndpoint.Texture) {
TextureImpl::NoteDriverSideTextureWrite(dstEndpoint.Texture);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The handle arm's endpoint names its texture by handle, and the server has no frontend
// object to note. The staged store's half of the note waits for the copy (P8-E, below).
const Bool handleArmDestination =
!dstEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(dstEndpoint.TextureHandle);
if (handleArmDestination) {
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle, /*storeFollowsTheWrite=*/true);
}
#endif
if (!MakeGLESCopyImageEndpoint(srcEndpoint, srcTarget, srcX, srcY, srcZ, src) ||
!MakeGLESCopyImageEndpoint(dstEndpoint, dstTarget, dstX, dstY, dstZ, dst)) {
MGLOG_E_ONCE("%s: source or destination image failed to sync; declining the copy", __func__);
return;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E (notes/p8/E.md): THE SERVER'S STAGED STORE FOLLOWS THE COPY. Both endpoints are synced,
// so every byte the store holds for them is on the driver, and the copy below is a raw
// texel-block move - the store makes the same move, which keeps the destination level
// answerable from here when the driver will not read it back (ReadTextureImageWire's store
// route) and keeps a whole-level replay of it (a re-mint's refused readback, a full-level
// re-upload) from putting the pre-copy texels back. Where the store cannot follow (a
// renderbuffer, a cube or 1D-array endpoint, a source the driver has written, a level whose
// bytes did not all cross) it is marked driver-written instead, so it declines rather than
// answering with stale bytes. Nothing moves to the client (ROADMAP P8's original row).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && handleArmDestination) {
const Bool followed =
!srcEndpoint.IsRenderbuffer() && !MG_Pipe::MGPipeHandleIsNull(srcEndpoint.TextureHandle) &&
TextureImpl::FollowCopyImageInStagedStore(
srcEndpoint.TextureHandle, static_cast<Uint32>(srcLevel), IntVec3{srcX, srcY, srcZ},
dstEndpoint.TextureHandle, static_cast<Uint32>(dstLevel), IntVec3{dstX, dstY, dstZ},
IntVec3{srcWidth, srcHeight, srcDepth});
if (!followed) TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
}
#endif
// Verbatim: GL already spells a 1D array's extent the way the ES 2D array it maps onto
// wants it (height 1, layers on depth) - see MakeGLESCopyImageEndpoint.
@@ -13394,6 +13763,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
MG_Util::ConvertGLEnumToString(dst.target).c_str(),
MG_Util::ConvertGLEnumToString(dstTarget).c_str());
MOBILEGL_ASSERT(false, "glCopyImageSubData failed after frontend validation accepted the request.");
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E: the store already made the move the driver just refused.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && handleArmDestination) {
TextureImpl::NoteDriverSideTextureWriteByHandle(dstEndpoint.TextureHandle);
}
#endif
return;
}
// The copy landed on the GPU. For a destination whose readback cannot be bit-exact the
@@ -14952,10 +15327,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Session::Fail entirely, which is the funnel the census gate watches.
//
// THE FUNNEL KEEPS ITS TEETH FOR THE OTHER SITES: MGPipeUnmigratedEmulation still
// aborts for generate-mipmap-storage, generate-mipmap-cpu-fallback,
// generate-mipmap-cpu-filter and texture-remint-pull, which really do reach into a
// client address space. Only this call site is retired, and only because its own
// premise did not hold.
// names generate-mipmap-storage and generate-mipmap-cpu-fallback, on monolith arms only
// (P8-SE dropped copy-image-shadow-mirror, a site no transport reached). Only this call
// site is retired here, and only because its own premise did not hold (texture-remint-pull
// went the same way in P9 W2, and P8-B the two split generate-mipmap names, likewise).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
if (shadow == nullptr) {
MGLOG_E_ONCE("GetTexImage: no level shadow on this side for target=0x%x level=%d, and under an "
@@ -15169,9 +15544,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
#include "WireTextureReadback.inc"
#endif
static Bool IsLegacyNativeReadPixelsFormat(GLenum format) {
return format == GL_RGBA || format == GL_RGBA_INTEGER || format == GL_RED || format == GL_RED_INTEGER ||
@@ -15859,7 +16232,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
// resizing depth/stencil without colour would report the default framebuffer incomplete.
static void PublishDefaultFramebufferDepthStencilFormat() {
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
#if MOBILEGL_PIPE_PUSH
// P8-SE: THE PLACEHOLDER IS ONLY THE MONOLITH ARM'S TARGET. With a transport the answer is
// posted (OnSurfaceChanged, below) and the client writes its own placeholder; a spawn or tcp
// server process has no frontend at all, so pDefaultFramebufferInfo is null there. Gating on
// it returned before the post, the client kept Init.cpp's GL_DEPTH32F_STENCIL8 guess for a
// D24S8 pbuffer, and a blit into a buffer allocated from that guess lost depth and stencil
// (notes/p8/SE.md). Inproc only passed because client and server share the process.
if (!g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return;
if (!defaultFBOInfo && MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged == nullptr) return;
#else
if (!defaultFBOInfo || !g_GLESFuncs.glGetFramebufferAttachmentParameteriv) return;
#endif
// GL_DEPTH / GL_STENCIL are the default framebuffer's spellings; a user framebuffer
// would need GL_DEPTH_ATTACHMENT / GL_STENCIL_ATTACHMENT and answers GL_INVALID_ENUM
@@ -15940,8 +16324,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
stencilFormat = TextureInternalFormat::StencilIndex8;
}
#if MOBILEGL_PIPE_PUSH
// Null in a spawn / tcp server (above); only the monolith arm below writes through them.
auto* depthTexture = defaultFBOInfo ? defaultFBOInfo->depthAttachment.get() : nullptr;
auto* stencilTexture = defaultFBOInfo ? defaultFBOInfo->stencilAttachment.get() : nullptr;
#else
auto* depthTexture = defaultFBOInfo->depthAttachment.get();
auto* stencilTexture = defaultFBOInfo->stencilAttachment.get();
#endif
#if MOBILEGL_PIPE_PUSH
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
// P5c ev (CONTRACT-P5C §4.2): with an active transport the attachments are CLIENT
+31 -5
View File
@@ -23,15 +23,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Content uploads use scratch bindings, so draws and dispatches call this after
// texture synchronization.
void BindCurrentTextures();
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Native/server-only color level readback. Returns tightly packed, owned
// Driver-side color/depth level readback. Returns tightly packed, owned
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
// The caller resolves identity/extent from its resource record and supplies
// whether the SOURCE allocation already uses an image carrier.
// The caller supplies identity and extent, and whether the SOURCE allocation
// already uses an image carrier.
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
// refuses answers true, keeping the caller on its readback path.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent);
#endif
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
@@ -243,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);
}
File diff suppressed because it is too large Load Diff
+119 -2
View File
@@ -1004,6 +1004,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (the UBO binding walk) skip the probe while their pre-pass epoch read
// matches, exactly like the per-VAO memo stamps.
Uint64 drawCleanEpoch = 0;
// Frame serial whose completion retires the last glCopyBufferSubData the upload
// ring staged INTO this store (the flush ladder's ring tier: FlushPendingRangesNow
// on the pull arm, FlushPendingRangesFrom on the push arm); 0 = none. Until it
// retires, both keep every partial range off the range-invalidating map: on
// Adreno 830 that map, on a store with a queued copy, loses the copy (see
// FlushPendingRangesNow and InvalidateFlushAccessFor). Draw thread only, like the
// flush.
Uint64 ringCopyRetireSerial = 0;
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
// immutable, persistently+coherently mapped, and persistentPtr is what the app
// (and the frontend PipeResource) write into directly. While set, draw-time
@@ -1042,6 +1050,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the legacy arm did; this member exists for the drains that have no object, which
// in P3a is the readback flush and the fp64 narrowing.
const Uint8* hostBytes = nullptr;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): the client's AHardwareBuffer this store IS - imported with
// glBufferStorageExternalEXT, so `id` is immutable, persistentMapped and
// persistentPtr is this server's own coherent map of the client's pages. The
// reference is this twin's own, released only after a fence that follows the
// store's last use has signalled (RetireT0Import). Survives an ES context loss:
// the next ensure re-imports it. Null for every store T0 did not adopt.
void* externalAhb = nullptr;
// P8-C: A SHADER WROTE THIS STORE ON THIS SIDE since the R-11 staged copy (or, for
// T0, the coherent map) was last known to match it - set by the draw / dispatch that
// bound it writable (storage block, atomic counter, writable buffer image), cleared
// by SplitHostBytesForCpuRead's refresh and by a respecify. Only the server's own
// CPU readers consult it; the client keeps its own set (GpuWritePending.h).
Bool serverGpuWritten = false;
#endif
};
@@ -1104,6 +1127,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
SizeT end, const char* site);
// P11 B2: THE CPU-READABLE BYTES OF A SPLIT SERVER'S BUFFER TWIN, for the handle-arm
// readers that have no frontend object (indirect commands, primitive-restart
// substitution, the multi-draw index rebase): the R-11 staged copy, or - for a T0 store,
// which has none - this server's own coherent map of the client's AHardwareBuffer, read
// exactly as the monolith arm reads the adopted map through MappedData().
inline const Uint8* SplitHostBytes(const GLESBufferResource& resource) {
if (resource.hostBytes != nullptr) return resource.hostBytes;
return resource.externalAhb != nullptr ? static_cast<const Uint8*>(resource.persistentPtr) : nullptr;
}
// P8-C: THE SAME BYTES, MADE CURRENT FIRST - for a reader that CONSUMES them (the restart
// rewrite, the multi-draw rebase, the *IndirectCount count, an indirect command's
// CPU-fed fields, the XFB scatter's pre-capture bytes). SplitHostBytes above is only
// "what uploads staged": a store a shader wrote on this side (serverGpuWritten) is
// refreshed from this server's own GL buffer - queued uploads flushed into it first,
// then the whole store read back into the staged copy, which also gives an orphaned
// store its first copy - and a T0 store lands its queued resident writes and finishes
// before its coherent map is read. A clean staged store costs one flag test.
// `res` names the record whose descriptor width the copy takes; `site` names the
// reader in the one-time diagnostic when the refresh cannot run.
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
const char* site);
// P8-C: bytes the SERVER itself put into a store's GL buffer and already holds on the CPU
// (an XFB capture it mapped for the client's writeback, a scatter it composed) go into
// the staged copy as well, so no refresh is owed for them. A T0 store has no copy.
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
SizeT offset, SizeT size);
// M-3's WHOLE-STORE predicate, and the GUARD every caller of RequireStagedCoverage over
// a whole-store read has to carry: true when the application DECLARED the store's
// content (glBufferData(size, data)), so a coverage gap is a missing record; false when
@@ -1143,10 +1194,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ("do not take tier 1"), which drops the range onto the staging ring and costs a copy
// rather than a corruption.
//
// `ringCopyInFlight`: the store still has an upload-ring glCopyBufferSubData queued that the
// GPU may not have run (GLESBufferResource::ringCopyRetireSerial not yet retired).
//
// Returns the glMapBufferRange access bits, or 0 when tier 1 must not be taken.
inline constexpr SizeT kEsprytInvalidateRangeMinBytes = 128u * 1024u;
constexpr GLbitfield InvalidateFlushAccessFor(SizeT queuedStart, SizeT queuedEnd, SizeT mapStart,
SizeT mapEnd, SizeT limit, SizeT storageSize) {
SizeT mapEnd, SizeT limit, SizeT storageSize,
Bool ringCopyInFlight) {
if (mapEnd <= mapStart) return 0u;
// THE WIDENING REFUSAL. Not >=, not "covers": exactly, in both directions. A map
// narrower than the queued range leaves bytes unwritten inside a range it has just
@@ -1158,6 +1213,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// page-substitution fast path needs a page-coverable range to engage, and below it
// the driver falls back to waiting out the WAR hazard on the CPU.
if (!wholeBuffer && size < kEsprytInvalidateRangeMinBytes) return 0u;
// A partial range-invalidating map must not overtake a ring copy into this store the GPU
// has not run yet. Adreno 830 answers such a map on a store with a copy still queued by
// losing the copy (dev f973008c, the pull arm's FlushPendingRangesNow: create-instancing
// drew whole Embeddium terrain sections empty, SSIM 0.870). The range goes to the ring
// instead, GPU-ordered behind the copies. The whole-buffer orphan-map is exempt: it
// rewrites every byte from the shadow the queued copies were taken from.
if (!wholeBuffer && ringCopyInFlight) return 0u;
return GL_MAP_WRITE_BIT |
(wholeBuffer ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT);
}
@@ -1267,6 +1329,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Deletes GL buffers whose owning frontend objects died (possibly on a
// thread without a current ES context). Called from draw-time sync.
void ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: the T0 half of that drain - run right after it (retired AHardwareBuffers).
void ProcessDeferredT0Retires();
#endif
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
void BindBufferId(GLenum target, Uint id);
@@ -1813,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;
}
@@ -1963,7 +2032,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// allocation, or capture the old native contents, merge pending
// upload regions and re-arm the server's whole-level replay set.
// First allocations use staged bytes directly. No frontend dirty
// flag, allocator or pixel-store object participates.
// flag, allocator or pixel-store object participates. P9 W2: a level
// the driver does not hold (defined after the last sync) is never read
// back, and a readback the driver refuses falls back to the staged
// level when the store covers it (notes/p9/W2-REMINT.md).
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record);
#endif
@@ -1972,6 +2044,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
// the application floats.
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
// See m_driverMayHoldGpuWrites. Callers holding the STATE texture use
// NoteDriverSideTextureWrite, which also reaches the storage behind a view.
void NoteDriverSideWrite() { m_driverMayHoldGpuWrites = true; }
void Bind(GLenum target, Uint unit = TempTextureUnit);
Uint GetBackendTextureId() const;
@@ -2088,6 +2163,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
// Whether the driver texture may hold texels the shadow never saw. Set the first time
// anything other than a texel upload can write it - an application framebuffer
// attachment, a writable image binding, a copy or blit into it, a driver-side mipmap
// generation - and never cleared. Until then the driver holds, outside a level's
// pending writes, exactly the shadow bytes it was last sent, so those writes may go up
// as their union box (one unpack-ring job, which is what Mali needs); from then on
// only the texels the client wrote may go up, since the shadow is stale everywhere
// else. See SyncMipmapsToBackend.
Bool m_driverMayHoldGpuWrites = false;
// Latches the "this driver has no buffer textures" report to once per texture. The
// report is emitted from the respecify path, which bails before recording the state
// it was asked to apply - so without the latch the texture stays permanently dirty
@@ -2257,6 +2341,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool imageBindableStorageRequired = false);
// Every path that lets the DRIVER write a texture's texels calls this (see
// BackendTextureObject::m_driverMayHoldGpuWrites): an application framebuffer attachment,
// a writable image binding, a copy, blit or mipmap generation into it. A view writes its
// storage texture's image, so the storage texture is the one marked.
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
#if MOBILEGL_PIPE_PUSH
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
// id/tx2) --------------------------------------------------------------------------
@@ -2273,6 +2362,29 @@ namespace MobileGL::MG_Backend::DirectGLES {
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(
MG_Pipe::MGPipeHandle texture, Bool imageBindableStorageRequired = false);
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle texture);
// NoteDriverSideTextureWrite's HANDLE ARM, for the same paths when the caller holds a
// Texture handle (a framebuffer surface's Res, an image unit's Res, VerbMipRes,
// VerbCopyTexDst, a copy-image endpoint): the twin the handle's record names is marked, or
// its storage owner's (Desc.ViewOf) for a view. Nothing on the apply thread reads a
// frontend object for it.
//
// P8-E: under a transport the same storage owner's staged store is marked driver-written
// too (StagedTextureStore::MarkDriverWritten), so a refused readback does not answer from
// bytes the driver has since overwritten - unless `storeFollowsTheWrite`, which only
// glCopyImageSubData passes: it tries FollowCopyImageInStagedStore first and marks the store
// itself when that fails.
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle texture, Bool storeFollowsTheWrite = false);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E (notes/p8/E.md): make glCopyImageSubData's texel-block move in the server's staged
// store too (StagedTextureStore::FollowCopy), so the destination level's bytes stay its
// content. Box in each image's own texel grid. False when the store cannot follow: a cube
// map or 1D array endpoint (copy-image puts faces / layers on z, the store does not), a view,
// two different internal formats, a level whose bytes did not all cross, a source the
// driver has written, or a monolith arm.
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
const IntVec3& destinationOrigin, const IntVec3& size);
#endif
// Brings every texture the next draw reads - the touched units' bindings and the draw
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
@@ -3030,6 +3142,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
// uniform write, and only such a program needs the reset after one.
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-C: same for the gl_BaseInstance uniform - which an indirect draw feeds from the
// command on the CPU only when the mg_IndirectParams view is absent.
Bool ReadsBaseInstance() const { return m_baseInstanceUniformLocation >= 0; }
#endif
// Which viewport indices the next draw's fragments may keep, one bit each. Written
// once per replay pass; see ForEachViewportRoutingPass.
void SetViewportPassMask(Uint32 indexMask) const;
+7 -6
View File
@@ -191,15 +191,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (question == IndexBufferQuestion::Presence) return view;
if (question == IndexBufferQuestion::HostBytes) {
// No Sync* pair here and none is missing: on this side the applier has
// already consumed the persistent-map blocks and the shader writebacks for
// this resource before the draw verb replayed, so the server's staged shadow
// IS the synced copy. That is also why this arm cannot be expressed as "find
// the object and run the monolith body".
// The applier has already consumed the persistent-map blocks for this
// resource before the draw verb replayed; the one Sync* this arm owes is the
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
// a shader wrote on this side is read back from this server's own GL buffer,
// because no writeback ever reaches the server's staged copy. That is also why
// this arm cannot be expressed as "find the object and run the monolith body".
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
view.Size = BufferImpl::ResourceWidthForHandle(res);
view.HostBytes = resource->hostBytes;
view.HostBytes = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
"multidraw_index_rebase");
@@ -1,6 +1,7 @@
// Included in DirectGLES.cpp after the native packed-word readback helper.
// The server needs owned tight rows for a storage promotion, not client PACK
// layout or a fallback into a frontend texture's potentially stale shadow.
// A storage promotion needs the driver's own texels as owned tight rows, not
// client PACK layout or a fallback into a frontend texture's potentially stale
// shadow.
class ScopedWireTextureReadLevel {
public:
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
@@ -280,6 +281,144 @@
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
// its FBO attachment is incomplete - which used to end the session at
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent) {
if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true;
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
GLenum binding = GL_NONE;
switch (nativeTarget) {
case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break;
case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break;
case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
default: return true;
}
// A cube's levels are per face; every other target answers for the whole level.
const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP
? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget;
DrainESErrors();
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(binding, &previous);
g_GLESFuncs.glBindTexture(nativeTarget, texture);
GLint width = 0, height = 0, depth = 0;
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth);
const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR;
g_GLESFuncs.glBindTexture(nativeTarget, static_cast<GLuint>(previous));
if (!answered) return true;
const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
}
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
// the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it.
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) {
static const IntVec3 s_refused = [] {
const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT");
if (value == nullptr) return IntVec3{0, 0, 0};
char* end = nullptr;
const long width = std::strtol(value, &end, 10);
if (end == value || *end != 'x') return IntVec3{0, 0, 0};
const long height = std::strtol(end + 1, nullptr, 10);
if (width <= 0 || height <= 0) return IntVec3{0, 0, 0};
return IntVec3{static_cast<Int>(width), static_cast<Int>(height), 1};
}();
return s_refused.x() > 0 && extent == s_refused;
}
// P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store
// is the only CPU copy of the level on this side, and it answers only while its bytes ARE the
// level: every byte crossed (IsCovered), at the extent asked for, and the texture was never
// written by the driver except by copies the store followed (!IsDriverWritten - see
// StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is
// GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type
// takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row.
// Anything else DECLINES BY NAME: the reply is an error, and the client's
// Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has
// since overwritten.
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
GLenum format, GLenum type, Vector<Uint8>& bytes) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const auto target = static_cast<Uint16>(uploadTarget);
const auto mip = static_cast<Uint16>(level);
ReadbackChannelMapping mapping{};
const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0;
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
static_cast<SizeT>(std::max(extent.z(), 0));
const char* declined = nullptr;
if (!store.CopiesIntoServerStorage()) {
declined = "this arm keeps no staged store";
} else if (store.IsDriverWritten(key)) {
declined = "the driver has written the texture since its bytes crossed";
} else if (!store.IsCovered(key, target, mip)) {
declined = "not every byte of the level crossed";
} else if (store.LevelExtentOrUndefined(key, target, mip) != extent) {
declined = "the staged level is not the extent asked for";
} else if (pixelBytes == 0 || texels == 0) {
declined = "the format/type pair has no readback mapping";
}
Vector<Uint8> out;
if (declined == nullptr) {
const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store");
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) {
declined = "the staged words are not the client's word size";
} else {
out.assign(staged, staged + texels * pixelBytes);
}
} else {
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide,
isInteger, isSigned)) {
declined = "the staged format has no wide decoding";
} else if (mapping.isInteger != isInteger) {
declined = "an integer read of a normalized level, or the reverse";
} else {
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
const SizeT rowTexels = static_cast<SizeT>(extent.x());
const SizeT rows = texels / rowTexels;
const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType);
out.resize(texels * pixelBytes);
for (SizeT row = 0; row < rows; ++row) {
ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes,
out.data() + row * rowTexels * pixelBytes, rowTexels,
wideType, mapping, type);
}
}
}
}
if (declined != nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store "
"cannot answer it (%s) - DECLINED (texture-readback-store-declined)",
res.Slot, res.Gen, level, declined);
return false;
}
MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); "
"answered from the server's staged store (texture-readback-from-store)",
res.Slot, res.Gen, level, static_cast<unsigned>(logicalFormat));
bytes = Move(out);
return true;
}
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
const auto* record = PipeTextureRecordForHandle(info.Res);
if (!record) return false;
@@ -293,17 +432,32 @@
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
const auto logicalFormat = static_cast<TextureInternalFormat>(record->Desc.InternalFormat);
// The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is
// ReadTextureLevelTight's first choice for exactly these reads).
const Bool packedWordRead =
!twin->RequiresImageBindableStorage() &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) &&
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32);
const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent);
Vector<Uint8> result;
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
const auto uploadTarget = wholeCube
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
Vector<Uint8> slice;
if (!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget,
static_cast<TextureInternalFormat>(record->Desc.InternalFormat), info.Level, extent,
twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) return false;
if (refusedForTesting ||
!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level,
extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) {
if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent,
info.Format, info.Type, slice)) return false;
}
result.insert(result.end(), slice.begin(), slice.end());
}
bytes = Move(result);
return true;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
@@ -345,6 +345,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
pVulkanRenderer.reset();
ClearProgramResourceCaches();
}
// And the swap interval it asked for (DirectVulkan.h).
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
ForgetRequestedSwapInterval();
}
}
#else
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
@@ -363,7 +367,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
VulkanRendererConfig config;
config.SwapInterval = GetRequestedSwapInterval();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
return true;
@@ -486,6 +492,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) {
// Same lock as SwapEGLBuffers: the renderer reads the request in Present.
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::SetEGLSwapInterval(interval);
}
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::ReleaseEGLSurface(surface);
@@ -647,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
@@ -727,6 +770,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool funcsTableInitialized = false;
if (!funcsTableInitialized) {
funcsTable.Present = Present;
funcsTable.SetSwapInterval = SetSwapInterval;
funcsTable.GL.DrawArrays = DrawArrays;
funcsTable.GL.DrawElements = DrawElements;
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
@@ -34,6 +34,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void SetEGLSwapInterval(Int interval) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
@@ -0,0 +1,29 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DescriptorPoolCensus.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 <cstdint>
namespace MobileGL::MG_Backend::DirectVulkan {
// A count of Magma's per-frame descriptor pools and the sets they hold (UniformManager), for
// tests. Dependency-free on purpose: the integration scenarios include it next to the system GL
// headers. They link the static library on desktop; the shipping shared library is built with
// hidden visibility, so on Android nothing outside the library can call GetDescriptorPoolCensus.
struct DescriptorPoolCensus {
bool available = false; // false when no Magma renderer is live (e.g. the DirectGLES backend)
uint32_t frameSlots = 0;
uint32_t pools = 0; // descriptor pools across every frame slot
uint32_t maxPoolsInOneSlot = 0;
uint64_t capacitySets = 0; // sum of maxSets over those pools
uint64_t cachedSets = 0; // sets allocated from them and held in the per-layout caches
uint64_t slotTrims = 0; // whole-slot trims since the renderer came up
};
DescriptorPoolCensus GetDescriptorPoolCensus();
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -370,16 +370,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif // !MOBILEGL_BUILD_DISAGGREGATED
#if MOBILEGL_BUILD_DISAGGREGATED
// The verb's handles identify server stores. Readback orders GPU-produced
// command/count bytes before CPU tier expansion; never inspect a client binding.
// The verb's handles identify server stores; never inspect a client binding.
//
// P8-D: THE GPU READS THE WORDS (VulkanRenderer::DrawWireIndirectNative, WireDraw.inc), as
// on the monolith arm. The CPU expansion below is kept for the one shape the monolith arm
// also expands - a COUNT form the device cannot issue natively (no VK_KHR_draw_indirect_count,
// or maxdrawcount > 1 without multiDrawIndirect: VulkanRenderer.cpp's
// MultiDrawElementsIndirectCount) - where ReadWireBuffer orders GPU-produced command/count
// bytes before the expansion.
void DrawWireIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride, Bool indexed, Bool counted = false, GLintptr countOffset = 0) {
if (drawcount <= 0) return;
if (drawcount <= 0 || countOffset < 0) return;
if (pVulkanRenderer->DrawWireIndirectNative(mode, type, reinterpret_cast<Uint64>(indirect), drawcount,
stride, indexed, counted, static_cast<Uint64>(countOffset)))
return;
auto& buffers = pVulkanRenderer->GetWireBufferManager();
const auto& state = MG_Pipe::MGPipeApplier();
// P8-D: this call reads its words on the CPU; the waits those reads take are its own.
struct ExpansionCount {
VkBufferManager& buffers;
const Uint64 waitsBefore = buffers.GetWireHostWaitCount();
~ExpansionCount() {
if (!MG_Util::PipeStats::Enabled()) return;
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectCpuExpansions, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitsIndirect,
buffers.GetWireHostWaitCount() - waitsBefore);
}
} expansionCount{buffers};
if (counted) {
Uint32 count = 0;
if (countOffset < 0 || !buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
if (!buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
static_cast<Uint64>(countOffset), sizeof(count), &count)) return;
drawcount = static_cast<GLsizei>(std::min<Uint32>(count, static_cast<Uint32>(drawcount)));
if (!drawcount) return;
@@ -917,12 +937,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DispatchComputeIndirect(GLintptr indirect) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-SV: THE GPU READS THE GROUP COUNTS on the wire arm too - VulkanRenderer's
// DispatchComputeIndirect takes its transport branch into DispatchWireComputeIndirect
// (WireDraw.inc), which issues vkCmdDispatchIndirect from the verb's store. The CPU read
// that was here (ReadWireBuffer, a whole-GPU wait once a shader had written the store) is gone.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
Uint32 groups[3]{};
if (indirect >= 0 && pVulkanRenderer->GetWireBufferManager().ReadWireBuffer(
MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer,
static_cast<Uint64>(indirect), sizeof(groups), groups))
pVulkanRenderer->DispatchCompute(groups[0], groups[1], groups[2]);
pVulkanRenderer->DispatchComputeIndirect(indirect);
return;
}
#endif
@@ -1741,4 +1761,52 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Util::PipeStats::OnPresent();
}
}
namespace {
Optional<Int> g_requestedSwapInterval;
} // namespace
void SetSwapInterval(Int interval) {
g_requestedSwapInterval = interval;
if (pVulkanRenderer) {
pVulkanRenderer->SetSwapInterval(interval);
}
}
Optional<Int> GetRequestedSwapInterval() {
return g_requestedSwapInterval;
}
#if MOBILEGL_BUILD_DISAGGREGATED
void ForgetRequestedSwapInterval() {
g_requestedSwapInterval.reset();
}
#endif
DescriptorPoolCensus GetDescriptorPoolCensus() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 M2: UNDER A TRANSPORT THE POOLS ARE THE SERVER RENDERER'S, and its apply thread
// grows, rewinds and trims them while the test thread would be reading - a census taken
// after a swap the client ran ahead of, or mid-burst, walks a set cache BeginFrame may be
// clearing. Inproc, the server is a thread of this process: the read is posted to it as
// the test probe, which runs between drain batches, so it sees every record published
// before it and none half-applied - without waiting on the GPU, which is the point of the
// trim case (it gives a burst back with other slots' work in flight). Spawn and tcp keep
// their pools in another process; the client has no renderer and answers unavailable.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
if (MG_Config::Transport != MG_Config::TransportMode::InProcess) return DescriptorPoolCensus{};
DescriptorPoolCensus census{};
const MobileGLResult result = MG_Remote::Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* user) -> MobileGLResult {
*static_cast<DescriptorPoolCensus*>(user) =
pVulkanRenderer ? pVulkanRenderer->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
return MOBILEGL_OK;
},
&census);
return result == MOBILEGL_OK ? census : DescriptorPoolCensus{};
}
#endif
return pVulkanRenderer ? pVulkanRenderer->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -147,4 +147,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
Int64 GetGpuTimestampNs();
void Present();
// eglSwapInterval. Remembered across renderers: one created for a later window surface
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
void SetSwapInterval(Int interval);
Optional<Int> GetRequestedSwapInterval();
#if MOBILEGL_BUILD_DISAGGREGATED
// The swap interval an ended server session asked for is not the next one's, the same rule as
// DirectGLES::ForgetRequestedSwapInterval: under a transport ~BackendObject_DirectVulkan forgets
// it, so the next session starts at the default until it asks.
void ForgetRequestedSwapInterval();
#endif
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,6 +8,8 @@
#include "SwapchainObject.h"
#include <span>
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
@@ -140,12 +142,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
const Vector<VkPresentModeKHR>& availablePresentModes) {
for (auto desiredPresentMode : s_desiredPresentModes) {
for (const auto& presentMode : availablePresentModes) {
if (presentMode == desiredPresentMode) {
return presentMode;
}
const Vector<VkPresentModeKHR>& availablePresentModes, Optional<Int> swapInterval) {
static constexpr VkPresentModeKHR s_unthrottledPresentModes[]{
VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR};
static constexpr VkPresentModeKHR s_vsyncPresentModes[]{VK_PRESENT_MODE_FIFO_KHR};
static constexpr VkPresentModeKHR s_adaptiveVsyncPresentModes[]{VK_PRESENT_MODE_FIFO_RELAXED_KHR,
VK_PRESENT_MODE_FIFO_KHR};
std::span<const VkPresentModeKHR> ranked = s_desiredPresentModes;
if (swapInterval) {
if (*swapInterval == 0) {
ranked = s_unthrottledPresentModes;
} else if (*swapInterval > 0) {
ranked = s_vsyncPresentModes;
} else {
ranked = s_adaptiveVsyncPresentModes;
}
}
for (auto desiredPresentMode : ranked) {
if (std::find(availablePresentModes.begin(), availablePresentModes.end(), desiredPresentMode) !=
availablePresentModes.end()) {
return desiredPresentMode;
}
}
@@ -153,9 +169,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return availablePresentModes[0];
}
const char* SwapchainObject::GetPresentModeName(VkPresentModeKHR presentMode) {
return string_VkPresentModeKHR(presentMode);
}
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
VkExtent2D desiredExtent) {
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
"SwapchainObject::Create failed: incomplete swapchain capabilities");
@@ -174,8 +194,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes, swapInterval);
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
m_presentMode = presentMode;
m_supportedPresentModes = swapchainCapabilities.presentModes;
const auto& swapchainCaps = swapchainCapabilities.capabilities;
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
@@ -26,13 +26,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
// swapInterval is the app's eglSwapInterval, empty until it asks (s_desiredPresentModes
// ranks the modes then). 0 wants IMMEDIATE, else MAILBOX, else FIFO; >= 1 wants FIFO,
// which cannot skip vblanks, so 2 and up pace like 1; a negative interval (adaptive
// vsync) wants FIFO_RELAXED, else FIFO.
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes,
Optional<Int> swapInterval = Nullopt);
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
Optional<Int> swapInterval);
void Shutdown(VkDevice device);
VkSwapchainKHR GetHandle() const { return m_swapchain; }
VkPresentModeKHR GetPresentMode() const { return m_presentMode; }
// The surface's present modes as queried when this swapchain was created.
const Vector<VkPresentModeKHR>& GetSupportedPresentModes() const { return m_supportedPresentModes; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
@@ -79,6 +89,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
Vector<VkPresentModeKHR> m_supportedPresentModes;
VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
@@ -591,6 +591,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_samplerResolveMemoHighWater = 0;
m_setsPerFrame = setsPerFrame;
m_peakDescriptorSetsObserved = 0;
m_trimQuietEpochs = MG_Config::Features.MagmaDescriptorTrimFrames;
// The renderer records its first frame into slot 0 before any BeginFrame runs.
m_epochSlot = 0;
m_epochOpen = true;
m_descriptorSlotTrims = 0;
m_textureManager = textureManager;
m_samplerManager = samplerManager;
@@ -600,6 +605,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
frame.capacitySets = m_setsPerFrame;
frame.quietEpochs = 0;
frame.descriptorPools.clear();
VkDescriptorPool initialPool = VK_NULL_HANDLE;
@@ -654,6 +661,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
frame.capacitySets = 0;
frame.quietEpochs = 0;
}
m_frames.clear();
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -662,6 +671,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_wirePrivateStoragePlaceholders = 0;
m_wireInvalidStorageImagesBindNull = false;
#endif
m_epochSlot = 0;
m_epochOpen = false;
m_bufferManager = nullptr;
m_programFactory = nullptr;
@@ -701,6 +712,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"UniformDescriptorBinder: new descriptor set peak observed=%u (base setsPerFrame=%u, frame=%u, pools=%zu)",
m_peakDescriptorSetsObserved, m_setsPerFrame, frameIndex, frame.descriptorPools.size());
}
// The epoch that just ended is the one of the slot begun last - this slot again on the
// drain path, the previous frame's slot on the Present path. Its peak is still unreset.
if (m_epochOpen && m_epochSlot < m_frames.size()) {
NoteEndedEpochForTrim(m_frames[m_epochSlot].peakAllocatedSetsThisFrame);
}
m_epochSlot = frameIndex;
m_epochOpen = true;
// No pending command buffer references this slot's sets here (see TrimFrameDescriptorPools
// for why that holds at both of BeginFrame's call sites), so a quiet grown slot can be
// handed back whole.
if (m_trimQuietEpochs != 0 && frame.descriptorPools.size() > 1 &&
frame.capacitySets >= kDescriptorTrimMinCapacity && frame.quietEpochs >= m_trimQuietEpochs) {
TrimFrameDescriptorPools(frame, frameIndex);
}
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
@@ -709,11 +734,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
// The frame's descriptor sets are recycled above, so last frame's reuse targets
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
InvalidateDescriptorSetMemos();
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a
// resolve has actually written can be valid, so the high-water mark bounds the
@@ -747,10 +768,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.second.cursor = 0;
}
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
for (auto& entry : m_descriptorReuseMemo) entry.valid = false;
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
// The PEAK is not reset: a rewind ends a window of the epoch, not the epoch. The trim scores
// an epoch by the most sets the slot held at once in it (NoteEndedEpochForTrim at the next
// BeginFrame), and on this arm that is the largest window - resetting here would score a
// burst frame by its last window alone and trim a slot that re-grows every frame.
InvalidateDescriptorSetMemos();
const Uint32 touchedBindings =
std::min<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
@@ -761,6 +783,68 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
#endif
void UniformManager::InvalidateDescriptorSetMemos() {
// Every VkDescriptorSet handle held outside the per-layout caches - in this backend they all
// live in this class. A trim can hand a destroyed set's handle value to a fresh allocation,
// so the bind-dedup shadow must drop too, not only the two reuse memos.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
}
void UniformManager::NoteEndedEpochForTrim(Uint32 epochPeakSets) {
for (auto& slot : m_frames) {
if (static_cast<Uint64>(epochPeakSets) * kDescriptorTrimHeadroom > slot.capacitySets) {
slot.quietEpochs = 0;
} else if (slot.quietEpochs < std::numeric_limits<Uint32>::max()) {
++slot.quietEpochs;
}
}
}
void UniformManager::TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex) {
const SizeT poolsBefore = frame.descriptorPools.size();
const Uint64 capacityBefore = frame.capacitySets;
// Holders first: the per-layout cache and every memo that may name one of these sets.
frame.descriptorSetCacheByLayout.clear();
InvalidateDescriptorSetMemos();
for (SizeT i = 1; i < frame.descriptorPools.size(); ++i) {
if (frame.descriptorPools[i].handle != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(m_device, frame.descriptorPools[i].handle, nullptr);
}
}
frame.descriptorPools.resize(1);
auto& base = frame.descriptorPools[0];
// Frees every set allocated from it; the spec defines no failure for vkResetDescriptorPool.
vkResetDescriptorPool(m_device, base.handle, 0);
base.allocatedSets = 0;
frame.activeDescriptorPoolIndex = 0;
frame.capacitySets = base.maxSets;
frame.quietEpochs = 0;
++m_descriptorSlotTrims;
MGLOG_D("UniformDescriptorBinder: frame %u trimmed its descriptor pools (%zu pools / %llu sets -> 1 / %u)",
frameIndex, poolsBefore, static_cast<unsigned long long>(capacityBefore), base.maxSets);
}
DescriptorPoolCensus UniformManager::GetDescriptorPoolCensus() const {
DescriptorPoolCensus census;
census.available = true;
census.frameSlots = static_cast<uint32_t>(m_frames.size());
for (const auto& frame : m_frames) {
const auto poolCount = static_cast<uint32_t>(frame.descriptorPools.size());
census.pools += poolCount;
census.maxPoolsInOneSlot = std::max(census.maxPoolsInOneSlot, poolCount);
census.capacitySets += frame.capacitySets;
for (const auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
census.cachedSets += cacheEntryPair.second.sets.size();
}
}
census.slotTrims = m_descriptorSlotTrims;
return census;
}
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
SizeT purgedSets = 0;
for (auto& frame : m_frames) {
@@ -2700,9 +2784,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
MOBILEGL_ASSERT(bufferObject != nullptr,
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
static thread_local Vector<Uint8> paddedUbo;
if (bufferObject == nullptr) {
// A block the program declares at a point with nothing bound - never bound, or its
// buffer was deleted while bound, which unbinds it (UnboundUniformBlockScenario,
// DeletedBoundBufferScenario). GL leaves what the shader READS undefined and raises no
// error; this used to dereference the null object. Answer it with a zeroed block of the
// reflected size, as the wire arm does (ResolveWireUniformBufferPayload).
const VkDeviceSize blockSize =
static_cast<VkDeviceSize>(program.GetUBOSizeAt(static_cast<Uint32>(blockIndex)));
paddedUbo.assign(static_cast<SizeT>(blockSize > 0 ? blockSize : 16), 0);
out.payload = paddedUbo.data();
out.payloadSize = static_cast<VkDeviceSize>(paddedUbo.size());
return true;
}
bufferObject->SyncPersistentMappedRange();
MOBILEGL_ASSERT(bufferObject->MappedData() != nullptr && bufferObject->GetSize() != 0,
@@ -2735,7 +2830,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outSize = blockSize;
outData = bufferObject->MappedData() + static_cast<SizeT>(rangeStart);
if (available < blockSize) {
static thread_local Vector<Uint8> paddedUbo;
paddedUbo.assign(static_cast<SizeT>(blockSize), 0);
Memcpy(paddedUbo.data(), outData, static_cast<SizeT>(available));
outData = paddedUbo.data();
@@ -2831,32 +2925,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Uint32 UniformManager::NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
Uint32 initialMaxSets) {
const Uint32 minSets = std::max<Uint32>(1, initialMaxSets);
const Uint32 maxSets = std::max(minSets, kMaxDescriptorPoolSets);
Uint32 largest = 0;
for (const auto& pool : pools) {
if (pool.updateAfterBind == updateAfterBind) {
largest = std::max(largest, pool.maxSets);
}
}
if (largest == 0) {
return minSets;
}
const Uint64 doubled = static_cast<Uint64>(largest) * 2;
return static_cast<Uint32>(std::clamp<Uint64>(doubled, minSets, maxSets));
}
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
if (frame.descriptorPools.empty()) {
return false;
}
const auto matchingBucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
? std::max<Uint32>(1, matchingBucket->maxSets)
: m_setsPerFrame;
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
: currentMaxSets;
Uint32 grownMaxSets = NextDescriptorPoolMaxSets(frame.descriptorPools, updateAfterBind, m_setsPerFrame);
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
// A large pool is a large driver allocation; if the device refuses one, settle for less
// rather than failing the draw - halving down to the base size, where the old policy lived.
while (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
if (grownMaxSets <= m_setsPerFrame) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u sets)",
grownMaxSets);
return false;
}
grownMaxSets = std::max(m_setsPerFrame, grownMaxSets / 2);
}
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
MGLOG_D(
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
frameIndex, currentMaxSets, grownMaxSets, frame.descriptorPools.size());
frame.capacitySets += grownMaxSets;
// Growing is the opposite of quiet: a trim must wait a full quiet run from here.
frame.quietEpochs = 0;
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool exhausted, grew a %u-set pool (updateAfterBind=%d), "
"poolCount=%zu",
frameIndex, grownMaxSets, updateAfterBind ? 1 : 0, frame.descriptorPools.size());
return true;
}
@@ -14,6 +14,7 @@
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "WirePlaceholderKey.h"
#include "../DescriptorPoolCensus.h"
#include "../VkIncludes.h"
#include <Includes.h>
@@ -84,9 +85,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
// references its sets. This is the only PER-LAYOUT eviction path for the
// caches - a live layout's entry must never be purged on its own (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here. The
// other path, TrimFrameDescriptorPools, drops a slot's whole cache together with
// the pools behind it.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
// in binding order, and within a binding in array-element order): the resolved texture
@@ -158,13 +161,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
private:
// ---- per-frame descriptor pools ----------------------------------------------------
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
// added by GrowFrameDescriptorPool when an allocation found no room. A pool serves one
// updateAfterBind flavour, because a set can only come from a pool created with the flag
// its layout was.
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
Bool updateAfterBind = false;
};
// One pool's maxSets never exceeds this. It bounds one pool's driver memory: CreateDescriptorPool
// declares 8 descriptors of each of 6 types per set, so a 4096-set pool asks for 196,608
// descriptors - about 12 MiB at 64 bytes each on a driver that backs every declared one.
static constexpr Uint32 kMaxDescriptorPoolSets = 4096;
// The growth policy, pure so the host tests can drive it: the size of the next pool a slot
// adds for `updateAfterBind` layouts, given the slot's existing pools. Twice the LARGEST pool
// of that flavour, capped at kMaxDescriptorPoolSets; `initialMaxSets` when the slot has no
// pool of that flavour yet. So a frame that needs N sets adds O(log N) pools while N stays
// under the cap, and one more pool per kMaxDescriptorPoolSets sets beyond it. Taking the
// largest (not the first) pool is the point: the first same-flavour pool is the oldest,
// smallest one, and doubling IT made every grown pool the same size.
static Uint32 NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
Uint32 initialMaxSets);
// Trim policy (see TrimFrameDescriptorPools). A slot is "quiet" in an epoch whose peak set
// count times kDescriptorTrimHeadroom still fits its capacity; a slot that has been quiet for
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES consecutive epochs, and holds at least
// kDescriptorTrimMinCapacity sets, is trimmed. The headroom is what keeps the trim from
// thrashing: geometric growth leaves a re-grown slot at least a third full (the pool a growth
// adds is at most twice the largest before it, so at most twice everything the slot held, and
// the epoch that grew it needed more than that), so the epoch that grew a slot is never quiet
// for it. The floor keeps a slot sized for ordinary
// frames from being trimmed at all - the trim exists to give back a burst.
static constexpr Uint32 kDescriptorTrimHeadroom = 4;
static constexpr Uint32 kDescriptorTrimMinCapacity = 1024;
// For tests (DescriptorPoolCensus.h): pools, capacity, cached sets and trims over every slot.
DescriptorPoolCensus GetDescriptorPoolCensus() const;
private:
// A cached descriptor set together with the pool it was allocated from, so a
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
@@ -239,6 +275,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
#endif
// descriptorSetCacheByLayout is the per-frame set cache: BeginFrame rewinds every cursor, so
// a slot re-writes the sets it allocated in earlier frames instead of allocating new ones, and
// the cache only ever grows - except through TrimFrameDescriptorPools, which gives a slot's
// whole burst back at once (and OnDescriptorSetLayoutDestroyed, one dead layout at a time).
struct FrameResources {
Vector<DescriptorPoolBucket> descriptorPools;
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
@@ -252,6 +292,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
// Sum of maxSets over descriptorPools, kept in step by Initialize, Grow and Trim.
Uint64 capacitySets = 0;
// Consecutive epochs (see NoteEndedEpochForTrim) that were quiet for THIS slot's capacity.
Uint32 quietEpochs = 0;
};
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
@@ -395,6 +439,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
// An epoch is the stretch between two BeginFrame calls: one frame, or the part of one between
// two drains (VulkanRenderer::TryDrainFrameTransients). Called by BeginFrame with the epoch
// that just ended, it updates every slot's quiet count - a burst in any slot counts against
// all of them, so bursts that land in the other slots still hold a slot's trim off.
void NoteEndedEpochForTrim(Uint32 epochPeakSets);
// Gives a slot's whole descriptor burst back: clears its per-layout cache, destroys every
// grown pool and resets the base pool, so the slot re-grows geometrically from setsPerFrame.
// Whole-slot rather than per-set: vkFreeDescriptorSets fragments a pool and can never empty
// a grown one. Called only from BeginFrame, which runs only when no pending command buffer
// references the slot's sets:
// - Present path (VulkanRenderer::Present, after WaitAndAcquireNextImage): the slot's
// imageInFlightFence has been waited. That fence rides the slot's LAST submission - the
// Present submit - and every mid-frame submission of the slot (FlushPendingCommands,
// SplitOversizedRecording, readbacks) went to the same queue earlier, so the fence's
// signal covers them (vkQueueSubmit's fence scope includes all earlier submissions).
// - Drain path (VulkanRenderer::TryDrainFrameTransients): every submission has been
// observed complete and no recording is open.
// Sets are per slot: a slot's sets are only ever bound into recordings made while it is the
// current slot, so trimming one slot never touches another slot's in-flight work.
void TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex);
// Drops every handle this class holds to a descriptor set outside the per-layout caches.
void InvalidateDescriptorSetMemos();
VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex,
@@ -412,6 +478,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_maxBindings = 0;
Uint32 m_setsPerFrame = 0;
Uint32 m_peakDescriptorSetsObserved = 0;
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES, latched at Initialize (0 = never trim).
Uint32 m_trimQuietEpochs = 0;
// The slot whose epoch is open (the last one BeginFrame was called for); valid once
// m_epochOpen is set. Its peak is the ended epoch's peak at the next BeginFrame.
Uint32 m_epochSlot = 0;
Bool m_epochOpen = false;
Uint64 m_descriptorSlotTrims = 0;
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
@@ -15,7 +15,14 @@
#include "MG_Pipe/MGPipeCallbacks.h"
#include "MG_Pipe/PipeApply.h"
#include "MG_Remote/Server/StagedShadow.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include "MG_Remote/Transport/AdoptT0.h"
#endif
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#include <Config.h>
#include <chrono>
#include <cstdlib>
#endif
@@ -206,6 +213,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// modified ranges. A server pointer is never donated across the wire.
.MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; },
.UnmapPersistent = [](MG_Pipe::MGPipeHandle) {},
#if defined(__ANDROID__)
// P11 B2 (T0): the one door a server pointer is still never donated through stays shut;
// what T0 adds is the other direction - the CLIENT's pages imported as the store.
.ImportExternal = [](MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) -> Bool {
return WireManager().ImportWireBuffer(res, ahb, size);
},
.SelfTestExternal = [](char* why, Uint64 whyBytes) -> Bool {
return VkBufferManager::SelfTestWireImport(why, whyBytes);
},
#endif
};
#endif
} // namespace
@@ -245,11 +262,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// to wait for a submission (DeferredWireRelease), and three lines from now the record
// no longer carries it.
const Uint64 orphanedUseSerial = resource->lastUseSerial;
DeferWireRelease(std::move(resource->buffer), orphanedUseSerial);
// P11 B2: a T0 store is ALWAYS released through the serial-gated list (never "at once"),
// so the client's AHardwareBuffer outlives every submission that can still name it however
// the client's own release (at this respecify) raced the frames in flight.
DeferWireRelease(std::move(resource->buffer),
resource->imported ? T0ReleaseSerial(orphanedUseSerial) : orphanedUseSerial);
resource->imported = false;
resource->size = desc.Width;
resource->lastUseSerial = 0;
resource->lastUseSubmitIndex = 0; // M2 r2: the new store carries no submission yet (B3's stamp is per store)
resource->gpuWritesPending = false;
resource->indirectReadBarrierPending = false;
resource->stagedCoverage.clear();
++m_sliceEpochCounter;
if (resource->size == 0) return;
@@ -294,7 +317,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT;
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 1, &barrier, 0, nullptr, 0, nullptr);
if (!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
// P8-D: counted here, past the idle early-out, so `whw` is waits TAKEN, not calls.
++m_wireHostWaits;
const Bool timed = MG_Util::PipeStats::Enabled();
const auto waitStart = timed ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const Bool waited =
pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true);
if (timed) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaits, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitMicros,
static_cast<Uint64>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - waitStart).count()));
}
if (!waited) {
return false;
}
resource.lastUseSerial = 0;
@@ -537,11 +572,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferManager::ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) {
Vector<Uint8> bytes(static_cast<SizeT>(size));
if (!ReadWireBuffer(res, offset, size, bytes.data())) {
// P11 B2: A T0 STORE'S READBACK IS "DONE, NO BYTES". Its pages are the client's
// AHardwareBuffer, so what the client needs before it reads them is this server's GPU
// having finished every write to them - the resident copies WriteWireBuffer recorded and
// any shader write - and nothing crossing SEG_EVENT. The wait is the store's own host-access
// wait (a barrier to host, then the sync point's submission); the reply then says "done".
// One death for both shapes: the store could not be made readable.
auto* resource = FindWireBuffer(res);
const Bool t0 = resource != nullptr && resource->imported;
Vector<Uint8> bytes(t0 ? 0 : static_cast<SizeT>(size));
if (t0 ? !WaitForWireBufferHostAccess(*resource) : !ReadWireBuffer(res, offset, size, bytes.data())) {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen);
std::abort();
}
if (t0) {
++m_sliceEpochCounter;
return;
}
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-writeback-callback\"}");
std::abort();
@@ -561,6 +608,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->lastUseSerial = m_frameSerial;
resource->stagedCoverage.clear();
resource->gpuWritesPending = true;
resource->indirectReadBarrierPending = true;
++m_sliceEpochCounter;
// The existing client dirty-state protocol accepts one whole-store
// notification. The store remains on the GPU and exact later SubData
@@ -573,10 +621,312 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range);
}
Bool VkBufferManager::TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res) {
auto* resource = FindWireBuffer(res);
if (!resource || !resource->indirectReadBarrierPending) return false;
resource->indirectReadBarrierPending = false;
return true;
}
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A WIRE STORE -----------------------------
// The serial a T0 store is released under: its last use, or - when no GPU command has named it
// since a host-access wait - the current frame's, so the release still waits for the sync
// point rather than taking DeferWireRelease's "never named, destroy now" arm. Conservative by
// at most one submission.
Uint64 VkBufferManager::T0ReleaseSerial(Uint64 lastUseSerial) const {
if (lastUseSerial != 0) return lastUseSerial;
return m_frameSerial != 0 ? m_frameSerial : 1;
}
#if defined(__ANDROID__)
namespace {
Int32 T0PickMemoryType(const VkPhysicalDeviceMemoryProperties& props, Uint32 bits,
VkMemoryPropertyFlags want) {
for (Uint32 i = 0; i < props.memoryTypeCount; ++i) {
if ((bits & (1u << i)) != 0 && (props.memoryTypes[i].propertyFlags & want) == want)
return static_cast<Int32>(i);
}
return -1;
}
struct T0ImportedBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
void* mapped = nullptr;
Bool coherent = true;
};
void T0DestroyImported(VkDevice device, T0ImportedBuffer& imported) {
if (imported.mapped != nullptr) vkUnmapMemory(device, imported.memory);
if (imported.buffer != VK_NULL_HANDLE) vkDestroyBuffer(device, imported.buffer, nullptr);
if (imported.memory != VK_NULL_HANDLE) vkFreeMemory(device, imported.memory, nullptr);
imported = T0ImportedBuffer{};
}
// A VkBuffer on a dedicated allocation IMPORTED from `ahb`, in a host-visible memory type
// (coherent preferred), mapped whole. The server's own host map is what WriteWireBuffer's
// idle path and every CPU reader of a wire store use, so a type without HOST_VISIBLE is a
// refusal, not a degraded import. Nothing is left behind on failure.
Bool T0ImportAhb(const VulkanRenderer::WireAhbImport& ctx, void* ahb, Uint64 size, VkBufferUsageFlags usage,
T0ImportedBuffer& out, String& why) {
out = T0ImportedBuffer{};
const auto getProperties =
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
VkAndroidHardwareBufferPropertiesANDROID properties{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
VkResult result = getProperties(ctx.device, static_cast<const AHardwareBuffer*>(ahb), &properties);
if (result != VK_SUCCESS) {
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(result));
return false;
}
VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bufferInfo.pNext = &external;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
result = vkCreateBuffer(ctx.device, &bufferInfo, nullptr, &out.buffer);
if (result != VK_SUCCESS) {
out.buffer = VK_NULL_HANDLE;
why = "vkCreateBuffer(external AHB) -> " + std::to_string(static_cast<int>(result));
return false;
}
Int32 type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
out.coherent = type >= 0;
if (type < 0)
type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
if (type < 0) {
why = "no host-visible memory type can import it (memoryTypeBits 0x" +
std::to_string(properties.memoryTypeBits) + ")";
T0DestroyImported(ctx.device, out);
return false;
}
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
dedicated.buffer = out.buffer;
VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import.buffer = static_cast<AHardwareBuffer*>(ahb);
import.pNext = &dedicated;
VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocateInfo.pNext = &import;
allocateInfo.allocationSize = properties.allocationSize;
allocateInfo.memoryTypeIndex = static_cast<Uint32>(type);
result = vkAllocateMemory(ctx.device, &allocateInfo, nullptr, &out.memory);
if (result != VK_SUCCESS) {
out.memory = VK_NULL_HANDLE;
why = "vkAllocateMemory(import AHB) -> " + std::to_string(static_cast<int>(result));
T0DestroyImported(ctx.device, out);
return false;
}
result = vkBindBufferMemory(ctx.device, out.buffer, out.memory, 0);
if (result == VK_SUCCESS) result = vkMapMemory(ctx.device, out.memory, 0, VK_WHOLE_SIZE, 0, &out.mapped);
if (result != VK_SUCCESS || out.mapped == nullptr) {
out.mapped = nullptr;
why = "vkBindBufferMemory/vkMapMemory -> " + std::to_string(static_cast<int>(result));
T0DestroyImported(ctx.device, out);
return false;
}
return true;
}
} // namespace
#endif
Bool VkBufferManager::ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
#if defined(__ANDROID__)
auto* resource = FindWireBuffer(res);
if (resource == nullptr || ahb == nullptr || size == 0 || size != resource->size) return false;
VulkanRenderer::WireAhbImport ctx;
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) {
MGLOG_W_ONCE("Magma T0: this device did not take VK_ANDROID_external_memory_android_hardware_buffer");
return false;
}
VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
T0ImportedBuffer imported;
String why;
if (!T0ImportAhb(ctx, ahb, size, usage, imported, why)) {
MGLOG_W("Magma T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen,
static_cast<unsigned long long>(size), why.c_str());
return false;
}
// The T2 store it replaces goes the way a respecify's does (and any draw still naming it
// keeps it until its submission completes).
DeferWireRelease(std::move(resource->buffer), resource->lastUseSerial);
MG_Remote::Transport::AdoptT0::AcquireImported(ahb);
resource->buffer.AdoptExternal(ctx.device, imported.buffer, imported.memory, size, imported.mapped,
imported.coherent, ahb);
resource->imported = true;
resource->lastUseSerial = 0;
resource->lastUseSubmitIndex = 0;
resource->gpuWritesPending = false;
// The whole store holds bytes the client supplied (its AHB, seeded from its shadow).
resource->stagedCoverage.clear();
MG_Remote::Server::StagedShadowStore::CoverageAdd(resource->stagedCoverage, 0, static_cast<SizeT>(size));
++m_sliceEpochCounter;
++m_wireStoreCount;
NoteWireStorePeaks();
PublishWireReclaimGauges();
MGLOG_D("Magma T0: imported {slot=%u, gen=%u} (%llu bytes, %s memory)", res.Slot, res.Gen,
static_cast<unsigned long long>(size), imported.coherent ? "coherent" : "non-coherent");
return true;
#else
(void)res;
(void)ahb;
(void)size;
return false;
#endif
}
// THE POST (B2; decided by probe, never by driver name). The AHB API promises CPU coherence only
// across an unlock/lock, and T0 holds the client's lock for the store's life, so this exercises
// that pattern on THIS device before kCapAdoptT0 may be published:
// hold - a 64 KiB BLOB allocated and locked by the test, never unlocked while it runs;
// read - this server's own host map of the import reads what the CPU wrote through the lock;
// gpu - one submission copies that pattern out of the import (the GPU read) and fills another
// region of it (the GPU write), then a transfer->host barrier and a fence wait;
// back - the fill is read through the HELD pointer, the copy from a host-visible staging buffer.
Bool VkBufferManager::SelfTestWireImport(char* whyOut, Uint64 whyBytes) {
const auto answer = [&](Bool ok, const String& text) {
std::snprintf(whyOut, static_cast<SizeT>(whyBytes), "%s", text.c_str());
return ok;
};
#if defined(__ANDROID__)
VulkanRenderer::WireAhbImport ctx;
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx))
return answer(false, "the device did not take VK_ANDROID_external_memory_android_hardware_buffer");
constexpr Uint64 kBytes = 64 * 1024;
constexpr Uint32 kWords = 1024; // one 4 KiB region
constexpr Uint64 kFillAt = 4 * 4096; // region 4
constexpr Uint32 kFillWord = 0x6C0FFEE0u;
Vector<Uint32> pattern(kBytes / 4);
for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast<Uint32>(i * 2654435761u);
MG_Remote::Transport::AdoptT0::HeldStore held;
String why;
if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why))
return answer(false, "hold: " + why);
T0ImportedBuffer imported, staging;
VkCommandPool pool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
String verdict;
Bool ok = false;
do {
if (!T0ImportAhb(ctx, held.ahb, kBytes,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
imported, why)) {
verdict = "import: " + why;
break;
}
if (!imported.coherent) {
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = imported.memory;
range.size = VK_WHOLE_SIZE;
vkInvalidateMappedMemoryRanges(ctx.device, 1, &range);
}
if (std::memcmp(imported.mapped, pattern.data(), kWords * 4) != 0) {
verdict = "read: the server's host map of the import does not hold the CPU's pattern";
break;
}
// A host-visible staging buffer for the GPU read's destination.
VkBufferCreateInfo stagingInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
stagingInfo.size = kWords * 4;
stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(ctx.device, &stagingInfo, nullptr, &staging.buffer) != VK_SUCCESS) {
staging.buffer = VK_NULL_HANDLE;
verdict = "staging buffer";
break;
}
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(ctx.device, staging.buffer, &requirements);
const Int32 stagingType = T0PickMemoryType(ctx.memory, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VkMemoryAllocateInfo stagingAlloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
stagingAlloc.allocationSize = requirements.size;
stagingAlloc.memoryTypeIndex = static_cast<Uint32>(stagingType < 0 ? 0 : stagingType);
if (stagingType < 0 || vkAllocateMemory(ctx.device, &stagingAlloc, nullptr, &staging.memory) != VK_SUCCESS ||
vkBindBufferMemory(ctx.device, staging.buffer, staging.memory, 0) != VK_SUCCESS ||
vkMapMemory(ctx.device, staging.memory, 0, VK_WHOLE_SIZE, 0, &staging.mapped) != VK_SUCCESS) {
verdict = "staging memory";
break;
}
std::memset(staging.mapped, 0, kWords * 4);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.queueFamilyIndex = ctx.queueFamily;
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
VkCommandBuffer commands = VK_NULL_HANDLE;
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandInfo.commandBufferCount = 1;
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (vkCreateCommandPool(ctx.device, &poolInfo, nullptr, &pool) != VK_SUCCESS) {
pool = VK_NULL_HANDLE;
verdict = "command pool";
break;
}
commandInfo.commandPool = pool;
if (vkAllocateCommandBuffers(ctx.device, &commandInfo, &commands) != VK_SUCCESS ||
vkCreateFence(ctx.device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
verdict = "command buffer / fence";
break;
}
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commands, &begin);
VkMemoryBarrier hostWrites{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
hostWrites.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
hostWrites.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&hostWrites, 0, nullptr, 0, nullptr);
VkBufferCopy region{0, 0, kWords * 4};
vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, &region);
vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kFillWord);
VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost,
0, nullptr, 0, nullptr);
vkEndCommandBuffer(commands);
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &commands;
if (vkQueueSubmit(ctx.queue, 1, &submit, fence) != VK_SUCCESS ||
vkWaitForFences(ctx.device, 1, &fence, VK_TRUE, 2'000'000'000ull) != VK_SUCCESS) {
verdict = "gpu: the submission did not complete within 2 s";
break;
}
Uint32 gpuRead = 0, heldSaw = 0;
const auto* copied = static_cast<const Uint32*>(staging.mapped);
const auto* heldWords = static_cast<const Uint32*>(held.ptr);
for (Uint32 i = 0; i < kWords; ++i) {
if (copied[i] == pattern[i]) ++gpuRead;
if (heldWords[kFillAt / 4 + i] == kFillWord) ++heldSaw;
}
ok = gpuRead == kWords && heldSaw == kWords;
verdict = "64 KiB AHB, lock held throughout: host map read ok (" +
String(imported.coherent ? "coherent" : "non-coherent") + " memory), GPU read " +
std::to_string(gpuRead) + "/1024 words of the CPU's pattern, GPU write seen through the "
"held lock " + std::to_string(heldSaw) + "/1024 words";
} while (false);
if (fence != VK_NULL_HANDLE) vkDestroyFence(ctx.device, fence, nullptr);
if (pool != VK_NULL_HANDLE) vkDestroyCommandPool(ctx.device, pool, nullptr);
T0DestroyImported(ctx.device, staging);
T0DestroyImported(ctx.device, imported);
MG_Remote::Transport::AdoptT0::ReleaseHeld(held);
return answer(ok, verdict);
#else
return answer(false, "no AHardwareBuffer on this platform");
#endif
}
void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) {
const auto found = m_wireBuffers.find(WireBufferKey(res));
if (found == m_wireBuffers.end()) return;
DeferWireRelease(std::move(found->second.buffer), found->second.lastUseSerial);
DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify
m_wireBuffers.erase(found);
++m_sliceEpochCounter;
PublishWireReclaimGauges();
@@ -125,6 +125,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
// P8-D: whether a shader write was marked on this store since the last indirect draw
// read it natively, clearing the mark. The caller records the INDIRECT_COMMAND_READ
// barrier before the draw when this answers true. Transfer writes need no mark: their own
// after-barriers already make them visible to MEMORY_READ at ALL_COMMANDS.
Bool TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res);
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
// the attribution base a caller reads before and after its own ReadWireBuffer.
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
// Resource-op entry points. All run on the server apply owner.
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
@@ -134,6 +142,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
void DestroyWireBuffer(MG_Pipe::MGPipeHandle res);
// P11 B2 (T0): make the client's AHardwareBuffer `ahb` (a BLOB of `size` bytes) buffer
// `res`'s store - a VkBuffer on memory imported through
// VK_ANDROID_external_memory_android_hardware_buffer, host-visible and mapped like every
// wire store, so every wire path (draw binds, WriteWireBuffer's ordered copy, CPU readers)
// applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not
// imported (the T2 store is untouched and the client is DECLINED).
Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size);
// The POST self-test of the sustained-lock pattern on this device (see the definition).
static Bool SelfTestWireImport(char* why, Uint64 whyBytes);
#endif
// Recreate all per-frame transient arenas
@@ -245,12 +262,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Only ranges actually submitted by resource_subdata are covered. No
// shadow is retained: flush cannot replay stale bytes over GPU writes.
Vector<Range1D> stagedCoverage;
// P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts
// no bytes (the client reads its own pages once the wait below it is done).
Bool imported = false;
// P8-D: a shader write was marked (MarkWireBufferGpuWritten) and no indirect draw has
// recorded its INDIRECT_COMMAND_READ barrier since (TakeWireIndirectReadBarrier).
Bool indirectReadBarrierPending = false;
};
// P8-D: see GetWireHostWaitCount.
Uint64 m_wireHostWaits = 0;
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
}
WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res);
Bool WaitForWireBufferHostAccess(WireBufferResource& resource);
// P11 B2: the serial a T0 store's release waits behind (never "destroy at once").
Uint64 T0ReleaseSerial(Uint64 lastUseSerial) const;
// P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME
// BOUNDARY.
@@ -8,13 +8,45 @@
#include "VkBufferObject.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Transport/AdoptT0.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
void VkBufferObject::MoveExternalFrom(VkBufferObject& other) {
m_device = other.m_device;
m_externalMemory = other.m_externalMemory;
m_externalAhb = other.m_externalAhb;
m_externalCoherent = other.m_externalCoherent;
other.m_device = VK_NULL_HANDLE;
other.m_externalMemory = VK_NULL_HANDLE;
other.m_externalAhb = nullptr;
other.m_externalCoherent = true;
}
void VkBufferObject::AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb) {
Destroy();
m_device = device;
m_buffer = buffer;
m_externalMemory = memory;
m_mappedData = mapped;
m_size = size;
m_externalCoherent = coherent;
m_externalAhb = ahb;
}
#endif
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -35,6 +67,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -89,6 +124,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Destroy() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
// P11 B2 (T0): an imported AHardwareBuffer store. Reached through the wire arm's
// deferred release, i.e. once no submission can still name it.
if (m_mappedData != nullptr) vkUnmapMemory(m_device, m_externalMemory);
if (m_buffer != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_buffer, nullptr);
vkFreeMemory(m_device, m_externalMemory, nullptr);
MG_Remote::Transport::AdoptT0::ReleaseImported(m_externalAhb);
m_mappedData = nullptr;
m_buffer = VK_NULL_HANDLE;
m_externalMemory = VK_NULL_HANDLE;
m_externalAhb = nullptr;
m_device = VK_NULL_HANDLE;
m_externalCoherent = true;
m_size = 0;
return;
}
#endif
Unmap();
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
@@ -117,6 +170,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Unmap() {
#if MOBILEGL_BUILD_DISAGGREGATED
// An imported store stays mapped for its life (Destroy unmaps it).
if (m_externalMemory != VK_NULL_HANDLE) return;
#endif
if (!IsValid() || m_mappedData == nullptr) {
m_mappedData = nullptr;
return;
@@ -143,6 +200,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = m_externalMemory;
range.offset = 0;
range.size = VK_WHOLE_SIZE;
return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
@@ -167,6 +234,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resolvedSize == 0) {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = m_externalMemory;
range.offset = 0;
range.size = VK_WHOLE_SIZE;
return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
@@ -64,7 +64,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
Bool IsValid() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE;
#endif
return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer
// imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a
// dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves
// like any mapped store (GetSlice, Upload, Invalidate), and Destroy frees the buffer and
// the memory and drops `ahb` - which is why the store goes through the wire arm's
// serial-gated deferred release like every other: the AHardwareBuffer is let go only once
// no submission can still name it. `coherent` = the memory type is HOST_COHERENT.
void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb);
Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; }
#endif
private:
VmaAllocator m_allocator = nullptr;
@@ -72,5 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocation m_allocation = nullptr;
void* m_mappedData = nullptr;
VkDeviceSize m_size = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
VkDevice m_device = VK_NULL_HANDLE;
VkDeviceMemory m_externalMemory = VK_NULL_HANDLE;
void* m_externalAhb = nullptr;
Bool m_externalCoherent = true;
void MoveExternalFrom(VkBufferObject& other);
#endif
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1333,6 +1333,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkTextureManager::TextureResource* VkTextureManager::FindTextureResource(
MG_State::GLState::ITextureObject& textureOrView) {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() ? &it->second : nullptr;
}
Bool VkTextureManager::HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE;
}
Bool VkTextureManager::HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const {
auto& storage = StorageTextureOf(textureOrView);
auto it = m_textureResources.find(MakeTextureIdentity(&storage));
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
return false;
}
return it->second.syncedShapeVersion == storage.GetShapeVersion() &&
it->second.syncedContentVersion != storage.GetContentVersion();
}
Bool VkTextureManager::WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() && it->second.lastRecordingWriteGeneration == m_recordingGeneration;
}
void VkTextureManager::StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(*texture)));
if (it != m_textureResources.end()) {
StampResourceRecordingWrite(it->second);
}
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture));
@@ -1361,7 +1397,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
StampResourceRecordingUse(resource);
StampResourceRecordingWrite(resource);
if (resource.layout != newLayout && resource.mipLevels > 1) {
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -1475,8 +1511,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->aspect, 0, resource->mipLevels);
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
// Pre-pass stream bookkeeping: a command referencing the image was recorded, and an
// image unit may store to it.
StampResourceRecordingWrite(*resource);
return ok;
}
@@ -4077,18 +4114,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize offset = 0;
// Sub-region upload (a small sprite in a big atlas): only the dirty box
// is staged and copied. texelSize keeps the LEVEL extent - the staging
// row copy needs it for the shadow's stride. Plain color formats only;
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
// levels and stay full-size.
// row copy needs it for the stride. Color formats only (an RGB-expanded
// level is cut from its expanded bytes); the depth(+stencil) conversion
// passes rewrite whole levels and stay full-size.
Bool subRegion = false;
IntVec3 regionLo = {0, 0, 0};
IntVec3 regionSize = {0, 0, 0};
SizeT texelBytes = 0;
// Scatter refinement of the single dirty box: when the storage's rect
// list reports the writes' true footprint (~100 sprites whose union box
// spans the whole atlas), each rect is staged tightly and copied with
// its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. Empty means
// "stage the one box above". Only set while subRegion.
// The writes' exact footprint when it is more than one box (~100 sprites
// whose union box spans the whole atlas): each rect is staged tightly and
// copied with its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage.
// Empty means "stage the one box above". Only set while subRegion.
Vector<MG_State::GLState::MipmapDirtyRegion> rects;
};
@@ -4100,6 +4136,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
targets.push_back(uploadTarget);
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(mipmapTexture.GetFormat());
Vector<MG_State::GLState::MipmapDirtyRegion> dirtyFootprint;
VkDeviceSize stagingSize = 0;
for (const TextureUploadTarget target : targets) {
@@ -4142,34 +4179,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
if (!formatInfo.expandRgbToRgba &&
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
// The RGB -> RGBA expansion rewrites the level texel for texel into a tight buffer
// of its own, so it runs first and the dirty region below is taken from whichever
// bytes are staged. Uploading an expanded level WHOLE put the shadow's stale texels
// over whatever the GPU had drawn into the rest of it.
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
const SizeT levelBytes = uploadItem.uploadByteSize;
if (GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0) {
// Exactly the texels the client wrote, never their union box or the storage's
// rect list (which merges touching writes into their bounding box, folds when
// full and is withheld past 3/4 of the box): the image may hold what the GPU
// drew or cleared anywhere else, and the shadow is stale there. Each box is one
// more VkBufferImageCopy in the same vkCmdCopyBufferToImage, which costs next
// to nothing, so there is no cheaper shape worth being wrong for. A union box
// that spans the level with gaps in it is not a whole-level write either.
mipmapTexture.GetStorageDirtyFootprint(target, level, dirtyFootprint);
if (!region.Empty() && (dirtyFootprint.size() >= 2 || !region.CoversWholeLevel(texelSize)) &&
texelCount > 0 && levelBytes % texelCount == 0) {
uploadItem.subRegion = true;
uploadItem.regionLo = region.lo;
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
region.hi.z() - region.lo.z()};
uploadItem.texelBytes = byteSize / texelCount;
uploadItem.texelBytes = levelBytes / texelCount;
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
static_cast<SizeT>(uploadItem.regionSize.y()) *
static_cast<SizeT>(uploadItem.regionSize.z()) *
uploadItem.texelBytes;
// Scatter refinement: the storage only hands out its rect list
// when the rects' summed area is materially smaller than the
// union box (0 otherwise), so taking it always stages fewer
// bytes than the box - the very amplification this path exists
// to avoid paying twice.
MG_State::GLState::MipmapDirtyRegion
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
const SizeT dirtyRectCount = mipmapTexture.GetStorageDirtyRects(
target, level, dirtyRects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
if (dirtyRectCount >= 2) {
uploadItem.rects.assign(dirtyRects, dirtyRects + dirtyRectCount);
// Several boxes: each is staged tightly and copied on its own. One box is
// the union box above.
if (dirtyFootprint.size() >= 2) {
uploadItem.rects.assign(dirtyFootprint.begin(), dirtyFootprint.end());
SizeT rectTexels = 0;
for (const auto& rect : uploadItem.rects) {
rectTexels += rect.TexelCount();
@@ -4195,15 +4246,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
uploadItems.push_back(Move(uploadItem));
if (!uploadItems.back().expandedData.empty()) {
uploadItems.back().source = uploadItems.back().expandedData.data();
@@ -4438,6 +4480,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
Vector<VkBufferImageCopy> rectCopies;
for (const auto& item : uploadItems) {
if (!item.rects.empty()) {
// Multi-rect item: one VkBufferImageCopy per rect, all submitted in a
@@ -4445,7 +4488,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// construction, so no two copies write the same texels. Multi-rect
// implies subRegion, which implies a plain color aspect - the combined
// depth-stencil split below can never see one of these.
VkBufferImageCopy rectCopies[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
rectCopies.resize(item.rects.size());
Uint32 rectCopyCount = 0;
VkDeviceSize runningOffset = item.offset;
for (const auto& rect : item.rects) {
@@ -4476,7 +4519,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
runningOffset += static_cast<VkDeviceSize>(rect.TexelCount() * item.texelBytes);
}
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies);
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies.data());
continue;
}
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
@@ -332,6 +332,11 @@ public:
// into the frame's PRE command buffer - which executes strictly before the
// frame's commands - instead of splitting the active render pass.
Uint64 lastRecordingGeneration = 0;
// The same, for the last recorded command that WRITES the image (a render pass it is an
// attachment of, a clear, a blit/copy into it, a mip generation, a storage-image binding).
// An upload into an image written this recording has to be ordered after that write
// (VulkanRenderer::OrderPendingUploadAfterRecording).
Uint64 lastRecordingWriteGeneration = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
@@ -381,6 +386,7 @@ public:
std::swap(this->storageUsageResolved, that.storageUsageResolved);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->lastRecordingWriteGeneration, that.lastRecordingWriteGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
@@ -623,6 +629,26 @@ public:
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// A write is a use too. See TextureResource::lastRecordingWriteGeneration.
void StampResourceRecordingWrite(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
resource.lastRecordingWriteGeneration = m_recordingGeneration;
}
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
//
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
// before that write, which then overwrites the newer texels. (An image the recording only
// read keeps the old trade: its earlier reads see the newer texels.)
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
// The next sync would record client texels into the image that is live now: same shape (a
// shape change mints a new image instead) and content moved since the last sync.
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
// The live resource, or null.
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
@@ -2003,6 +2003,9 @@ void main() {
outBinding.label, texture->GetExternalIndex());
return false;
}
if (!isReadFramebuffer) {
textureManager.StampResourceRecordingWrite(*resource);
}
outBinding.image = resource->image;
outBinding.trackedLayout = &resource->layout;
@@ -2119,6 +2122,9 @@ void main() {
outBinding.label, texture->GetExternalIndex(), static_cast<Uint32>(requiredAspectMask));
return false;
}
if (!isReadFramebuffer) {
textureManager.StampResourceRecordingWrite(*resource);
}
outBinding.image = resource->image;
outBinding.trackedLayout = &resource->layout;
@@ -2159,6 +2165,7 @@ void main() {
texture.GetExternalIndex(), mipLevel, resource->mipLevels);
return false;
}
textureManager.StampResourceRecordingWrite(*resource);
outBinding.image = resource->image;
outBinding.trackedLayout = &resource->layout;
@@ -3175,7 +3182,10 @@ void main() {
}
VulkanRenderer::VulkanRenderer(NativeWindowType window, const VulkanRendererConfig& cfg)
: m_window(window), m_config(cfg) {
: m_window(window), m_config(cfg), m_presentsToAppWindow(window != NativeWindowType{}) {
if (!m_presentsToAppWindow) {
m_config.SwapInterval.reset();
}
// Initialize();
}
@@ -3572,6 +3582,9 @@ void main() {
m_device = VK_NULL_HANDLE;
}
s_vkCmdDrawIndexedIndirectCount = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
s_vkCmdWireDrawIndirectCount = nullptr;
#endif
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
@@ -7073,6 +7086,10 @@ void main() {
Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags<DrawSetupAspect> aspects,
const DrawCmdParam& drawParams,
const IndexBufferView* pIndexBufferView) {
// First, before anything of this draw is recorded or resolved - on the wire arm too: a
// split server records every draw its client sends into the same frame command buffer, so
// the same loading frame grows it the same way there.
SplitOversizedRecording();
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
RewindWireDescriptorSetsIfDue();
@@ -7381,6 +7398,15 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
activeRenderPass = nullptr;
}
// The syncs below upload each sampled texture's pending texels into a batch that runs
// ahead of this recording; one whose image the recording already cleared or drew into
// would be overwritten by that earlier work. Submit it first, here, before anything below
// records into the frame or decides from the active pass.
for (auto* sampledTexture : sampledTextures) {
if (sampledTexture != nullptr && OrderPendingUploadAfterRecording(frame, *sampledTexture)) {
activeRenderPass = nullptr;
}
}
Bool needSampledTextureTransitions = false;
auto& sampledResources = m_sampledResourcesScratch;
sampledResources.assign(sampledTextures.size(), nullptr);
@@ -7699,6 +7725,8 @@ void main() {
}
void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) {
// Before the wire branch, for SetupDraw's reason.
SplitOversizedRecording();
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
RewindWireDescriptorSetsIfDue();
@@ -7760,6 +7788,16 @@ void main() {
}
void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) {
SplitOversizedRecording();
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-SV: the wire arm's own indirect dispatch, DispatchCompute's shape one call over.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
RewindWireDescriptorSetsIfDue();
m_textureManager->CollectGarbage();
DispatchWireComputeIndirect(indirect);
return;
}
#endif
m_textureManager->CollectGarbage();
auto& frame = m_frameContext.GetCurrent();
// See DispatchCompute: the dispatch accessor, not the draw one.
@@ -8203,6 +8241,7 @@ void main() {
m_renderPassManager->QueueRenderbufferClear(
{.mask = GL_COLOR_BUFFER_BIT, .color = payload.color}, attachment);
} else if (attachment.IsTexture()) {
SettleTexelsBeforeQueuedClear(*fbo, attachment);
m_clearManager->QueueClear({.mask = GL_COLOR_BUFFER_BIT, .color = payload.color},
attachment);
}
@@ -8214,6 +8253,19 @@ void main() {
return;
}
if ((deferredMask & GL_COLOR_BUFFER_BIT) != 0) {
for (const auto drawBuffer : fbo->GetDrawBuffers()) {
if (drawBuffer != FramebufferAttachmentType::None) {
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(drawBuffer));
}
}
}
if ((deferredMask & GL_DEPTH_BUFFER_BIT) != 0) {
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(FramebufferAttachmentType::Depth));
}
if ((deferredMask & GL_STENCIL_BUFFER_BIT) != 0) {
SettleTexelsBeforeQueuedClear(*fbo, fbo->GetAttachment(FramebufferAttachmentType::Stencil));
}
m_clearManager->QueueClear(deferredMask, payload, *fbo);
m_renderPassManager->QueueRenderbufferClear(deferredMask, payload, *fbo);
}
@@ -8291,6 +8343,7 @@ void main() {
if (!attachment.IsTexture()) {
return;
}
SettleTexelsBeforeQueuedClear(framebuffer, attachment);
m_clearManager->QueueClear(payload, attachment);
};
@@ -8657,7 +8710,8 @@ void main() {
}
Bool VulkanRenderer::MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture) {
MG_State::GLState::ITextureObject& texture,
Bool uploadPendingTexels) {
Vector<PendingClearEntry> pendingClears;
if (!m_clearManager->GetPendingClears(&texture, pendingClears)) {
return true;
@@ -8669,8 +8723,9 @@ void main() {
commandBuffer != m_frameContext.GetCurrent().commandBuffer,
"MaterializePendingClearForTexture requires no active render pass on the target buffer");
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(texture);
if (resource == nullptr) {
auto* resource = uploadPendingTexels ? m_textureManager->SyncTextureAndGetDescriptor(texture)
: m_textureManager->FindTextureResource(texture);
if (resource == nullptr || resource->image == VK_NULL_HANDLE) {
// Declined sync (an incomplete texture, say). Nothing to clear into, and every line
// below dereferences this - the assert that used to stand here is compiled out of
// every build past DEBUG.
@@ -8822,6 +8877,9 @@ void main() {
"MaterializePendingClearForTexture: failed to transition textureId=%d to sampled layout",
texture.GetExternalIndex());
resource->layout = sampledLayout;
// The clear is now a write in this recording, which is what orders a later upload into
// the image after it (OrderPendingUploadAfterRecording).
m_textureManager->StampResourceRecordingWrite(*resource);
m_clearManager->PopPendingClear(&texture);
MGLOG_D("MaterializePendingClearForTexture: textureId=%d pending clear materialized",
@@ -8829,6 +8887,62 @@ void main() {
return true;
}
Bool VulkanRenderer::OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture) {
if (!m_textureManager->HasPendingTexelUpload(texture)) {
return false;
}
// An open pass drawing into the image stamps that write only when it ends.
if (const auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass();
activeRenderPass != nullptr && ActiveRenderPassUsesTexture(*activeRenderPass, texture)) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
const Bool clearFirst = m_clearManager->HasPendingClear(&texture);
if (!clearFirst && !m_textureManager->WasTextureWrittenThisRecording(texture)) {
// Nothing recorded writes the image: the upload may run ahead of the recording.
return false;
}
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
if (VkRenderPassManager::GetActiveRenderPass() != nullptr) {
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
}
if (clearFirst && !MaterializePendingClearForTexture(frame.commandBuffer, texture,
/*uploadPendingTexels=*/false)) {
MGLOG_E_ONCE("OrderPendingUploadAfterRecording: textureId=%d kept its queued clear; it may land over "
"texels written after it",
texture.GetExternalIndex());
}
if (!FlushPendingCommands()) {
MGLOG_E_ONCE("OrderPendingUploadAfterRecording: submit failed; textureId=%d's upload may run ahead of "
"earlier work on it",
texture.GetExternalIndex());
return false;
}
m_frameContext.BeginCommandRecording();
// New command buffer: the same reset SetupDraw applies when it begins one.
m_lastSampledSetValid = false;
return true;
}
void VulkanRenderer::SettleTexelsBeforeQueuedClear(
const MG_State::GLState::FramebufferObject& framebuffer,
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
// The default framebuffer's attachments are placeholders: its clears land on the
// swapchain image (MaterializePendingClearForDefaultFramebuffer) and it has no texels.
if (framebuffer.IsDefaultFramebuffer() || !attachment.IsTexture() || attachment.IsRenderbuffer()) {
return;
}
const auto texture = attachment.GetTexture();
if (!texture || (m_textureManager->HasLiveImage(*texture) &&
!m_textureManager->HasPendingTexelUpload(*texture))) {
return;
}
OrderPendingUploadAfterRecording(m_frameContext.GetCurrent(), *texture);
m_textureManager->SyncTextureAndGetDescriptor(*texture);
}
Bool VulkanRenderer::MaterializeMultisamplePendingClear(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture,
VkTextureManager::TextureResource& resource,
@@ -10782,6 +10896,7 @@ void main() {
const Bool dstClearReady = materializeClear(dstEndpoint);
MOBILEGL_ASSERT(dstClearReady, "%s: failed to materialize pending clear for destination objectId=%u",
__func__, CopyImageEndpointName(dstEndpoint));
m_textureManager->StampTextureRecordingWrite(dstEndpoint.Texture.get());
const VkImageLayout srcOriginalLayout = *srcImage.trackedLayout;
const VkImageLayout dstOriginalLayout = *dstImage.trackedLayout;
@@ -11066,6 +11181,13 @@ void main() {
}
auto& frame = m_frameContext.GetCurrent();
if (!readFbo->IsDefaultFramebuffer()) {
// The read syncs the source texture, whose pending texels must land after work already
// recorded against it (see OrderPendingUploadAfterRecording).
if (const auto readTexture = readFbo->GetAttachment(readFbo->GetReadBuffer()).GetTexture()) {
OrderPendingUploadAfterRecording(frame, *readTexture);
}
}
if (!frame.isCommandRecording) {
m_frameContext.BeginCommandRecording();
}
@@ -11791,6 +11913,9 @@ void main() {
return;
}
// The sync below uploads pending texels, which must land after work already recorded
// against this image (see OrderPendingUploadAfterRecording).
OrderPendingUploadAfterRecording(m_frameContext.GetCurrent(), *textureObject);
auto* resource = m_textureManager->SyncTextureAndGetDescriptor(*textureObject);
// Two shapes end up in the same place, and for the same reason: the GL level being read has
// no GPU storage, so UploadDirtyMipLevels never wrote it and the CPU shadow is the ONLY copy
@@ -12136,6 +12261,7 @@ void main() {
resource = m_textureManager->SyncTextureAndGetDescriptor(*texture);
MOBILEGL_ASSERT(resource != nullptr && resource->image != VK_NULL_HANDLE,
"GenerateMipmap failed to resync the backend texture after allocating mip storage.");
m_textureManager->StampResourceRecordingWrite(*resource);
if (resource->layout == VK_IMAGE_LAYOUT_UNDEFINED) {
const VkImageLayout finalLayout = ResolveGenerateMipmapFinalLayout(resource->aspect);
Bool transitioned = VkTextureManager::TransitionImageLayout(
@@ -14091,6 +14217,54 @@ void main() {
return FlushPendingCommands();
}
void VulkanRenderer::SplitOversizedRecording() {
const Uint32 maxDraws = MG_Config::Features.MagmaMaxDrawsPerCommandBuffer;
if (maxDraws == 0) {
return;
}
// Counted before the check, so a buffer carries at most maxDraws calls before the one
// that splits it. OnFrameCommandRecordingBegan resets the count on every new recording.
if (++m_drawsInRecording <= maxDraws || !HasPendingRecordedWork()) {
return;
}
// Nothing of the current call is recorded yet, so the split falls between two GL calls,
// exactly where a glFlush would put it: the render pass ends with STORE, the next draw
// re-begins it with LOAD, and queries and transform feedback are bracketed per draw.
if (!FlushPendingCommands()) {
return;
}
// Fresh command buffer: the same reset every other mid-frame flush site applies.
m_lastSampledSetValid = false;
++m_oversizedRecordingSplits;
MGLOG_I_ONCE("DirectVulkan: a frame recorded more than %u draws into one command buffer; submitting it and "
"continuing on a fresh one (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded)",
maxDraws);
MGLOG_D("DirectVulkan: split an oversized recording (%llu so far, submit index %llu)",
static_cast<unsigned long long>(m_oversizedRecordingSplits),
static_cast<unsigned long long>(m_submitCounter));
// Splitting only bounds the driver's command memory if the submitted buffers are freed
// as fast as they are produced. A GPU that falls behind would park them all on the
// retired list, which is the same growth spread over more buffers - so hold the queue
// to frames-in-flight + 1 outstanding submissions, the steady state of a pipelined
// frame plus this split. Wait on the fence directly rather than through
// WaitForSubmitIndex: its opportunistic TryDrainFrameTransients may rewind the transient
// arena, and this runs at the start of a draw whose caller may already hold slices of it.
const SizeT maxOutstanding = static_cast<SizeT>(m_frameContext.GetFrameCount()) + 1;
while (m_inFlightSubmits.size() > maxOutstanding) {
const SubmitRecord oldest = m_inFlightSubmits.front();
if (oldest.fence == VK_NULL_HANDLE) {
break;
}
const VkResult waitResult = vkWaitForFences(m_device, 1, &oldest.fence, VK_TRUE, UINT64_MAX);
if (waitResult != VK_SUCCESS) {
MGLOG_E_ONCE("SplitOversizedRecording: vkWaitForFences returned %d", waitResult);
break;
}
OnSubmitsCompletedUpTo(oldest.submitIndex);
}
}
Bool VulkanRenderer::WaitForSubmitIndex(Uint64 submitIndex, Uint64 timeoutNs, Bool flushIfPending) {
if (IsSubmitIndexComplete(submitIndex)) {
return true;
@@ -14136,6 +14310,7 @@ void main() {
}
void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) {
m_drawsInRecording = 0;
// Dynamic state does not survive a command-buffer boundary.
ResetDynamicStateShadow();
InvalidateSetupDrawSnapshots();
@@ -14406,8 +14581,22 @@ void main() {
if (!m_swapchainResizeRequested && SwapchainIsOutOfDate()) {
m_swapchainResizeRequested = true;
}
// A new eglSwapInterval lands here too, between frames: this one is on screen and the
// next has recorded nothing. Only a different present mode is worth the rebuild (it
// drops every pipeline); otherwise the live swapchain already paces as asked.
if (m_config.SwapInterval != m_swapchainSwapInterval) {
if (SwapchainObject::ChooseSwapchainPresentMode(m_swapchainObject.GetSupportedPresentModes(),
m_config.SwapInterval) !=
m_swapchainObject.GetPresentMode()) {
m_swapchainResizeRequested = true;
} else {
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
m_swapchainSwapInterval = m_config.SwapInterval;
}
}
if (m_swapchainResizeRequested) {
MGLOG_D("Present, processing requested swapchain resize");
MGLOG_D("Present, processing requested swapchain rebuild (resize or swap interval)");
if (!RecreateSwapchain()) {
m_presentSuspended = true;
m_swapchainResizeRequested = false;
@@ -15156,6 +15345,30 @@ void main() {
// P7 gate 5 (g5-msprobe): which multisample depth/stencil resolve arm goes first is measured
// once the device exists (ArmWireDepthResolveOrder, below), not keyed on the vendor.
m_wirePreferShaderDepthResolve = false;
// P11 B2 (T0): a client's AHardwareBuffer imported as a wire buffer store. Wire arms, on
// Android, whenever the device advertises the extension - not per session: the device is
// created once per server and a later session asking T0 must find it able. Enabling it
// changes no T2 behaviour; only an import uses it. The 1.0-era dependencies are core at the
// 1.1 this device is created at, and are enabled too when advertised (the spike's set).
m_wireAhbImport = false;
m_wireGetAhbProperties = nullptr;
#if defined(__ANDROID__)
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
m_physicalDevice.properties.apiVersion >= VK_API_VERSION_1_1 &&
IsExtensionSupported(availableExtensions, VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME) &&
IsExtensionSupported(availableExtensions, VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME)) {
m_wireAhbImport = true;
for (const char* name : {VK_ANDROID_EXTERNAL_MEMORY_ANDROID_HARDWARE_BUFFER_EXTENSION_NAME,
VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME,
VK_KHR_SAMPLER_YCBCR_CONVERSION_EXTENSION_NAME,
VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME,
VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME,
VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_1_EXTENSION_NAME}) {
if (IsExtensionSupported(availableExtensions, name))
EnableOptionalDeviceExtension(availableExtensions, enabledDeviceExtensions, name);
}
}
#endif
#endif
if ((descriptorIndexingCore || descriptorIndexingExtension) && getPhysicalDeviceFeatures2 != nullptr &&
getPhysicalDeviceProperties2 != nullptr) {
@@ -15715,6 +15928,11 @@ void main() {
m_wireCreateRenderPass2 = reinterpret_cast<PFN_vkCreateRenderPass2>(
vkGetDeviceProcAddr(m_device, wireDepthResolveCore ? "vkCreateRenderPass2" : "vkCreateRenderPass2KHR"));
}
if (m_wireAhbImport) {
m_wireGetAhbProperties =
reinterpret_cast<void*>(vkGetDeviceProcAddr(m_device, "vkGetAndroidHardwareBufferPropertiesANDROID"));
if (m_wireGetAhbProperties == nullptr) m_wireAhbImport = false;
}
#endif
s_vkCmdDrawIndexedIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
@@ -15727,6 +15945,16 @@ void main() {
MGLOG_W("VK_KHR_draw_indirect_count enabled but vkCmdDrawIndexedIndirectCount entry point is missing, will continue as if VK_KHR_draw_indirect_count is not supported!");
m_drawIndirectCountExtensionEnabled = false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-D: the wire arm's glMultiDrawArraysIndirectCount. Null = that verb reads its count on
// the CPU (WireDraw.inc's DrawWireIndirectNative answers false for it).
s_vkCmdWireDrawIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCountKHR"));
if (s_vkCmdWireDrawIndirectCount == nullptr) {
s_vkCmdWireDrawIndirectCount = reinterpret_cast<PFNDrawIndexedIndirectCountFunc>(
vkGetDeviceProcAddr(m_device, "vkCmdDrawIndirectCount"));
}
#endif
s_vkCmdDrawMultiEXT = nullptr;
s_vkCmdDrawMultiIndexedEXT = nullptr;
@@ -16085,7 +16313,12 @@ void main() {
m_swapchainObject.Create(m_device, m_physicalDevice.handle, m_surface,
static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily),
static_cast<Uint32>(m_physicalDevice.queueFamilies.presentFamily),
m_config.MaxFramesInFlight, desiredExtent);
m_config.MaxFramesInFlight, desiredExtent, m_config.SwapInterval);
if (m_config.SwapInterval && m_config.SwapInterval != m_swapchainSwapInterval) {
MGLOG_D("DirectVulkan: swap interval %d -> %s", *m_config.SwapInterval,
SwapchainObject::GetPresentModeName(m_swapchainObject.GetPresentMode()));
}
m_swapchainSwapInterval = m_config.SwapInterval;
// The FragCoordYFlip variants bake this height in; it is the only input to a shader
// module that lives outside the GL program, so the factory has to learn it here (and on
// every recreation, which is the only way it can change).
@@ -16541,10 +16774,29 @@ void main() {
m_swapchainResizeRequested = true;
}
void VulkanRenderer::SetSwapInterval(Int interval) {
if (m_presentsToAppWindow) {
m_config.SwapInterval = interval;
}
}
VkInstance VulkanRenderer::GetInstance() const {
return m_instance;
}
#if MOBILEGL_BUILD_DISAGGREGATED
Bool VulkanRenderer::GetWireAhbImport(WireAhbImport& out) const {
out = WireAhbImport{};
if (!m_wireAhbImport || m_device == VK_NULL_HANDLE || m_wireGetAhbProperties == nullptr) return false;
out.device = m_device;
out.queue = m_graphicsQueue;
out.queueFamily = static_cast<Uint32>(m_physicalDevice.queueFamilies.graphicsFamily);
vkGetPhysicalDeviceMemoryProperties(m_physicalDevice.handle, &out.memory);
out.getAhbProperties = m_wireGetAhbProperties;
return true;
}
#endif
Bool VulkanRenderer::IsDrawIndirectCountExtensionEnabled() const {
return m_drawIndirectCountExtensionEnabled;
}
@@ -160,6 +160,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// GetSyncPointSubmitIndex(), which answers m_submitCounter when nothing is recorded -
// true for a fence taken at that instant, wrong for work about to be recorded.
Uint64 GetWireNextSubmitIndex() const { return m_submitCounter + 1; }
// P8-D: the wire arm's indirect draw families, issued as vkCmdDraw[Indexed]Indirect[Count]
// from the wire stores MGPipeApplier().VerbIndirectBuffer / VerbIndirectParameterBuffer
// name. `offset` / `countOffset` are byte offsets into those stores; `stride` 0 means
// tightly packed. FALSE = this device cannot issue the COUNT form natively (the monolith
// arm's own condition, see the definition in WireDraw.inc) and the caller must read the
// words on the CPU; TRUE = issued, or declined by name.
Bool DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount, GLsizei stride,
Bool indexed, Bool counted, Uint64 countOffset);
// P11 B2 (T0): what importing a client's AHardwareBuffer as a wire buffer store needs from
// this device. False when the device did not take VK_ANDROID_external_memory_android_
// hardware_buffer at creation (not Android, not advertised, or a monolith device).
struct WireAhbImport {
VkDevice device = VK_NULL_HANDLE;
VkQueue queue = VK_NULL_HANDLE;
Uint32 queueFamily = 0;
VkPhysicalDeviceMemoryProperties memory{};
void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID
};
Bool GetWireAhbImport(WireAhbImport& out) const;
#endif
// FrameContext::IRecordingObserver: prepares the frame's timer-query
@@ -327,6 +346,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// signal as soon as the GPU actually finishes, which MC 1.21.5's
// fence-paced ring buffers rely on to recycle their space.
Uint64 GetSyncPointSubmitIndex() const;
// For tests: the per-frame descriptor pools (see DescriptorPoolCensus.h).
DescriptorPoolCensus GetDescriptorPoolCensus() const {
return m_uniformManager ? m_uniformManager->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
}
// Non-blocking: polls outstanding submission fences and reports
// whether every submission up to `submitIndex` has completed.
Bool IsSubmitIndexComplete(Uint64 submitIndex);
@@ -390,6 +413,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool ResolveOcclusionQueryResult(const Vector<Uint32>& slots, Uint64& outSamples);
void RequestSwapchainResize(Uint32 width, Uint32 height);
// eglSwapInterval. Records the request; Present rebuilds the swapchain for it once the
// current frame is on screen, and only when it maps to a different present mode. A
// renderer created without an app window ignores it: its surface (headless, or an
// AImageReader nobody consumes) is never displayed, and FIFO there would block once
// the reader's queue fills.
void SetSwapInterval(Int interval);
// Re-query the surface and report whether the live swapchain no longer matches it
// (size or orientation). This - not a VK_SUBOPTIMAL_KHR result - is what decides a
// rebuild, so a surface the driver merely considers suboptimal cannot thrash.
@@ -564,6 +593,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// target unwritten while the shader control resolved - the shader arm then resolves
// first and the render-pass arm is the fallback (P7 gate 5, g5-msprobe).
Bool m_wirePreferShaderDepthResolve = false;
// P11 B2 (T0): the device took the AHardwareBuffer import extensions at creation, and the
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
Bool m_wireAhbImport = false;
void* m_wireGetAhbProperties = nullptr;
// Runs the probe (memoized per device identity) and sets the member above. Called at the end of
// device creation, after ArmPrimGenReroute: it records on m_graphicsQueue.
void ArmWireDepthResolveOrder();
@@ -655,7 +688,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void CollectWireObjects(Uint64 completedSubmit, Bool all = false);
Vector<WireRetiredObjects> m_wireRetiredObjects;
Uint32 m_wirePreparationDepth = 0;
// P8-D: set only while DrawWireIndirectNative is inside SetupDraw, so SetupWireDraw can
// tell the wire arm's own indirect draw from a monolith indirect entry point reached under
// a transport (still the `buffer-legacy-arm` role violation).
Bool m_wireNativeIndirectDraw = false;
void DispatchWireCompute(GLuint x, GLuint y, GLuint z);
// P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the
// dispatch-indirect store, as the monolith arm issues it (WireDraw.inc).
void DispatchWireComputeIndirect(GLintptr offset);
void RewindWireDescriptorSetsIfDue();
UniquePtr<RenderPassEntry> m_wireDrawPass;
Vector<VkImageView> m_wireDrawViews;
@@ -730,6 +770,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only), so the presenting path's frames-in-flight pipelining is
// untouched. Returns true when the drain ran.
Bool TryDrainFrameTransients();
// Bounded recording: called at the top of every GL draw and dispatch, before anything
// of that call is recorded. Once the open frame command buffer holds
// MagmaMaxDrawsPerCommandBuffer draws/dispatches, submits it (FlushPendingCommands) so
// the call continues on a fresh one, then waits until at most frames-in-flight + 1
// submissions are outstanding. A driver backs a command buffer with GPU memory it only
// returns when the buffer is freed (Adreno: one 16 KiB mapping per chunk), so a frame
// that is never split grows it without bound - rd12's loading frame records ~1M draws
// into one buffer and runs the process out of vm.max_map_count.
void SplitOversizedRecording();
Vector<SubmitRecord> m_inFlightSubmits;
Vector<VkFence> m_freeSubmitFences;
@@ -741,6 +790,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// aging clocks, while present-less loops still cross a boundary every few
// iterations. Reset in Present.
Uint32 m_drainsSinceLastPresent = 0;
// GL draws/dispatches recorded into the open frame command buffer; reset whenever a
// recording begins (OnFrameCommandRecordingBegan). See SplitOversizedRecording.
Uint32 m_drawsInRecording = 0;
Uint64 m_oversizedRecordingSplits = 0;
NativeWindowType m_window = 0;
void* m_platformDisplay = nullptr;
@@ -760,6 +813,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Shutdown() deletes it.
void* m_fallbackImageReader = nullptr;
VulkanRendererConfig m_config;
// Constructed for the app's window rather than a pbuffer (m_window is later filled in
// for the pbuffer fallbacks too, so it cannot tell them apart).
Bool m_presentsToAppWindow = false;
// The m_config.SwapInterval the live swapchain was built for; Present compares the two.
Optional<Int> m_swapchainSwapInterval;
Bool m_swapchainResizeRequested = false;
// Presentation is suspended while the window is zero-area (minimized): the
// swapchain is unusable/out of date, so Present drops frames instead of
@@ -878,6 +936,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
Uint32 stride);
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-D: vkCmdDrawIndirectCount (same signature, same extension), for the wire arm's
// glMultiDrawArraysIndirectCount. The monolith arm never loads it: it reads that count on
// the CPU (DirectVulkan.cpp's MultiDrawArraysIndirectCount).
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
#endif
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
// (and its multiDraw feature) is enabled; null otherwise.
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
@@ -1861,8 +1925,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
Uint32 depthSlice, const VkClearValue& clearValue,
VkImageLayout finalLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);
// Records the texture's queued clears. By default the texture is synced first, which
// uploads any pending texels BEFORE the clear - right only for texels written before it.
// `uploadPendingTexels = false` leaves them pending (OrderPendingUploadAfterRecording).
Bool MaterializePendingClearForTexture(VkCommandBuffer commandBuffer,
MG_State::GLState::ITextureObject& texture);
MG_State::GLState::ITextureObject& texture,
Bool uploadPendingTexels = true);
// Texture uploads run in a batch submitted ahead of the frame's recording, so the upload
// the next sync of `texture` records would land BEFORE a write already recorded into the
// same image (a render, clear, blit or copy), and before a clear still queued for it (such
// a clear predates the texels: SettleTexelsBeforeQueuedClear). When either holds, this
// records the queued clear without the texels and submits the recording, so the upload
// follows both. An image the recording only READ keeps the old trade: no submit, and its
// earlier reads see the newer texels. It may end the render pass and replace
// frame.commandBuffer (recording again on return), so call it only where the caller holds
// no render-pass or command-buffer state across it. Returns whether it submitted.
Bool OrderPendingUploadAfterRecording(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture);
// Called for each texture a glClear/glClearBuffer* is about to queue a clear on: uploads
// whatever texels are still pending (and mints the image if there is none), so that any
// texel pending when the clear is materialized was written after it.
void SettleTexelsBeforeQueuedClear(const MG_State::GLState::FramebufferObject& framebuffer,
const MG_State::GLState::FramebufferAttachmentObject& attachment);
// The multisample arm of the above. Split out rather than branched inline because it
// shares none of the transfer path: a multisample image carries no TRANSFER_DST usage, so
// neither the TRANSFER_DST transition nor vkCmdClearColorImage is legal on one.
@@ -58,7 +58,7 @@ MGL_WIRE_DECLINE(CurrentAttributePayload)
MGL_WIRE_DECLINE(CurrentAttributeUpload)
MGL_WIRE_DECLINE(BeginRenderPass)
// ---- DispatchWireCompute ----------------------------------------------------------------
// ---- DispatchWireCompute (and DispatchWireComputeIndirect, P8-SV) -------------------------
MGL_WIRE_DECLINE(ComputeProgramLinkOrSpirv)
MGL_WIRE_DECLINE(ComputeTextureResourcesUnprepared)
MGL_WIRE_DECLINE(ComputePipelineUnavailable)
@@ -104,3 +104,25 @@ MGL_WIRE_DECLINE(ShapeCreateImageFailed)
// Appended by the B3 review round: the preserve-copy arm of a reallocation was the one exit in
// SyncWireTextureShape still leaving no trace at all.
MGL_WIRE_DECLINE(ShapePreserveCopyFailed)
// ---- VulkanRenderer::GenerateWireMipmap (P8-F) -----------------------------------------
// `mipmap-shader-format-or-shape` was a Fatal on this arm and a decline on the monolith arm; it
// is the monolith's decline now. Counted once per refused glGenerateMipmap, whose chain is left
// as it was.
MGL_WIRE_DECLINE(MipmapShaderFormatOrShape)
// ---- VulkanRenderer::DrawWireIndirectNative (P8-D), in statement order -----------------------
MGL_WIRE_DECLINE(IndirectBufferUnbound)
MGL_WIRE_DECLINE(IndirectCommandRange)
MGL_WIRE_DECLINE(IndirectCountRange)
MGL_WIRE_DECLINE(IndirectIndexStore)
// ---- P8-SV -------------------------------------------------------------------------------
// VulkanRenderer::GenerateWireMipmap: a combined depth/stencil (or stencil-only) chain, which no
// arm generates (CONTRACT-P7 §3.2). It logged but was never counted, so MGWIRE-DECLINES could
// not see it. Counted once per refused glGenerateMipmap, whose chain is left as it was.
MGL_WIRE_DECLINE(MipmapDepthStencilAspect)
// VulkanRenderer::DispatchWireComputeIndirect, in statement order: the dispatch-indirect store
// is missing, or the three group-count words lie outside it or are misaligned.
MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound)
MGL_WIRE_DECLINE(DispatchIndirectRange)
@@ -64,6 +64,14 @@
if (source.samples != VK_SAMPLE_COUNT_1_BIT || destination.samples != VK_SAMPLE_COUNT_1_BIT)
return false;
if (source.is3D || destination.is3D) return false;
// P8-SV: THE PASS MAKES 2D VIEWS (makeView below), which a 1D, 1D-array or 3D image does
// not admit. A caller that fills viewType from the image gets a refusal here, and the
// wire arm's GenerateWireMipmap asks the same question before it grows the chain.
for (const VkImageViewType viewType : {source.viewType, destination.viewType}) {
if (viewType == VK_IMAGE_VIEW_TYPE_1D || viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
viewType == VK_IMAGE_VIEW_TYPE_3D)
return false;
}
if (source.format != destination.format) return false;
// DEPTH ONLY. A combined depth/stencil image can be sampled for its depth and
// rendered as a depth attachment, but this pass writes gl_FragDepth and NOTHING
@@ -207,7 +207,10 @@
MGL_WIRE_DECLINE_AT(ProgramLinkOrSpirv, "program is not linked or carries no SPIR-V");
return false;
}
if (aspects & DrawSetupAspect::IndirectDrawBuffer)
// P8-D: the wire arm's own indirect draw (DrawWireIndirectNative) is the one caller that
// may pass this aspect; a monolith indirect entry point reached here is still the role
// violation.
if ((aspects & DrawSetupAspect::IndirectDrawBuffer) && !m_wireNativeIndirectDraw)
WireBufferLegacyFatal();
MakeXfbWritesVisible();
const auto* fbo = state.DrawFramebuffer();
@@ -294,7 +297,14 @@
const Uint32 location = input.bindingAttributeLocations[binding];
const auto& attr = vao.Attributes[location];
const auto& resource = state.VertexBuffers[location];
if (MG_Pipe::MGPipeHandleIsNull(resource.Res)) MagmaWireFatal("client-vertex-array@P8");
// P8-F: A RECORD INVARIANT, NOT AN UNMIGRATED VERB (it carried a P8 phase tag).
// The client turns every enabled client-memory array into an owned buffer before the
// draw record is built, or refuses the draw itself (MG_Impl/Pipe/OwnedDrawInputs.h),
// so an attribute the program reads with no buffer handle is a foreign peer's record.
if (MG_Pipe::MGPipeHandleIsNull(resource.Res))
MG_Pipe::MGPipeSessionFail(MG_Pipe::MGPipeFatalFamily::ProtocolCorruption,
"MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an "
"attribute the program reads carries no buffer handle", location);
BufferSlice slice{};
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice))
MagmaWireFatal("vertex-buffer-resource");
@@ -310,8 +320,10 @@
const SizeT stride = static_cast<SizeT>(attr.Stride);
SizeT count = stride ? 1 + (static_cast<SizeT>(slice.size - offset) - elementSize) / stride : 1;
// Direct non-indexed draws bound the conversion; indexed/multi-draw may
// name arbitrary vertices, so preserve the entire valid source stream.
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) && !resource.Divisor)
// name arbitrary vertices, so preserve the entire valid source stream. So may an
// indirect draw (P8-D): its vertex range is in GPU words nobody read.
if (stride && !(aspects & DrawSetupAspect::IndexBuffer) &&
!(aspects & DrawSetupAspect::IndirectDrawBuffer) && !resource.Divisor)
count = std::min<SizeT>(count, static_cast<SizeT>(drawParams.firstVertex) + drawParams.vertexCount);
if (!count) {
MGL_WIRE_DECLINE_AT(VertexStreamCountZero, "the converted vertex stream would hold no elements");
@@ -601,6 +613,136 @@
return true;
}
// ---- P8-D: THE WIRE ARM'S INDIRECT DRAWS READ THEIR WORDS ON THE GPU ----------------------
//
// The shape the monolith arm records (VulkanRenderer.cpp's MultiDraw{Elements,Arrays}Indirect
// and MultiDrawElementsIndirectCount), with the wire store's own VkBuffer in place of the
// frontend buffer's resident slice. Before this, every wire indirect draw read its commands
// through ReadWireBuffer and expanded them on the CPU, and a store a shader had written made
// that read a whole-GPU wait (WaitForWireBufferHostAccess) per draw.
//
// THE ONE FALSE: a COUNT form this device cannot issue - the monolith arm's own condition for
// vkCmdDrawIndexedIndirectCount (MultiDrawElementsIndirectCount), extended to the non-indexed
// twin the monolith never loads. Everything else is issued here or declined by name.
Bool VulkanRenderer::DrawWireIndirectNative(GLenum mode, GLenum type, Uint64 offset, GLsizei drawcount,
GLsizei stride, Bool indexed, Bool counted, Uint64 countOffset) {
if (drawcount <= 0) return true;
if (counted) {
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
if (!m_drawIndirectCountExtensionEnabled || countEntry == nullptr ||
(!m_multiDrawIndirectFeatureEnabled && drawcount != 1))
return false;
}
const auto& state = MG_Pipe::MGPipeApplier();
// GL's command sizes (GL 4.6 10.3.10), which are also Vulkan's.
const Uint64 commandSize = indexed ? 5 * sizeof(Uint32) : 4 * sizeof(Uint32);
if (stride == 0) stride = static_cast<GLsizei>(commandSize);
BufferSlice commands{};
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectBuffer, commands)) {
MGL_WIRE_DECLINE_AT(IndirectBufferUnbound, "an indirect draw names no live indirect buffer store");
return true;
}
const Uint64 span = static_cast<Uint64>(stride) * static_cast<Uint64>(drawcount - 1) + commandSize;
// GL refuses a stride that is not a multiple of four (INVALID_VALUE), and Vulkan requires
// it for drawCount > 1; a peer's record that breaks either is declined, not issued.
if (stride < static_cast<GLsizei>(commandSize) || (stride & 3) != 0 || (offset & 3u) != 0 ||
offset > commands.size || span > commands.size - offset) {
MGL_WIRE_DECLINE_AT(IndirectCommandRange, "the indirect commands lie outside their store or are misaligned");
return true;
}
BufferSlice parameters{};
if (counted) {
if (MG_Pipe::MGPipeHandleIsNull(state.VerbIndirectParameterBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbIndirectParameterBuffer, parameters) ||
(countOffset & 3u) != 0 || countOffset > parameters.size || parameters.size - countOffset < sizeof(Uint32)) {
MGL_WIRE_DECLINE_AT(IndirectCountRange, "the indirect draw count lies outside its parameter store");
return true;
}
}
// The index view is the WHOLE bound element store past its binding offset, as the monolith
// arm binds it: the commands name their own firstIndex, which only the GPU reads.
IndexBufferView indices{};
DrawCmdParam vertexRange{};
vertexRange.instanceCount = 1;
Flags<DrawSetupAspect> aspects = DrawSetupAspect::IndirectDrawBuffer;
if (indexed) {
const SizeT width = MG_Util::GetGLTypeSize(type);
BufferSlice elements{};
if (width == 0 || MG_Pipe::MGPipeHandleIsNull(state.IndexBuffer.Res) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, elements) ||
state.IndexBuffer.Offset > elements.size) {
MGL_WIRE_DECLINE_AT(IndirectIndexStore, "an indexed indirect draw names no usable element store");
return true;
}
indices.indexType = type;
indices.indexByteOffset = 0;
indices.indexByteSize = static_cast<SizeT>(elements.size - state.IndexBuffer.Offset);
vertexRange.vertexCount = static_cast<Uint32>(indices.indexByteSize / width);
aspects |= DrawSetupAspect::IndexBuffer;
}
// A shader write the store was marked with (MarkWireBufferGpuWritten) is made visible to
// the indirect read here, once per write, so a program that omits GL_COMMAND_BARRIER_BIT
// is not left racing a read the old CPU path used to take after a whole-GPU wait. The
// application's own glMemoryBarrier is BuildMemoryBarrierForGlBarriers, as on the monolith
// arm; transfer writes carry their own MEMORY_READ after-barrier (VkBufferManager.cpp).
const Bool commandsWritten = m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectBuffer);
const Bool countWritten = counted && m_bufferManager.TakeWireIndirectReadBarrier(state.VerbIndirectParameterBuffer);
if (commandsWritten || countWritten) {
RetireWireDrawPass();
auto& barrierFrame = m_frameContext.GetCurrent();
if (!barrierFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
VkRenderPassManager::EndRenderPass(barrierFrame.commandBuffer);
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
vkCmdPipelineBarrier(barrierFrame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
}
auto& frame = m_frameContext.GetCurrent();
m_wireNativeIndirectDraw = true;
const Bool ready = SetupDraw(frame, mode, aspects, vertexRange, indexed ? &indices : nullptr);
m_wireNativeIndirectDraw = false;
if (!ready) return true;
MOBILEGL_ASSERT(frame.isCommandRecording, "%s: frame recording was not started", __func__);
const VkCommandBuffer commandBuffer = frame.commandBuffer;
const VkDeviceSize first = commands.offset + offset;
const Uint32 count = static_cast<Uint32>(drawcount);
const Uint32 byteStride = static_cast<Uint32>(stride);
if (counted) {
const auto countEntry = indexed ? s_vkCmdDrawIndexedIndirectCount : s_vkCmdWireDrawIndirectCount;
countEntry(commandBuffer, commands.buffer, first, parameters.buffer, parameters.offset + countOffset,
count, byteStride);
} else if (count == 1 || (!m_multiDrawForceUnrollIndirect && m_multiDrawIndirectFeatureEnabled)) {
// maxDrawIndirectCount bounds one call; GL's drawcount does not.
const Uint32 limit = std::max<Uint32>(1u, m_physicalDevice.properties.limits.maxDrawIndirectCount);
for (Uint32 base = 0; base < count; base += limit) {
const Uint32 chunk = std::min(count - base, limit);
const VkDeviceSize at = first + static_cast<VkDeviceSize>(base) * byteStride;
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, chunk, byteStride);
}
} else {
// No multiDrawIndirect: one indirect draw per command, still read by the GPU
// (the monolith arm's own fallback).
for (Uint32 i = 0; i < count; ++i) {
const VkDeviceSize at = first + static_cast<VkDeviceSize>(i) * byteStride;
if (indexed) vkCmdDrawIndexedIndirect(commandBuffer, commands.buffer, at, 1, 0);
else vkCmdDrawIndirect(commandBuffer, commands.buffer, at, 1, 0);
}
}
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDraws, 1);
return true;
}
void VulkanRenderer::DispatchWireCompute(GLuint x, GLuint y, GLuint z) {
RetireWireDrawPass();
auto& frame = m_frameContext.GetCurrent();
@@ -628,4 +770,80 @@
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdDispatch(frame.commandBuffer, x, y, z);
}
// ---- P8-SV: THE WIRE ARM'S INDIRECT DISPATCH READS ITS GROUP COUNTS ON THE GPU ----------
//
// The monolith arm's shape (VulkanRenderer.cpp's DispatchComputeIndirect) with the wire
// store's own VkBuffer in place of the frontend buffer's resident slice. Before this the wire
// arm read the three words through ReadWireBuffer and dispatched them from the CPU, and a store
// a shader had written made that read a whole-GPU wait (WaitForWireBufferHostAccess) per call.
// There is no shape the device cannot issue, so there is no CPU path left: a record whose
// store or words are unusable is declined by name.
//
// The barrier is P8-D's draw rule (DrawWireIndirectNative): a store marked shader-written gets
// one MEMORY_WRITE -> INDIRECT_COMMAND_READ barrier before its next indirect read, whether or
// not the application issued GL_COMMAND_BARRIER_BIT; DRAW_INDIRECT is the stage that consumes
// VkDispatchIndirectCommand too. It is TAKEN before the bindings below, because binding this
// dispatch's own storage buffers marks them written, and a store that is both this dispatch's
// parameters and one of its outputs must keep that mark for the next reader.
void VulkanRenderer::DispatchWireComputeIndirect(GLintptr offset) {
RetireWireDrawPass();
const auto& state = MG_Pipe::MGPipeApplier();
constexpr Uint64 kCommandSize = 3 * sizeof(Uint32); // GL 4.6 10.3.10 / VkDispatchIndirectCommand
BufferSlice parameters{};
if (MG_Pipe::MGPipeHandleIsNull(state.VerbDispatchIndirectBuffer) ||
!m_bufferManager.AcquireWireSlice(BufferKind::Indirect, state.VerbDispatchIndirectBuffer, parameters)) {
MGL_WIRE_DECLINE_AT(DispatchIndirectBufferUnbound, "an indirect dispatch names no live dispatch-indirect store");
return;
}
// GL refuses an offset that is negative or not a multiple of four (INVALID_VALUE) and a
// command past the end of the buffer (INVALID_OPERATION); a peer's record that breaks
// either is declined, not issued.
if (offset < 0 || (static_cast<Uint64>(offset) & 3u) != 0 || static_cast<Uint64>(offset) > parameters.size ||
parameters.size - static_cast<Uint64>(offset) < kCommandSize) {
MGL_WIRE_DECLINE_AT(DispatchIndirectRange, "the dispatch group counts lie outside their store or are misaligned");
return;
}
auto& frame = m_frameContext.GetCurrent();
if (m_bufferManager.TakeWireIndirectReadBarrier(state.VerbDispatchIndirectBuffer)) {
if (!frame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (VkRenderPassManager::GetActiveRenderPass() != nullptr)
VkRenderPassManager::EndRenderPass(frame.commandBuffer);
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_INDIRECT_COMMAND_READ_BIT;
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr);
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectBarriers, 1);
}
const auto program = WireProgramSource(state.DispatchProgram);
if (!program.GetLinkStatus() || !program.GetSpirvStatus()) {
MGL_WIRE_DECLINE_AT(ComputeProgramLinkOrSpirv, "the compute program is not linked or carries no SPIR-V");
return;
}
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
return;
}
const auto pipeline = GetOrCreateComputePipeline(programObj);
if (!pipeline) {
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
return;
}
// PrepareWireTextureResources may have flushed and begun a new recording.
auto& dispatchFrame = m_frameContext.GetCurrent();
if (!dispatchFrame.isCommandRecording) m_frameContext.BeginCommandRecording();
if (!m_uniformManager->BindProgramUniformBuffers(dispatchFrame.commandBuffer, program, programObj,
m_frameContext.GetCurrentFrameIndex(), VK_PIPELINE_BIND_POINT_COMPUTE)) {
MGL_WIRE_DECLINE_AT(ComputeUniformBufferBinding, "the dispatch's uniform/storage bindings could not be bound");
return;
}
vkCmdBindPipeline(dispatchFrame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdDispatchIndirect(dispatchFrame.commandBuffer, parameters.buffer,
parameters.offset + static_cast<VkDeviceSize>(offset));
if (MG_Util::PipeStats::Enabled())
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectNativeDispatches, 1);
}
#endif
@@ -20,6 +20,8 @@
// missing WHICH feature bit - was the one thing it did not. The census, the retrace
// refusal tally and every log grep key on the leading word, so it is unchanged and
// unprefixed; everything after the first space is for the reader of the next device run.
// P8-F: the refusal is a named decline now (WireDeclines.def MipmapShaderFormatOrShape);
// the line it logs keeps this word and this detail.
static String MagmaWireShapeDetail(const char* name, VkFormat format, VkFormat storageFormat,
const VkFormatProperties& props, VkImageAspectFlags aspect,
VkImageViewType viewType, VkImageUsageFlags usage,
@@ -1392,16 +1394,22 @@
// release build both arms ship as. Nothing in GL promises a stencil mip either - the
// pass writes gl_FragDepth and would leave the stencil aspect of the level it produced
// undefined, which is worse than leaving the whole level alone and saying so.
//
// P8-SV: a NAMED decline (WireDeclines.def MipmapDepthStencilAspect), so MGWIRE-DECLINES
// counts it like every other refusal on this arm; the line keeps its words.
if (resource->aspect != VK_IMAGE_ASPECT_COLOR_BIT && resource->aspect != VK_IMAGE_ASPECT_DEPTH_BIT) {
MGLOG_E_ONCE("DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or "
"stencil-only texture, format %d) has no mip generation on either arm; "
"declining and leaving levels %u..%u as they are",
static_cast<Uint32>(resource->aspect), static_cast<Int>(resource->format),
base + 1, base + count - 1);
MGL_WIRE_DECLINE_AT(MipmapDepthStencilAspect,
"DirectVulkan::GenerateWireMipmap: aspect 0x%x (a combined depth/stencil or "
"stencil-only texture, format %d) has no mip generation on either arm; "
"declining and leaving levels %u..%u as they are",
static_cast<Uint32>(resource->aspect), static_cast<Int>(resource->format),
base + 1, base + count - 1);
return;
}
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
MagmaWireFatal("mipmap-growth");
// P8-F: WHICH ARM TAKES THIS CHAIN IS DECIDED BEFORE THE CHAIN IS GROWN, for the
// depth-stencil decline's reason above. Nothing here depends on the growth: the format,
// its features, the knob tier, the aspect, view type, usage and layer window are the
// same before and after it (GrowWireTextureMipChain carries every one of them over).
VkFormatProperties props{};
const VkFormat format = viewFormat == VK_FORMAT_UNDEFINED ? resource->format : viewFormat;
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle,format,&props);
@@ -1411,6 +1419,55 @@
(props.optimalTilingFeatures & required) == required;
const VkFilter filter = props.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT ?
VK_FILTER_LINEAR : VK_FILTER_NEAREST;
const Bool layersInRange = baseLayer < resource->arrayLayers &&
layerCount <= resource->arrayLayers - baseLayer;
// Both shader arms render a 2D view of one layer of one level, so a 1D, 1D-array or 3D
// image is a shape neither takes.
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
// The depth arm's shape: the layer window, and what WireDepthMipmap.inc's pass refuses
// for an image this function hands it (the view type is passed to it below) - a 1D,
// 1D-array or 3D view type, and a format that does not both sample and attach as depth.
// Its other refusals, multisample and a default image, are never built here. P8-SV: the
// view type half is new; before it a 1D depth chain reached the pass, which made a 2D
// view of the 1D image.
const auto depthPassFeatures =
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT;
const Bool depthArmTakesShape = planar &&
layersInRange && (props.optimalTilingFeatures & depthPassFeatures) == depthPassFeatures;
// The colour arm's shape (the two shader shapes are explained at the colour arm below).
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
// The shader arm samples through a sampler2D and writes a vec4, which is what
// BlitWireColorImage's own float check enforces; naming it here rather than letting the
// blit return false is what puts the reason in the decline line.
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
const Bool renderableFormat =
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool attachable = forcedShaderTier < 2 &&
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
const Bool viaScratch = !attachable && renderableFormat &&
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
const Bool colourArmTakesShape =
colour && planar && layersInRange && sampledFloat && (attachable || viaScratch);
// P8-F: `mipmap-shader-format-or-shape` IS A NAMED DECLINE, NOT A FATAL (rule I (a)).
// The monolith arm has always answered this chain with a decline (VulkanRenderer.cpp's
// GenerateMipmap: a format without blit support is skipped and the chain left unwritten),
// and a shape no shader arm takes is a coverage gap in this backend, not a broken record.
// The leading word stays, so every device-log grep keyed on it still finds the line.
const Bool shaderArmTakesShape =
resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT ? depthArmTakesShape : colourArmTakesShape;
if (!nativeBlit && !shaderArmTakesShape) {
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType, resource->usageFlags,
resource->extent, resource->depth, resource->mipLevels, baseLayer, layerCount,
resource->arrayLayers);
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; no mip arm takes this shape, levels %u..%u left as they are",
detail.c_str(), base + 1, base + count - 1);
return;
}
if (!m_textureManager->GrowWireTextureMipChain(handle,base+count,frame.commandBuffer))
MagmaWireFatal("mipmap-growth");
if (!nativeBlit && resource->aspect == VK_IMAGE_ASPECT_DEPTH_BIT) {
// P7 wave 2-B, CONTRACT-P7 §5.1 (A): THE DEPTH ARM, and it is first because a
// depth format without BLIT_DST is the ordinary case, not the exotic one -
@@ -1422,18 +1479,17 @@
// The per-level pass is WireDepthMipmap.inc's, which is the monolith's fragment
// shader with the monolith's arithmetic on a baked pipeline that owns no frontend
// object - §3.2's "observably the monolith's" without the GL program the monolith
// needs to get there.
if (baseLayer >= resource->arrayLayers || layerCount > resource->arrayLayers - baseLayer)
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType,
resource->usageFlags, resource->extent, resource->depth, resource->mipLevels,
baseLayer, layerCount, resource->arrayLayers).c_str());
// needs to get there. Its layer window and format were checked before the growth.
for (Uint32 level = base + 1; level < base + count; ++level) {
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
WireImage source{};
source.image = resource->image;
source.format = format;
source.aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
// P8-SV: the image's own view type, so the pass can refuse a shape it cannot
// render (it makes 2D views) instead of receiving the struct's 2D default.
source.viewType = resource->viewType;
source.is3D = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
source.layout = resource->layout;
source.trackedLayout = &resource->layout;
source.levels = resource->mipLevels;
@@ -1445,15 +1501,19 @@
destination.level = level;
destination.extent = {std::max(resource->extent.width >> level, 1u),
std::max(resource->extent.height >> level, 1u)};
// The pass reports the SHAPE it cannot take (1D, 3D, multisample, a
// format without sampled or depth-attachment support), so the refusal
// keeps its name and gains the reason.
if (!GenerateWireDepthMipLevel(source, destination))
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
// The pass reports the SHAPE it cannot take. depthArmTakesShape above asks
// its questions first, so this is reached only if the two lists drift apart
// - and then it is the same named decline (P8-F), not a session's end. It
// can only refuse the first level: the shape does not change down the chain.
if (!GenerateWireDepthMipLevel(source, destination)) {
const String detail = MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType,
resource->usageFlags, resource->extent, resource->depth,
resource->mipLevels, baseLayer, layerCount,
resource->arrayLayers).c_str());
resource->mipLevels, baseLayer, layerCount, resource->arrayLayers);
MGL_WIRE_DECLINE_AT(MipmapShaderFormatOrShape, "%s; the depth pass refused level %u",
detail.c_str(), level);
return;
}
}
}
} else if (!nativeBlit) {
@@ -1470,27 +1530,9 @@
// cannot, an owned scratch attachment of the destination level's size renders
// the mip and vkCmdCopyImage moves it in: a raw texel move, no conversion, and
// the texels still come from GPU contents rather than from a staged shadow.
const Bool colour = resource->aspect == VK_IMAGE_ASPECT_COLOR_BIT;
const Bool planar = resource->viewType != VK_IMAGE_VIEW_TYPE_1D &&
resource->viewType != VK_IMAGE_VIEW_TYPE_1D_ARRAY &&
resource->viewType != VK_IMAGE_VIEW_TYPE_3D;
const Bool layersInRange = baseLayer < resource->arrayLayers &&
layerCount <= resource->arrayLayers - baseLayer;
// The shader arm samples through a sampler2D and writes a vec4, which is what
// BlitWireColorImage's own float check enforces; naming it here rather than
// letting the blit return false is what puts the reason in the fatal line.
const Bool sampledFloat = vkuFormatIsSampledFloat(format);
const Bool renderableFormat =
(props.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0;
const Bool attachable = forcedShaderTier < 2 &&
(resource->usageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) != 0;
const Bool viaScratch = !attachable && renderableFormat &&
(resource->usageFlags & VK_IMAGE_USAGE_TRANSFER_DST_BIT) != 0;
if (!colour || !planar || !layersInRange || !sampledFloat || (!attachable && !viaScratch))
MagmaWireFatal(MagmaWireShapeDetail("mipmap-shader-format-or-shape", format,
resource->format, props, resource->aspect, resource->viewType,
resource->usageFlags, resource->extent, resource->depth, resource->mipLevels,
baseLayer, layerCount, resource->arrayLayers).c_str());
//
// Which of the two (`attachable` or `viaScratch`), and whether either takes the
// shape at all, was decided before the growth (colourArmTakesShape, P8-F).
for (Uint32 level = base + 1; level < base + count; ++level) {
for (Uint32 layer = baseLayer; layer < baseLayer + layerCount; ++layer) {
WireImage source{};
@@ -1574,14 +1616,24 @@
if (!VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,resource->layout,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,stage,VK_PIPELINE_STAGE_TRANSFER_BIT,access,
VK_ACCESS_TRANSFER_WRITE_BIT,resource->aspect,0,resource->mipLevels)) MagmaWireFatal("mipmap-transition");
// P8-SV: A 3D IMAGE HAS ONE ARRAY LAYER, AND ITS DEPTH IS THE Z EXTENT OF THE BLIT.
// ResolveWireTextureStorage answers `layerCount` in slices for Tex3D (Depth >> level,
// VkTextureManager.cpp) - the addressing attachments, descriptors and readback use - and
// passing that on as the blit's subresource layerCount names layers the image (created
// with arrayLayers = 1) does not have (VUID-vkCmdBlitImage-srcImage-00240,
// -srcSubresource-01707, -dstSubresource-01708). The monolith arm blits layer 0, count 1,
// with the z offsets below halving the depth level by level; so does this one now.
const Bool volume = resource->viewType == VK_IMAGE_VIEW_TYPE_3D;
const Uint32 blitBaseLayer = volume ? 0u : baseLayer;
const Uint32 blitLayerCount = volume ? 1u : layerCount;
for (Uint32 level=base+1;level<base+count;++level) {
VkImageLayout previous = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkTextureManager::TransitionImageLayout(frame.commandBuffer,resource->image,previous,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,VK_PIPELINE_STAGE_TRANSFER_BIT,VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_TRANSFER_WRITE_BIT,VK_ACCESS_TRANSFER_READ_BIT,resource->aspect,level-1,1);
VkImageBlit blit{};
blit.srcSubresource = {resource->aspect,level-1,baseLayer,layerCount};
blit.dstSubresource = {resource->aspect,level,baseLayer,layerCount};
blit.srcSubresource = {resource->aspect,level-1,blitBaseLayer,blitLayerCount};
blit.dstSubresource = {resource->aspect,level,blitBaseLayer,blitLayerCount};
blit.srcOffsets[1] = {static_cast<Int32>(std::max(resource->extent.width>>(level-1),1u)),
static_cast<Int32>(std::max(resource->extent.height>>(level-1),1u)),
static_cast<Int32>(std::max(resource->depth>>(level-1),1u))};
@@ -22,6 +22,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 CacheVersion = MG_Config::CacheVersion;
Uint32 SurfaceWidth = 1;
Uint32 SurfaceHeight = 1;
// The app's eglSwapInterval; empty until it asks. Window renderers only - see
// VulkanRenderer::SetSwapInterval.
Optional<Int> SwapInterval;
Bool DisablePipelineCache = false;
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
Bool EnableValidationLayers = true;
+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{};
};
}
+11 -6
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>
@@ -81,9 +82,10 @@ namespace MobileGL::MG_Backend {
// DirectGLES consumes all thirteen migrated families (P2's 0..6, P3a's 7..8, P4a's
// 9..12): it registers the resource op table in Initialize()
// (BackendObject_DirectGLES.cpp:849) and reads every other family out of gPipeInputs.
// DirectVulkan registers NO resource ops - MGPipeSetResourceOps has exactly one caller
// in the whole tree and it is Managers.cpp:2594 - so bit 7 is CLEAR for it, which is
// the same fact ObjectSubsystemControlScenario already pins from the client side.
// DirectVulkan registers its wire resource op table off monolith only
// (VkBufferManager::RegisterWireResourceOps, VkBufferManager.cpp): under a transport it
// owns buffer stores and claims bit 7 (the P4a mask, see ConsumedSubsystemsFor below);
// under monolith it registers none and its buffers keep the legacy BufferBackendOps path.
// P5e (MG_Remote/CONTRACT-P5E.md §6.2). THE ONE CONSTANT THE WHOLE PHASE HANGS ON.
//
// kCapRunAheadApply says "this server applies an unbarriered record without reading
@@ -185,9 +187,9 @@ namespace MobileGL::MG_Backend {
// ZERO IS THE EXPLICIT ANSWER FOR P5, not an omission (ServerSession.h's block):
// every optional capability bit belongs to the package that owns its question, and
// withholding one leaves the legacy path running, which is the safe direction.
// kCapNeedsHostIndexBytes and kCapNeedsHostUboBytes must be 0 for the whole of P5
// by ruling - they are the only two things that ask for an MGHostSpan, and 0 is
// what keeps every one of them out of the first IPC frame (contract table 0).
// kCapNeedsHostIndexBytes, kCapNeedsHostUboBytes and kCapViewportArray are RESERVED
// (P8-F) and never set here: the first two are the only things that ask for an
// MGHostSpan, and 0 keeps every one of them off the wire (contract table 0).
//
// P5b t2 (CONTRACT-P5B.md §6.5) PUBLISHES THE ONE BIT P5b ADDS, and this is the
// only place that can: the question kCapBackendOwnsXfbCapture answers is "does the
@@ -397,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
+385 -27
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
@@ -146,19 +258,51 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_NO_SURFACE;
}
Uint32 requestedWidth = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_WIDTH, 0), 0));
Uint32 requestedHeight = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0));
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
// P12: A GAME'S OWN WINDOW SIZE IS THE SIZE OF THE FRAMES IT RENDERS, and a server-owned
// window has to take that size or clip. SDL (Minecraft's Android backend) creates the EGL
// window surface with no EGL_WIDTH / EGL_HEIGHT, so the request went out as 0x0 - "use the
// server window's own size" - and a landscape 2620x1280 game drawn into a portrait 1280x2620
// server window came out clipped to its bottom-left 1280x1280 corner (Espryt and Magma
// 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 (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 on-screen surface is asked for "
"the native window's %dx%d", nativeWidth, nativeHeight);
}
}
#endif
const MG_Backend::WindowHandle windowHandle = {
.Backend = DetectWindowBackend(),
.Handle = ToVoidHandle(window),
.Width = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_WIDTH, 0), 0)),
.Height = static_cast<Uint32>(std::max<EGLint>(GetAttribValue(attrib_list, EGL_HEIGHT, 0), 0)),
.Width = requestedWidth,
.Height = requestedHeight,
};
#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
@@ -188,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;
}
@@ -197,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);
@@ -207,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;
}
@@ -215,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) {
@@ -259,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();
@@ -275,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);
@@ -332,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);
}
@@ -440,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;
@@ -682,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;
@@ -698,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
@@ -726,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
@@ -799,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>(
@@ -41,6 +41,18 @@
// call sites below are declarations only, exactly as the frontend's mutators are.
#include <MG_Pipe/PipeMutation.h>
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): AN UNPACK SOURCE IS READ FROM THE BUFFER'S CLIENT SHADOW,
// and under a transport a GPU write into that buffer - a read_pixels_to_buffer, which is how a
// glReadPixels into a pack buffer now crosses, or a shader store - reaches the shadow only through
// SyncGpuWrites, exactly as glMapBufferRange and glGetBufferSubData already do. Monolith keeps its
// read as it was: its ReadPixels maps the driver PBO back into the shadow inside the call, and the
// pull build compiles this to nothing (G1).
#if MOBILEGL_BUILD_DISAGGREGATED
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) do { if (MG_Config::Transport != MG_Config::TransportMode::Monolith) (buffer)->SyncGpuWrites(); } while (0)
#else
#define MGL_SYNC_PIXEL_UNPACK_SOURCE(buffer) ((void)0)
#endif
namespace MobileGL::MG_Impl::GLImpl {
static SharedPtr<MG_State::GLState::ITextureObject> nullTextureObject;
static UnorderedMap<Uint, Bool> g_autoGenerateMipmapByTextureId;
@@ -1773,6 +1785,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& unpackBuffer =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (!unpackBuffer) return data;
MGL_SYNC_PIXEL_UNPACK_SOURCE(unpackBuffer);
return reinterpret_cast<const char*>(unpackBuffer->MappedData()) + reinterpret_cast<SizeT>(data);
}
@@ -1819,6 +1832,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& pixelUnpackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (pixelUnpackBufferObject) {
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
@@ -1941,6 +1955,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (pixelUnpackBufferObject) {
MGLOG_D("TexSubImage2D_State: Using Pixel Unpack Buffer Object ID: %u",
pixelUnpackBufferObject->GetExternalIndex());
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
@@ -2030,6 +2045,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& pixelUnpackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (pixelUnpackBufferObject) {
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
@@ -2535,6 +2551,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (pixelUnpackBufferObject) {
MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__,
pixelUnpackBufferObject->GetExternalIndex());
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
@@ -2682,6 +2699,7 @@ namespace MobileGL::MG_Impl::GLImpl {
if (pixelUnpackBufferObject) {
MGLOG_D("%s: Using Pixel Unpack Buffer Object ID: %u", __func__,
pixelUnpackBufferObject->GetExternalIndex());
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
@@ -2800,6 +2818,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& pixelUnpackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (pixelUnpackBufferObject) {
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
@@ -6262,6 +6281,7 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& pixelUnpackBufferObject =
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::PixelUnpack).GetBoundObject();
if (pixelUnpackBufferObject) {
MGL_SYNC_PIXEL_UNPACK_SOURCE(pixelUnpackBufferObject);
originalPixels = reinterpret_cast<const char*>(pixelUnpackBufferObject->MappedData()) +
reinterpret_cast<SizeT>(pixels);
}
+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
+4 -4
View File
@@ -247,10 +247,10 @@ namespace MobileGL::MG_Pipe {
for (Uint32 w = 0; w < kMGPipeShaderBufferWritableMaskWords; ++w) {
header.WritableMask[w] = writableMask[w];
}
// HostSpanCount IS 0 ALWAYS on Espryt, and that is a ruling rather than an omission:
// the second var-tail exists for kCapNeedsHostUboBytes (Magma's named-UBO ring) and
// that bit is 0 for the whole of P5 (CONTRACT-P5.md table 0). The codec's host-span
// honesty pass is the guard that says so out loud if a backend ever publishes it.
// HostSpanCount IS 0 ALWAYS, and that is a ruling rather than an omission: the
// second var-tail exists for kCapNeedsHostUboBytes (a Magma named-UBO ring that was
// never built) and that bit is RESERVED (P8-F): no server publishes it. The codec's
// host-span honesty pass is the guard that says so out loud if a peer sends one.
header.HostSpanCount = 0;
header.ContentHash = hash;
MGPipeRouteSetShaderBuffers(header, m_entries.data());
+51 -16
View File
@@ -233,9 +233,9 @@ namespace MobileGL::MG_Pipe {
desc.StorageKind = MGPipeTextureStorageKindForTarget(texture.GetTarget());
desc.BindMask = bindMask;
// STICKY AND FOREVER: everImageBound, the client-side answer MGPipeTypes.h asks for.
// It is the PREVENTION half of the texture-remint stall class - a texture the server
// knows may be image-bound is allocated image-bindable up front, so the re-mint that
// would have to pull its texels back never happens.
// It PREVENTS the texture re-mint - a texture the server knows may be image-bound is
// allocated image-bindable up front, so the server never has to read its levels back
// and re-upload them into a new carrier (P9 W2).
desc.ImageBindableHint = (bindMask & kMGPipeBindShaderImage) != 0 ? 1 : 0;
if (storageDefined) {
const MGPipeTextureExtent extent = MGPipeTextureExtentOf(texture);
@@ -702,8 +702,7 @@ namespace MobileGL::MG_Pipe {
// mask rides resource_create and every resource_respecify - and an IMMUTABLE texture has
// no further respecify, that being what immutable means - so for the canonical order
// `glTexStorage2D(...); glBindImageTexture(...)` the applier's record kept
// ImageBindableHint = 0 for ever and the PREVENTION half of the texture-remint stall
// class never fired. So a mask that actually MOVES re-emits the stored descriptor with
// ImageBindableHint = 0 for ever and the re-mint PREVENTION never fired. So a mask that actually MOVES re-emits the stored descriptor with
// the new mask: every storage-defining field is byte-identical to what the applier
// holds, which is exactly the shape wire applies as a METADATA UPDATE - the descriptor
// is replaced, no reallocation is acked, and NO pending upload is dropped, so a mask
@@ -1470,21 +1469,53 @@ namespace MobileGL::MG_Pipe {
const MGPBox unionBox = MGPipeBoxOfDirtyRegion(mipmap->GetStorageDirtyRegion(uploadTarget, level));
if (unionBox.W == 0 || unionBox.H == 0 || unionBox.D == 0) return false;
// THE REGION LIST BEHIND THE BOX. 0 is legal and means "the union box is the whole
// story" - it covers a single rect (identical to the box by construction), more
// rects than the cap, and a summed area so close to the box's that one big upload
// beats many small ones. The invariant that makes the server's choice safe is that
// the two describe the SAME texels: every rect lies inside the box, and their union
// is the box.
// THE REGION LIST BEHIND THE BOX IS THE LEVEL'S EXACT DIRTY FOOTPRINT (P11 M2, dev
// 9524c688): disjoint boxes holding every texel written since the level was last
// clean and no other. The storage's rect list this used to carry only COVERS the
// writes - it merges touching rects into their bounding box, folds when full and is
// withheld (0) past 3/4 of the union box - and once the far side's image may hold GPU
// writes, whatever it uploads beyond the writes goes up over them. Both servers read
// the list as the texels to upload: Magma always, Espryt once its driver texture may
// hold GPU writes (and otherwise it re-applies the storage's box-or-rects choice
// itself, so the Mali job count is the server's call as before).
//
// 0 IS NOW THE WHOLE-LEVEL SPELLING - a footprint that is the whole level - with one
// exception (below). Every other write carries its boxes, a single one included, so the
// applier's accumulation (boxes union, lists concatenate, "0 regions" makes the entry
// box only) never widens two exact contributions into their union box. THE EXCEPTION:
// a footprint larger than one record may carry (kMGPipeMaxPendingUploadRegions) falls
// back to the storage's rect list - a COVER, stated once in the log - and when that list
// is withheld (0: its rects fill 3/4 of the union box) this 0 is a PARTIAL, INEXACT
// box. A reader that takes "box only" as "the client owns every texel of the box"
// (AdoptDriverLevelIntoShadow's covering-box rule) is exact everywhere but here.
//
// READ AS BOXES, ONCE, because both shapes below want them in that form: the
// whole-level record converts each one straight back through MGPipeBuildSubRegion,
// and a piece clips each one to its own slab.
MG_State::GLState::MipmapDirtyRegion rects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
const SizeT rectCount = mipmap->GetStorageDirtyRects(
uploadTarget, level, rects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
MGPBox rectBoxes[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
for (SizeT i = 0; i < rectCount; ++i) rectBoxes[i] = MGPipeBoxOfDirtyRegion(rects[i]);
mipmap->GetStorageDirtyFootprint(uploadTarget, level, m_footprint);
m_rectBoxes.clear();
const Bool wholeLevelWrite =
m_footprint.size() == 1 && m_footprint.front().lo.x() <= 0 && m_footprint.front().lo.y() <= 0 &&
m_footprint.front().lo.z() <= 0 && m_footprint.front().hi.x() >= levelSize.x() &&
m_footprint.front().hi.y() >= levelSize.y() &&
m_footprint.front().hi.z() >= std::max<Int>(levelSize.z(), 1);
if (wholeLevelWrite) {
// The box is the story.
} else if (m_footprint.size() <= static_cast<SizeT>(kMGPipeMaxPendingUploadRegions)) {
for (const auto& box : m_footprint) m_rectBoxes.push_back(MGPipeBoxOfDirtyRegion(box));
} else {
MGLOG_W_ONCE("MGPipe: texture {slot=%u, gen=%u} level %u has %zu disjoint dirty boxes, more than one "
"record carries (%u); it goes out as the storage's rect list, which covers the writes "
"but not exactly",
pending.Handle.Slot, pending.Handle.Gen, static_cast<Uint>(level), m_footprint.size(),
kMGPipeMaxPendingUploadRegions);
MG_State::GLState::MipmapDirtyRegion rects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
const SizeT coverCount = mipmap->GetStorageDirtyRects(
uploadTarget, level, rects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
for (SizeT i = 0; i < coverCount; ++i) m_rectBoxes.push_back(MGPipeBoxOfDirtyRegion(rects[i]));
}
const SizeT rectCount = m_rectBoxes.size();
const MGPBox* rectBoxes = m_rectBoxes.data();
Bool dispatched = false;
Bool accepted = true;
@@ -1624,6 +1655,10 @@ namespace MobileGL::MG_Pipe {
Vector<Entry> m_renderbuffers;
Vector<DrainEntry> m_drain;
Vector<MGPSubRegion> m_regions;
// EmitOneLevel's scratch, kept for its capacity: the level's exact footprint and the
// boxes the record's regions are built from.
Vector<MG_State::GLState::MipmapDirtyRegion> m_footprint;
Vector<MGPBox> m_rectBoxes;
Bool m_draining = false;
MGPResourceDesc m_lastDesc{};
+10 -2
View File
@@ -263,8 +263,8 @@ namespace MobileGL::MG_Pipe {
if (vao) {
if (const auto& bound = vao->GetIndexBufferBindingSlot().GetBoundObject()) {
m_lastIndex.Res = MGPipeSlots().Acquire(MGPipeKind::Buffer, bound->GetLifetimeId());
// The ELEMENT_ARRAY bit, and it is the one the split path keys on
// (kCapNeedsHostIndexBytes -> restart rewriting, multi-draw flattening).
// The INDEX|ELEMENT_ARRAY bind bits. ELEMENT_ARRAY's D-B7 reader
// (kCapNeedsHostIndexBytes) is RESERVED (P8-F) and was never built.
// Noted at every draw for RefreshBindMask's reason: an EBO defined through
// DSA and unbound before its last respecify would otherwise never publish
// it, and getting that bit wrong is invisible in monolith.
@@ -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:
File diff suppressed because it is too large Load Diff
@@ -29,9 +29,11 @@ namespace MGITest {
};
bool PeekPipeTextureResourceRecord(unsigned glTextureName, PipeTextureResourceRecordPeek* out);
// The process-wide texture-remint pull count (PipeStats "tex-remint-pulls", `trp=` on the
// The process-wide texture re-mint count (PipeStats "tex-remint-pulls", `trp=` on the
// summary line; ROADMAP open question 2). Arms the PipeStats counters for this process on
// the first call, which is what lets a case read the number without a stats-enabled lane.
// Process-wide means it sees the SERVER's count only where the server is this process:
// monolith and inproc, never spawn or tcp.
bool PeekPipeStatsTextureRemintPulls(unsigned long long* out);
// Espryt's count of texture uploads it actually issued (PipeStats "tex-upload-emissions"):
// what tells a CONSUMED pending upload apart from a DROPPED one, since the record's set is
@@ -25,6 +25,9 @@ foreach(listName IN ITEMS @MGL_SPLIT_ARM_TEST_LISTS@)
elseif(entry MATCHES "PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares$")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT
"MGITEST_PMAP_LANE=1;MGITEST_PERSISTENT_MAP_ARM=emulated;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1")
elseif(entry MATCHES "LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares$")
# P11 A2: every split arm runs T2, so the arena is declared emulated.
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MGITEST_PERSISTENT_MAP_ARM=emulated")
elseif(entry MATCHES "CtWireScenario\\.")
set_tests_properties("${entry}" PROPERTIES ENVIRONMENT "MOBILEGL_TEST_SERVER_COUNTERS=1")
endif()
@@ -20,6 +20,7 @@
#include <MG_Remote/Client/CapsMirror.h>
#include <MG_Remote/Client/ClientSession.h>
#include <MG_Remote/Client/EmitTables.h>
#include <MG_Remote/Client/GpuWritePending.h>
#include <MG_Remote/Server/ServerLoop.h>
#include <chrono>
#include <atomic>
@@ -164,6 +165,14 @@ namespace MGITest {
state.cmdWrapPads = encoder.CmdWrapPads();
state.cmdBytesWritten = encoder.CmdBytesWritten();
state.stageReclaimWaits = encoder.StageReclaimWaits();
state.replyPostings = session->ReplyPostings();
state.packBufferReadbackMarks = MobileGL::MG_Remote::Client::ProducerMarkCount(
MobileGL::MG_Remote::Client::GpuWriteProducer::ReadPixelsPackBuffer);
const auto fences = MobileGL::MG_Remote::Client::ReadFencePollCounters();
state.fenceLocalAnswers = fences.LocalAnswers;
state.fenceEscalations = fences.Escalations;
state.fenceRoundTrips = fences.RoundTrips;
state.fenceServerReports = fences.ServerReports;
}
#endif
return state;
@@ -119,6 +119,26 @@ namespace MGITest {
unsigned long long cmdWrapPads = 0;
unsigned long long cmdBytesWritten = 0;
unsigned long long stageReclaimWaits = 0;
// ---- P9 (W1, CONTRACT-P9.md §1): the pack-buffer readback's two proofs -------------
//
// replyPostings: records this client published on a row that carries a reply slot
// (ClientSession::ReplyPostings, always counted - it is the create window's clock, not a
// stats counter). A read into a pack buffer that moves it went through the reply form.
// packBufferReadbackMarks: GpuWritePending's row-4 producer count - the buffers a pack
// read marked GPU-written. The reply form marks nothing, so the pair tells the two forms
// apart from both sides.
unsigned long long replyPostings = 0;
unsigned long long packBufferReadbackMarks = 0;
// ---- P10 (CONTRACT-P10.md §1): what the fence polls cost ----------------------------
// EmitTables.h FencePollCounters, verbatim: polls answered with no record, zero-timeout
// polls escalated to a round trip, every fence answer that crossed, and the server's
// kEventFenceSignaled reports consumed.
unsigned long long fenceLocalAnswers = 0;
unsigned long long fenceEscalations = 0;
unsigned long long fenceRoundTrips = 0;
unsigned long long fenceServerReports = 0;
};
SplitRuntimeState PeekSplitRuntime();
@@ -0,0 +1,38 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.cpp
// Copyright (c) 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 "WireDeclinePeek.h"
#if !defined(__ANDROID__)
#include <Config.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Backend/DirectVulkan/Renderer/WireDeclineTally.h>
#include <cstring>
#define MGITEST_WIRE_DECLINE_PEEK_LIVE 1
#endif
#endif
namespace MGITest {
#if defined(MGITEST_WIRE_DECLINE_PEEK_LIVE)
bool PeekWireDeclineCount(const char* row, unsigned long long* out) {
if (row == nullptr || out == nullptr) return false;
namespace Magma = MobileGL::MG_Backend::DirectVulkan;
for (MobileGL::SizeT i = 0; i < Magma::WireDeclineTally::kSiteCount; ++i) {
const auto site = static_cast<Magma::WireDeclineSite>(i);
if (std::strcmp(Magma::WireDeclineSiteName(site), row) != 0) continue;
*out = Magma::WireDeclineTally::Get(site);
return true;
}
return false;
}
#else
bool PeekWireDeclineCount(const char*, unsigned long long*) { return false; }
#endif
} // namespace MGITest
@@ -0,0 +1,20 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireDeclinePeek.h
// Copyright (c) 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
//
// P8-SV: one row of the Magma wire arm's decline tally (WireDeclines.def, the counts behind the
// server's MGWIRE-DECLINES lines), read as a run total out of THIS process - WireIndirectPeek.h's
// shape. Process-wide means the SERVER's counts only where the server is this process: the inproc
// arm, never spawn or tcp, and never the monolith arm, which takes no wire path. False where the
// reading cannot be taken: a pull build, Android, or a name that is not a row.
#pragma once
namespace MGITest {
bool PeekWireDeclineCount(const char* row, unsigned long long* out);
} // namespace MGITest
@@ -0,0 +1,38 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.cpp
// Copyright (c) 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 "WireIndirectPeek.h"
#if !defined(__ANDROID__)
#include <MG_Pipe/MGPipe.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Util/Metrics/PipeStats.h>
#define MGITEST_WIRE_INDIRECT_PEEK_LIVE 1
#endif
#endif
namespace MGITest {
#if defined(MGITEST_WIRE_INDIRECT_PEEK_LIVE)
bool PeekWireIndirectCounters(WireIndirectCounters* out) {
if (out == nullptr) return false;
namespace Stats = MobileGL::MG_Util::PipeStats;
if (!Stats::Enabled()) Stats::SetEnabledForTesting(true);
out->nativeDraws = Stats::TotalCalls(Stats::CallClass::WireIndirectNativeDraws);
out->cpuExpansions = Stats::TotalCalls(Stats::CallClass::WireIndirectCpuExpansions);
out->barriers = Stats::TotalCalls(Stats::CallClass::WireIndirectBarriers);
out->hostWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaits);
out->indirectWaits = Stats::TotalCalls(Stats::CallClass::WireHostWaitsIndirect);
out->nativeDispatches = Stats::TotalCalls(Stats::CallClass::WireIndirectNativeDispatches);
return true;
}
#else
bool PeekWireIndirectCounters(WireIndirectCounters*) { return false; }
#endif
} // namespace MGITest
@@ -0,0 +1,29 @@
// MobileGL - MobileGL/MG_IntegrationTest/Harness/WireIndirectPeek.h
// Copyright (c) 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
//
// P8-D: the Magma wire arm's indirect-draw counters (PipeStats `windr[...]`), read as run totals
// out of THIS process - P4aFinalFixPeek.cpp's shape. Process-wide means the SERVER's counts only
// where the server is this process: monolith and inproc, never spawn or tcp. The first call arms
// the counters (a case reads before and after its workload, so it must peek before it draws).
// False where the reading cannot be taken: a pull build, Android.
#pragma once
namespace MGITest {
struct WireIndirectCounters {
unsigned long long nativeDraws = 0; // wire-indirect-native-draws (`wind`)
unsigned long long cpuExpansions = 0; // wire-indirect-cpu-expansions (`wixp`)
unsigned long long barriers = 0; // wire-indirect-barriers (`wibar`)
unsigned long long hostWaits = 0; // wire-host-waits (`whw`)
unsigned long long indirectWaits = 0; // wire-host-waits-indirect (`whwi`)
unsigned long long nativeDispatches = 0; // P8-SV: wire-indirect-native-dispatches (`wdsp`)
};
bool PeekWireIndirectCounters(WireIndirectCounters* out);
} // namespace MGITest
@@ -0,0 +1,244 @@
# P8-A (ID-P8-2): the monolith cases' split, spawn and tcp registrations.
#
# Included ONCE from CMakeLists.txt, inside `if (MOBILEGL_BUILD_DISAGGREGATED)`, and the place is
# load-bearing twice over: it must come BEFORE the Magma tier-2 replay (every
# mgl_itest_register_split_arms call here is replayed onto DirectVulkan's informational
# integration-magma-all-{split,spawn,tcp} arms from MGL_SPLIT_ARM_CALLS) and BEFORE the
# SplitLogPaths configure_file at the end of that file (every entry here gets its private
# MOBILEGL_LOG_FILE_PATH from MGL_SPLIT_ARM_TEST_LISTS, or SplitLogPaths.PrivateAndDistinct reds).
#
# THE GATE IS scripts/ci/split_coverage.py: after the arm and tail segments come off, every
# monolith DirectGLES. / DirectVulkan. case is on a gated split arm or is named, with a reason
# class, in scripts/data/split_coverage_exemptions.txt. Deleting a line below reds it BY NAME.
# The per-case census this list was cut from (inproc / spawn / tcp, junit per arm) is
# docs/Disaggregated/notes/p8/A.md.
# ---- (a) THE AMBIENT CASES -----------------------------------------------------------------
#
# Every case here was registered on monolith only, and on all three arms under the armed split
# environment it either PASSED (records crossed: ScenarioFixture's EmitSeq assertion is armed on
# these arms) or SKIPPED for the same reason its monolith entry skips on this host (ClipDistance,
# DualSourceBlend, IterationRP*, SsboDeclarationForm's two known gaps, and the knob-pinned cases
# the (a') block below runs with their knob). Cases that belong to monolith by nature are in the
# exemption table instead, never here: no-records (limits / shader-binary queries answered from
# the caps mirror), single-backend (Magma-only lanes and declines), mechanism, pending-fix.
#
# ONE FILTER, not one call per scenario: each call is three discovery runs plus three more for
# the Magma replay. Explicit names where a scenario is split between registered and exempt
# cases; `Scenario.*` only where every case of the scenario is here.
set(MGL_P8A_AMBIENT_CASES
AdvertisedLimitsScenario.ImageUnitBindingsAreReportedFromTheFrontendState
AdvertisedLimitsScenario.IndexedBufferBindingsAreReportedVerbatimOnBothWidths
BufferTextureScenario.*
ClearTexImageUndefinedLevelZeroScenario.*
ClipDistanceScenario.*
CopyImageLevelRangeScenario.*
CopyImagePacked16Scenario.*
CrossFrameBufferScenario.*
DefaultFramebufferAcrossSwapScenario.*
DepthStencilReadbackAttachmentShapeScenario.DepthOfALayeredCubeAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.DepthOfAPlainTexture2DAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.DepthOfAnArrayLayerAttachmentReadsBack
DepthStencilReadbackAttachmentShapeScenario.PackedArrayLayerAttachmentReadsBackBothAspects
DepthStencilReadbackScenario.*
DoublePrecisionScenario.*
DualSourceBlendScenario.*
EmptyScissorScenario.*
F1WireScenario.ColorBlitToDefaultFromANonSampleableSourceDegrades
F1WireScenario.ColorBlitToDefaultHonorsScissorAndReversedRect
F1WireScenario.ColorBlitToDefaultIgnoresViewportAndPreservesOrientation
F1WireScenario.ComputeImageStoreFramebufferPixels
F1WireScenario.ComputeImageStoreThroughViewPreservesOtherRootLayer
F1WireScenario.ComputeWrittenVertexAndIndexBuffersDrawAndReadBack
F1WireScenario.CopyImageInPlaceAcrossLevelsAndLayers
F1WireScenario.EnabledVertexBufferDrawKeepsNamedP7Fatal
F1WireScenario.GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels
F1WireScenario.GenerateMipmapThroughViewKeepsOwnerLevelsAndLayersOutsideItsWindow
F1WireScenario.MultisampleColorBlitScalesAndFlipsThroughTheResolveScratch
F1WireScenario.PartialTextureUploadPreservesGpuClearPixels
F1WireScenario.SrgbDrawTracksFramebufferConversion
F1WireScenario.StreamedBufferSubDataBeforeEachDrawIsOrdered
F1WireScenario.Texture1DFramebufferClearPixels
F1WireScenario.TextureReadbackLargerThanAReplySlotContainsGpuWrites
F1WireScenario.TextureViewClearTargetsItsRootMipAndLayer
F1WireScenario.UniformBufferRangeAndRebindPixels
F1WireScenario.VertexIdSamplerAndScalarUniformPixels
FormatlessImageBakeScenario.*
FragCoordOriginScenario.*
FragmentOutputArrayIndexScenario.*
GeometryDrawModeScenario.*
GuiBatchScenario.*
# dev f973008c merge: the empty-binding uniform block (853c5f12 + P11 7784d6bb)
UnboundUniformBlockScenario.*
ImageFormatQualifierScenario.*
ImageLoadStoreSsoScenario.PerElementImageUnitsReachAPipelineDraw
ImageLoadStoreSsoScenario.ReassigningAnImageUnitBetweenDrawsReachesTheNextDraw
IterationRPFirstReductionScenario.*
IterationRPProgram203Scenario.*
IterationRPScratchFixScenario.*
LayeredAttachmentBarrierScenario.*
LayeredAttachmentShapeScenario.*
NonCoreImageFormatScenario.*
OrientationScenario.*
P4aFinalFixScenario.*
P4aSeamAuditScenario.*
PipelineFailureScenario.*
PointSizeDemotionScenario.*
ProgramPipelineScenario.ASamplerUnitRewrittenBetweenDrawsKeepsPaintingTheRightTexture
ProgramPipelineScenario.AStageProgramsUniformBlockBindingReachesThePipelineDraw
ProgramPipelineScenario.ATwoStagePipelinePaintsWhatItsStagesDescribe
ProgramPipelineScenario.AUniformDeclaredInTwoStagesKeepsTheValueTheWrittenStageHolds
ProgramPipelineScenario.ComputeAndGraphicsStagesShareOnePipeline
ProgramPipelineScenario.RebindingAUniformBlockBetweenDrawsReachesTheNextDraw
ProgramPipelineScenario.SwitchingBetweenAPipelineAndAMonolithicProgramLeavesNoError
ProgramPipelineScenario.UniformsGoToTheActiveShaderProgram
RenderbufferBlendFormatScenario.*
ResidentIndexScenario.*
SampleMaskScopeScenario.*
SampleVariablesScenario.*
SnormAttachmentScenario.*
SpirvShaderBinaryScenario.SpecializedModulesLinkAndRenderWithTheirConstantsApplied
SsboArrayDynamicIndexScenario.*
SsboArrayLengthScenario.*
SsboDeclarationFormScenario.*
StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding
StreamedArenaScenario.*
TessellationDrawModeScenario.*
ThreeChannelAttachmentScenario.*
UnboundCounterBlockScenario.*
UnlocatedIoBlockScenario.*
UnwrittenPositionOutputScenario.ARedeclaredButUnwrittenPositionStillDraws
UnwrittenPositionOutputScenario.AShaderWithNoPositionBlockStillDraws
UnwrittenPositionOutputScenario.AWrittenRedeclaredPositionStillDraws
UnwrittenPositionOutputScenario.CapturingAWrittenPositionStillDraws
VertexArrayEnableDisableScenario.*
VertexAttribBindingScenario.*
ViewportArrayScenario.*
)
list(JOIN MGL_P8A_AMBIENT_CASES ":" mglP8aAmbientFilter)
mgl_itest_register_split_arms("${mglP8aAmbientFilter}" "")
# ---- (a') THE SERVER-READ KNOB LANES, EACH WITH A TCP SUPERVISOR OF ITS OWN ------------------
#
# The monolith file pins five whole-process knobs in lanes of their own (PointSizeDemotion.,
# UnlocatedIoBlocks., NoViewportArrayEmulation., WidenedPacked16., ForcedDepthStencilEmulation.),
# because without them the cases are unfalsifiable on llvmpipe or skip. Every one of the five is
# read through ConfigLoader IN THE PROCESS THAT RUNS THE BACKEND - the server. Under inproc that
# is this process and under spawn the child inherits ::environ, so the entry's ENVIRONMENT
# reaches it; under tcp the server is a supervisor started by a fixture, and the lane-wide
# TcpServer.Start supervisor never sees a per-entry variable. MEASURED on this tree before this
# block existed: with the knob in the client's environment only, the Tcp copies of
# PointSizeDemotion's and UnlocatedIoBlocks' arming cases read no arming line and
# NoViewportArrayEmulation's negative control saw the emulation still on - red, where the Split
# and Spawn copies were green.
#
# THE KNOB CANNOT GO ON THE SHARED FIXTURE (the IC TcpServer.Start precedent, which works for
# MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW only because every arm sets that one): it would retarget
# every other tcp case - viewport emulation off breaks ViewportArrayScenario's positive cases,
# the IO-block strip rewrites every interface block. So each lane gets a supervisor of its own,
# on its own port, started by its own fixture with the knob in the fixture's ENVIRONMENT.
#
# THE PORTS: the lane endpoint's port + MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET + the lane's index.
# The default offset (1000) keeps the five clear of every per-tree lane endpoint in use (40613
# CI, 407xx / 409xx split and verify builds, 484x0 package trees). One overlap remains: a tree
# whose split and verify builds sit one port apart (409x3 / 409x4) gets knob ports one apart as
# well, so the two builds must not run integration-tcp at the same moment - set the cache
# variable differently in one of them if they ever do.
#
# THE LOCK IS STILL mobilegl-tcp: spawn_lane_parity.py / junit_tally.py require it of every
# integration-tcp entry, and one tcp session at a time is what every other tcp entry assumes.
#
# NOT FOR DirectVulkan: all five knobs are Espryt's (Magma routes viewports natively, has no
# IO-block strip, no packed-16 widening, no DS readback emulation; point-size demotion's Magma
# lane is monolith-only and stays so here).
set(MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET 1000 CACHE STRING
"Port offset (from MOBILEGL_ITEST_TCP_ENDPOINT's) of the per-knob loopback supervisors")
if(NOT MOBILEGL_ITEST_TCP_ENDPOINT MATCHES "^(tcp://.+):([0-9]+)$")
message(FATAL_ERROR "P8-A: MOBILEGL_ITEST_TCP_ENDPOINT '${MOBILEGL_ITEST_TCP_ENDPOINT}' is not "
"tcp://<host>:<port>; the per-knob tcp supervisors derive their ports from it")
endif()
set(mglP8aTcpHost "${CMAKE_MATCH_1}")
set(mglP8aTcpPort "${CMAKE_MATCH_2}")
set(MGL_P8A_KNOB_LANES PointSizeDemotion UnlocatedIoBlocks NoViewportArrayEmulation WidenedPacked16
ForcedDs)
set(MGL_P8A_KNOB_FILTER_PointSizeDemotion "PointSizeDemotionScenario.*")
set(MGL_P8A_KNOB_ENV_PointSizeDemotion "MOBILEGL_POINT_SIZE_DEMOTION=1")
set(MGL_P8A_KNOB_FILTER_UnlocatedIoBlocks "UnlocatedIoBlockScenario.*")
set(MGL_P8A_KNOB_ENV_UnlocatedIoBlocks "MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS=1")
# One case, as on monolith: the three positive cases describe behaviour the backend does not
# have with the emulation off.
set(MGL_P8A_KNOB_FILTER_NoViewportArrayEmulation
"ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero")
set(MGL_P8A_KNOB_ENV_NoViewportArrayEmulation "MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION=0")
set(MGL_P8A_KNOB_FILTER_WidenedPacked16 "CopyImagePacked16Scenario.*")
set(MGL_P8A_KNOB_ENV_WidenedPacked16 "MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE=1")
# ForcedDs keeps the spelling the existing Split/Spawn DepthStencilReadbackMatrix block uses. The
# default-framebuffer case was left out here while it was red on spawn and tcp; P8-SE fixed it
# (notes/p8/SE.md) and it runs under the knob on every arm like the rest.
set(MGL_P8A_KNOB_FILTER_ForcedDs
"DepthStencilReadbackScenario.*:DepthStencilReadbackAttachmentShapeScenario.*")
set(MGL_P8A_KNOB_ENV_ForcedDs "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1")
# The Matrix scenario's Tcp copy moves HERE from the ForcedDs block (same names): that block
# put the knob in the client's environment only, so its Tcp arm ran the native readback path.
set(MGL_P8A_KNOB_TCP_FILTER_ForcedDs
"DepthStencilReadbackMatrixScenario.*:${MGL_P8A_KNOB_FILTER_ForcedDs}")
set(mglP8aKnobIndex 0)
foreach(mglP8aLane IN LISTS MGL_P8A_KNOB_LANES)
string(TOLOWER "${mglP8aLane}" mglP8aLaneLower)
math(EXPR mglP8aKnobPort "${mglP8aTcpPort} + ${MOBILEGL_ITEST_TCP_KNOB_PORT_OFFSET} + ${mglP8aKnobIndex}")
math(EXPR mglP8aKnobIndex "${mglP8aKnobIndex} + 1")
set(mglP8aKnobEndpoint "${mglP8aTcpHost}:${mglP8aKnobPort}")
set(mglP8aKnobFixture "mobilegl-tcp-p8a-${mglP8aLaneLower}")
set(mglP8aKnobState "${CMAKE_CURRENT_BINARY_DIR}/tcp-server-p8a-${mglP8aLaneLower}.json")
set(mglP8aKnob "${MGL_P8A_KNOB_ENV_${mglP8aLane}}")
add_test(NAME TcpServer.Start.P8a${mglP8aLane} COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
start --server "${MGL_ITEST_SPLIT_SERVER_PATH}" --endpoint "${mglP8aKnobEndpoint}"
--state "${mglP8aKnobState}")
add_test(NAME TcpServer.Stop.P8a${mglP8aLane} COMMAND "${Python3_EXECUTABLE}" "${MGL_TCP_FIXTURE}"
stop --state "${mglP8aKnobState}")
# The texture-view opt-in as on the shared fixture, so this supervisor differs from it in
# the knob alone.
set_tests_properties(TcpServer.Start.P8a${mglP8aLane} PROPERTIES
FIXTURES_SETUP ${mglP8aKnobFixture} TIMEOUT 20
ENVIRONMENT "MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1;${mglP8aKnob}")
set_tests_properties(TcpServer.Stop.P8a${mglP8aLane} PROPERTIES
FIXTURES_CLEANUP ${mglP8aKnobFixture} TIMEOUT 20)
foreach(mglP8aArm Split Spawn Tcp)
string(TOLOWER "${mglP8aArm}" mglP8aArmLower)
string(TOUPPER "${mglP8aArm}" mglP8aArmUpper)
set(mglP8aEnvironment "${MGL_ITEST_GLES_${mglP8aArmUpper}_ENVIRONMENT}")
set(mglP8aFilter "${MGL_P8A_KNOB_FILTER_${mglP8aLane}}")
set(mglP8aProperties)
if(mglP8aArm STREQUAL "Tcp")
string(REPLACE "MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT}"
"MOBILEGL_IPC_CONTROL=${mglP8aKnobEndpoint}"
mglP8aEnvironment "${mglP8aEnvironment}")
if(mglP8aEnvironment STREQUAL MGL_ITEST_GLES_TCP_ENVIRONMENT)
message(FATAL_ERROR "P8-A: the Tcp arm environment names no "
"MOBILEGL_IPC_CONTROL=${MOBILEGL_ITEST_TCP_ENDPOINT} to retarget")
endif()
set(mglP8aProperties FIXTURES_REQUIRED ${mglP8aKnobFixture} RESOURCE_LOCK mobilegl-tcp)
if(DEFINED MGL_P8A_KNOB_TCP_FILTER_${mglP8aLane})
set(mglP8aFilter "${MGL_P8A_KNOB_TCP_FILTER_${mglP8aLane}}")
endif()
endif()
gtest_discover_tests(MobileGLIntegrationTest
TEST_PREFIX "DirectGLES.${mglP8aArm}.${mglP8aLane}."
TEST_LIST "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}"
TEST_FILTER "${mglP8aFilter}"
DISCOVERY_TIMEOUT 30
PROPERTIES
LABELS "integration-gpu\;integration-${mglP8aArmLower}"
TIMEOUT ${MGL_ITEST_TIMEOUT}
ENVIRONMENT "${mglP8aEnvironment}\;${mglP8aKnob}"
${mglP8aProperties}
)
list(APPEND MGL_SPLIT_ARM_TEST_LISTS "MGL_SPLIT_ARM_TESTS_${MGL_SPLIT_ARM_INDEX}")
math(EXPR MGL_SPLIT_ARM_INDEX "${MGL_SPLIT_ARM_INDEX} + 1")
endforeach()
endforeach()
message(STATUS "Integration tests: P8-A registers ${mglP8aKnobIndex} server-knob lane(s) on "
"split/spawn/tcp, each tcp arm on its own supervisor")
@@ -0,0 +1,373 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/CopyImageStoreReadbackScenario.cpp
// Copyright (c) 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
//
// Scenario - glCopyImageSubData INTO A LEVEL THE DRIVER WILL NOT READ BACK (P8-E,
// docs/Disaggregated/notes/p8/E.md).
//
// The formats are the ones an ES driver keeps as texture-only storage: GL_RGB16 (EXT_texture_norm16
// makes the three-channel spelling sampleable, never colour-renderable) and the 16-bit SNORM family
// (renderable only with EXT_render_snorm, which Adreno does not have). Their glGetTexImage cannot go
// through a framebuffer on such a driver, so something other than the GPU image has to answer it:
// the CPU shadow on the monolith arm, the server's staged store on a split one. A copy writes the
// GPU image and nothing else, which is exactly the write such a byte store never sees.
//
// GL_RGB9_E5 is the positive control: a 32-bit packed format, read through the scratch GL_R32UI
// image whoever wrote the level, so it must stay green under every configuration below.
//
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp, every case, with no knob:
// on llvmpipe every one of these formats is attachable, so these runs pin the GPU route and the
// copy itself. Magma is the control backend - it reads the VkImage back whatever the format, so it
// has no store route to exercise.
//
// THE KNOB. MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h> is read BY THE ESPRYT SERVER
// (WireTextureReadback.inc, ReadTextureImageWire): a colour level of exactly that extent is
// treated as a level the driver refused to attach, which is what an Adreno does for every one of
// these formats and what llvmpipe can be made to do no other way. The `DirectGLES.<arm>.StoreRead.`
// entries set it, and this scenario then builds its textures at THAT extent instead of the default
// one - so the knob is scoped by the extent, and the tcp lane's shared supervisor can carry it
// (the TcpServer.Start fixture's ENVIRONMENT) without touching any other entry, this scenario's
// own knob-free tcp entries included. Under the knob every read must still be right, and the
// server log must name the store route - otherwise the entry would be pinning the GPU route a
// second time under a name that claims otherwise.
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr const char* kRefuseExtentKnob = "MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT";
// What the Espryt server logs (once per process) when it answers a read from its store.
constexpr const char* kStoreReadMarker = "texture-readback-from-store";
// The knob-free extent. Deliberately not the extent the knob entries use, so the tcp
// supervisor - which carries the knob for the whole lane - leaves these textures alone.
constexpr int kDefaultWidth = 12;
constexpr int kDefaultHeight = 6;
struct CopyFormat {
GLenum internalFormat;
GLenum format;
GLenum type;
int components; // 16-bit components per texel; 0 for the one 32-bit packed format
bool isSigned;
const char* name;
};
const CopyFormat kFormats[] = {
{GL_RGB16, GL_RGB, GL_UNSIGNED_SHORT, 3, false, "rgb16"},
{GL_RGBA16_SNORM, GL_RGBA, GL_SHORT, 4, true, "rgba16_snorm"},
{GL_RGB16_SNORM, GL_RGB, GL_SHORT, 3, true, "rgb16_snorm"},
{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT, 4, false, "rgba16"},
{GL_RGB9_E5, GL_RGB, GL_UNSIGNED_INT_5_9_9_9_REV, 0, false, "rgb9_e5"},
};
int TexelBytes(const CopyFormat& format) { return format.components == 0 ? 4 : format.components * 2; }
// Every word differs from its neighbours and from the other patterns, so a mis-addressed
// row, a swapped endpoint or a copy that never happened cannot cancel out. SNORM never
// gets -32768: it and -32767 are the same value (-1.0), and a read that goes through a
// normalized float is entitled to answer either.
std::vector<std::uint8_t> MakeTexels(const CopyFormat& format, int width, int height, std::uint32_t seed) {
const std::size_t texels = static_cast<std::size_t>(width) * static_cast<std::size_t>(height);
std::vector<std::uint8_t> bytes(texels * static_cast<std::size_t>(TexelBytes(format)));
if (format.components == 0) {
for (std::size_t i = 0; i < texels; ++i) {
std::uint32_t word = (static_cast<std::uint32_t>(i) + 1u) * 2654435761u ^ seed * 40503u;
word ^= word >> 13;
std::memcpy(bytes.data() + i * 4, &word, 4);
}
return bytes;
}
const std::size_t words = texels * static_cast<std::size_t>(format.components);
for (std::size_t i = 0; i < words; ++i) {
std::uint32_t mix = (static_cast<std::uint32_t>(i) + 1u) * 2654435761u ^ seed * 40503u;
mix ^= mix >> 15;
std::uint16_t value = static_cast<std::uint16_t>(mix >> 8);
if (format.isSigned && value == 0x8000u) value = 0x8001u;
std::memcpy(bytes.data() + i * 2, &value, 2);
}
return bytes;
}
// `image` with the w x h block at (dx, dy) replaced by the block of `source` at (sx, sy).
std::vector<std::uint8_t> WithBlock(std::vector<std::uint8_t> image, int width, const std::vector<std::uint8_t>& source,
int sourceWidth, int sx, int sy, int dx, int dy, int w, int h,
int texelBytes) {
for (int row = 0; row < h; ++row) {
std::memcpy(image.data() + (static_cast<std::size_t>(dy + row) * width + dx) * texelBytes,
source.data() + (static_cast<std::size_t>(sy + row) * sourceWidth + sx) * texelBytes,
static_cast<std::size_t>(w) * texelBytes);
}
return image;
}
class CopyImageStoreReadbackScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_wire = SplitRuntimeSkipReason().empty();
m_espryt = Gl().BackendName() == "DirectGLES";
if (const char* knob = std::getenv(kRefuseExtentKnob)) {
int width = 0, height = 0;
ASSERT_EQ(std::sscanf(knob, "%dx%d", &width, &height), 2)
<< kRefuseExtentKnob << "=" << knob << " is not <w>x<h>";
ASSERT_GE(width, 12) << "the copy windows below need at least a 12x6 level";
ASSERT_GE(height, 6) << "the copy windows below need at least a 12x6 level";
m_width = width;
m_height = height;
m_forced = true;
}
// 16-bit RGB rows are 6 bytes a texel: the default 4-byte alignment would pad every
// row of an odd-width level and shear both the upload and the read.
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
ASSERT_EQ(FirstGLError(), 0u);
}
void TearDown() override {
if (!Ready()) return;
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glBindTexture(GL_TEXTURE_2D, 0);
if (!m_textures.empty()) {
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
}
if (m_renderbuffer != 0) glDeleteRenderbuffers(1, &m_renderbuffer);
if (m_fbo != 0) {
BindDefaultFramebuffer();
glDeleteFramebuffers(1, &m_fbo);
}
FirstGLError();
}
GLuint MakeTexture(const CopyFormat& format, const std::vector<std::uint8_t>& texels, bool immutable) {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
if (immutable) {
glTexStorage2D(GL_TEXTURE_2D, 1, format.internalFormat, m_width, m_height);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_width, m_height, format.format, format.type,
texels.data());
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(format.internalFormat), m_width, m_height, 0,
format.format, format.type, texels.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
std::vector<std::uint8_t> ReadTexImage(GLuint texture, const CopyFormat& format) {
std::vector<std::uint8_t> bytes(
static_cast<std::size_t>(m_width) * m_height * static_cast<std::size_t>(TexelBytes(format)), 0xA5);
glBindTexture(GL_TEXTURE_2D, texture);
glGetTexImage(GL_TEXTURE_2D, 0, format.format, format.type, bytes.data());
glBindTexture(GL_TEXTURE_2D, 0);
return bytes;
}
// The first texel that differs names the defect (which half of the copy, which word);
// the rest would only bury it.
void ExpectImage(const std::vector<std::uint8_t>& got, const std::vector<std::uint8_t>& want,
const CopyFormat& format, const std::string& what) {
ASSERT_EQ(got.size(), want.size());
const int texelBytes = TexelBytes(format);
for (std::size_t at = 0; at < got.size(); ++at) {
if (got[at] == want[at]) continue;
const std::size_t texel = at / static_cast<std::size_t>(texelBytes);
const int x = static_cast<int>(texel % static_cast<std::size_t>(m_width));
const int y = static_cast<int>(texel / static_cast<std::size_t>(m_width));
std::string gotHex, wantHex;
char buffer[4];
for (int b = 0; b < texelBytes; ++b) {
std::snprintf(buffer, sizeof(buffer), "%02x", got[texel * texelBytes + b]);
gotHex += buffer;
std::snprintf(buffer, sizeof(buffer), "%02x", want[texel * texelBytes + b]);
wantHex += buffer;
}
ADD_FAILURE() << what << " (" << format.name << ", " << Gl().BackendName() << ", "
<< (m_wire ? "split" : "monolith") << (m_forced ? ", store read forced" : "")
<< "): texel (" << x << ", " << y << ") reads " << gotHex << ", want " << wantHex;
return;
}
}
// Under the knob the Espryt server must have answered from its store at least once;
// otherwise the entry proves nothing about that route.
void ExpectStoreReadNamedSince(const PipeStatsWindow::LogMark& mark) {
if (!m_forced || !m_espryt || !m_wire) return;
const std::string log = PipeStatsWindow::ReadServerLogSince(mark);
EXPECT_NE(log.find(kStoreReadMarker), std::string::npos)
<< kRefuseExtentKnob << " is set but the server log carries no '" << kStoreReadMarker
<< "' line: the knob did not reach the server, or no read took the store route";
}
// The copy window: 5x3 texels from (1, 1) of the source to (6, 2) of the destination,
// inside a 12x6 level and away from the partial upload's (0, 0) 3x2 corner.
static constexpr int kSrcX = 1, kSrcY = 1, kDstX = 6, kDstY = 2, kCopyW = 5, kCopyH = 3;
void CopyWindow(GLuint source, GLuint destination) {
glCopyImageSubData(source, GL_TEXTURE_2D, 0, kSrcX, kSrcY, 0, destination, GL_TEXTURE_2D, 0, kDstX,
kDstY, 0, kCopyW, kCopyH, 1);
}
bool m_wire = false;
bool m_espryt = false;
bool m_forced = false;
int m_width = kDefaultWidth;
int m_height = kDefaultHeight;
std::vector<GLuint> m_textures;
GLuint m_renderbuffer = 0;
GLuint m_fbo = 0;
};
// The control: an uploaded level reads back as uploaded.
TEST_F(CopyImageStoreReadbackScenario, AnUploadedLevelReadsBackExactly) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto texels = MakeTexels(format, m_width, m_height, 11);
const GLuint texture = MakeTexture(format, texels, /*immutable=*/false);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
ExpectImage(ReadTexImage(texture, format), texels, format, "the uploaded level");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// THE CASE THE PACKAGE IS ABOUT: the copy lands in the destination's GPU image only, and the
// read has to see it - whatever answers the read.
TEST_F(CopyImageStoreReadbackScenario, ACopiedWindowReadsBackOverTheUpload) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto sourceTexels = MakeTexels(format, m_width, m_height, 23);
const auto destinationTexels = MakeTexels(format, m_width, m_height, 37);
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/false);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
CopyWindow(source, destination);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glCopyImageSubData raised a GL error";
const auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX,
kDstY, kCopyW, kCopyH, TexelBytes(format));
ExpectImage(ReadTexImage(destination, format), want, format, "the copy destination");
ExpectImage(ReadTexImage(source, format), sourceTexels, format, "the copy source");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// The same into immutable storage (glTexStorage2D, then the whole level uploaded).
TEST_F(CopyImageStoreReadbackScenario, ACopiedWindowReadsBackFromAnImmutableLevel) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto sourceTexels = MakeTexels(format, m_width, m_height, 41);
const auto destinationTexels = MakeTexels(format, m_width, m_height, 43);
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/true);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/true);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
CopyWindow(source, destination);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glCopyImageSubData raised a GL error";
const auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX,
kDstY, kCopyW, kCopyH, TexelBytes(format));
ExpectImage(ReadTexImage(destination, format), want, format, "the immutable copy destination");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// A copy, then an ordinary partial upload into the same level elsewhere. The upload is the
// client's; the copied window is not, and a byte store that took the upload as "the level"
// would lose it again.
TEST_F(CopyImageStoreReadbackScenario, APartialUploadAfterTheCopyKeepsTheCopiedWindow) {
if (!Ready()) return;
const auto mark = PipeStatsWindow::MarkLaneLog();
for (const CopyFormat& format : kFormats) {
const auto sourceTexels = MakeTexels(format, m_width, m_height, 53);
const auto destinationTexels = MakeTexels(format, m_width, m_height, 59);
const auto patch = MakeTexels(format, 3, 2, 61);
const GLuint source = MakeTexture(format, sourceTexels, /*immutable=*/false);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": setup raised a GL error";
CopyWindow(source, destination);
glBindTexture(GL_TEXTURE_2D, destination);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 3, 2, format.format, format.type, patch.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": the copy or the upload raised a GL error";
auto want = WithBlock(destinationTexels, m_width, sourceTexels, m_width, kSrcX, kSrcY, kDstX, kDstY,
kCopyW, kCopyH, TexelBytes(format));
want = WithBlock(want, m_width, patch, 3, 0, 0, 0, 0, 3, 2, TexelBytes(format));
ExpectImage(ReadTexImage(destination, format), want, format, "the copy destination after the upload");
ASSERT_EQ(FirstGLError(), 0u) << format.name << ": glGetTexImage raised a GL error";
}
ExpectStoreReadNamedSince(mark);
}
// A RENDERBUFFER endpoint. The split arm resolves it by handle (DirectGLES.cpp,
// MakeGLESCopyImageEndpoint), which is what retired the "CopyImageSubData+RENDERBUFFER"
// latch; this is the case that says so on every arm. It has no store to answer from - a
// renderbuffer's texels were never uploaded - so the knob entries do not run it.
TEST_F(CopyImageStoreReadbackScenario, ARenderbufferSourceLandsInTheTexture) {
if (!Ready()) return;
const CopyFormat format{GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT, 4, false, "rgba16"};
const auto destinationTexels = MakeTexels(format, m_width, m_height, 71);
const GLuint destination = MakeTexture(format, destinationTexels, /*immutable=*/false);
glGenRenderbuffers(1, &m_renderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, m_renderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA16, m_width, m_height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, m_renderbuffer);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE))
<< "the RGBA16 renderbuffer is not attachable";
// k / 65535 for four 16-bit words: exact through a float clear and a UNORM16 store.
const std::uint16_t words[4] = {0x1234, 0x5678, 0x9abc, 0xffff};
const GLfloat color[4] = {words[0] / 65535.0f, words[1] / 65535.0f, words[2] / 65535.0f,
words[3] / 65535.0f};
glDisable(GL_SCISSOR_TEST);
glClearBufferfv(GL_COLOR, 0, color);
BindDefaultFramebuffer();
ASSERT_EQ(FirstGLError(), 0u) << "setup raised a GL error";
glCopyImageSubData(m_renderbuffer, GL_RENDERBUFFER, 0, kSrcX, kSrcY, 0, destination, GL_TEXTURE_2D, 0,
kDstX, kDstY, 0, kCopyW, kCopyH, 1);
ASSERT_EQ(FirstGLError(), 0u) << "glCopyImageSubData from the renderbuffer raised a GL error";
std::vector<std::uint8_t> cleared(static_cast<std::size_t>(m_width) * m_height * 8);
for (std::size_t texel = 0; texel < cleared.size() / 8; ++texel) {
std::memcpy(cleared.data() + texel * 8, words, 8);
}
const auto want = WithBlock(destinationTexels, m_width, cleared, m_width, kSrcX, kSrcY, kDstX, kDstY,
kCopyW, kCopyH, 8);
ExpectImage(ReadTexImage(destination, format), want, format, "the texture a renderbuffer was copied into");
ASSERT_EQ(FirstGLError(), 0u) << "glGetTexImage raised a GL error";
}
} // namespace
} // namespace MGITest
@@ -257,7 +257,7 @@ namespace {
// RESIDENT, residency comes only from AdoptPersistentMap, and every persistent-map
// acquisition declines under split by R-6 (PipeApply.cpp's MGPipeApplyMapPersistent:
// "A SPLIT BUILD RUNS AT TIER T2 AND DECLINES EVERY ACQUISITION, ALWAYS"; T0/T1 are
// P11's and AdoptTierIsEmulate is a named Fatal for them). So the capability is wired
// unimplemented, refused by name at the handshake since P11 A1). So the capability is wired
// AHEAD of its consumer on purpose, and `rsd=` on the PipeStats line is the counter that
// will show the records the day P11 lands a tier that adopts. See notes/p7/magma-c.md.
TEST_F(CtWireScenario, TheServerPublishesTheResidentSubDataCapabilityFromItsOwnTable) {
@@ -0,0 +1,369 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DeletedBoundBufferScenario.cpp
// Copyright (c) 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
//
// Scenario - A BUFFER DELETED WHILE IT IS STILL BOUND TO AN INDEXED BINDING POINT.
//
// glDeleteBuffers unbinds the buffer from every binding point of the current context (GL 4.6
// core 5.1.2, ES 3.2 6.1.1), indexed ones included, and the frontend does (BufferState::
// MarkBufferObjectForDeletion). Under a transport that is not enough: the SERVER holds its own
// copy of each indexed window (set_shader_buffers / the uniform window), re-sent only when the
// target's bind-point GENERATION moves (Tracker.h's bits 15/16/17). A delete that cleared the
// points without moving the generation left the server naming the dead buffer's handle; once
// the client recycled that handle's slot at a newer generation, the next draw or dispatch that
// walked the window asked the backend for the old one:
// Espryt Fatal{ProtocolCorruption, "BackendSlotTable.Generation"} (the whole session dies);
// Magma the handle resolves to nothing and the draw / dispatch that declares the point is
// dropped.
// (P11 B2 addendum; first seen as LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack
// followed by TheArenaLandsInTheTierItsLaneDeclares in one process.)
//
// Each case, for one indexed target: bind a buffer at point 0 and use it (so the window naming it
// reaches the server), delete it WHILE BOUND, create buffers until its slot has been reused at a
// newer generation, then draw and dispatch with a program that DECLARES point 0 but never
// touches it (a uniform-guarded branch: reading an empty point would be undefined, declaring it
// is not). Both must run - no Fatal, a marker written, the frame drawn - and a fresh buffer bound
// at the same point afterwards must be read correctly.
#include <array>
#include <initializer_list>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
// More than enough for any free-list order: the dead buffer's slot is reused well before.
constexpr int kRecycledBuffers = 16;
constexpr unsigned int kMarker = 0xC0FFEEu;
constexpr unsigned int kFreshWord = 0x1234u;
constexpr GLuint kResultBinding = 7;
constexpr const char* kQuadVertexSource = R"(#version 430 core
layout(location = 0) in vec2 a_pos;
void main() { gl_Position = vec4(a_pos, 0.0, 1.0); }
)";
constexpr const char* kQuadFragmentSource = R"(#version 430 core
out vec4 o_color;
void main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }
)";
// The target's declaration and the one read of it; the read is only ever taken with
// u_read = 1 (a fresh buffer bound). Result: the marker, unconditional, and what was read.
std::string ComputeSource(const char* declaration, const char* read) {
std::ostringstream os;
os << "#version 430 core\n"
"layout(local_size_x = 1) in;\n"
"layout(std430, binding = 7) buffer Result { uint marker; uint value; };\n"
"uniform uint u_read;\n"
<< declaration << "\n"
<< "void main() {\n"
" uint v = 0u;\n"
" if (u_read != 0u) { v = " << read << "; }\n"
" marker = 0xC0FFEEu;\n"
" value = v;\n"
"}\n";
return os.str();
}
constexpr const char* kCaptureVertexSource = R"(#version 430 core
layout(location = 0) in vec2 a_pos;
out float vs_value;
void main() {
vs_value = float(gl_VertexID) * 2.0 + 1.0;
gl_Position = vec4(a_pos, 0.0, 1.0);
gl_PointSize = 1.0;
}
)";
class DeletedBoundBufferScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_quad = Link(kQuadVertexSource, kQuadFragmentSource);
ASSERT_NE(m_quad, 0u) << m_log;
const float quad[] = {-1.f, -1.f, 1.f, -1.f, 1.f, 1.f, -1.f, -1.f, 1.f, 1.f, -1.f, 1.f};
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
glEnableVertexAttribArray(0);
// The result block lives at its own point, bound ONCE here: nothing later rebinds
// a storage point, so the storage window the server holds is only ever re-sent
// by what the case itself does.
const unsigned int zero[2] = {0u, 0u};
glGenBuffers(1, &m_result);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(zero), zero, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kResultBinding, m_result);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
if (!m_owned.empty()) glDeleteBuffers(static_cast<GLsizei>(m_owned.size()), m_owned.data());
if (m_result != 0) glDeleteBuffers(1, &m_result);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
for (GLuint p : {m_quad, m_compute, m_capture}) {
if (p != 0) glDeleteProgram(p);
}
m_owned.clear();
m_result = m_vbo = m_vao = m_quad = m_compute = m_capture = 0;
}
GLuint Compile(GLenum stage, const char* source) {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
m_log = std::string("shader did not compile: ") + log;
glDeleteShader(shader);
return 0;
}
return shader;
}
GLuint LinkShaders(std::initializer_list<GLuint> shaders, const char* varying = nullptr) {
const GLuint program = glCreateProgram();
for (GLuint s : shaders) glAttachShader(program, s);
if (varying != nullptr) glTransformFeedbackVaryings(program, 1, &varying, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
for (GLuint s : shaders) glDeleteShader(s);
GLint ok = 0;
glGetProgramiv(program, GL_LINK_STATUS, &ok);
if (!ok) {
char log[2048] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
m_log = std::string("program did not link: ") + log;
glDeleteProgram(program);
return 0;
}
return program;
}
GLuint Link(const char* vs, const char* fs, const char* varying = nullptr) {
const GLuint v = Compile(GL_VERTEX_SHADER, vs);
if (v == 0) return 0;
const GLuint f = Compile(GL_FRAGMENT_SHADER, fs);
if (f == 0) {
glDeleteShader(v);
return 0;
}
return LinkShaders({v, f}, varying);
}
GLuint LinkCompute(const std::string& source) {
const GLuint c = Compile(GL_COMPUTE_SHADER, source.c_str());
return c == 0 ? 0 : LinkShaders({c});
}
GLuint MakeBuffer(GLenum target, const void* data, GLsizeiptr size) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(target, buffer);
glBufferData(target, size, data, GL_DYNAMIC_DRAW);
glBindBuffer(target, 0);
return buffer;
}
// Delete the buffer still bound at `target` point 0, then take the dead buffer's slot
// back at a newer generation: every new buffer is defined with content, which is what
// makes the server twin it (and so move its live generation) at once.
void DeleteWhileBoundAndRecycle(GLuint& bound) {
glDeleteBuffers(1, &bound);
bound = 0;
std::vector<unsigned int> payload(64);
for (int i = 0; i < kRecycledBuffers; ++i) {
for (auto& word : payload) word = static_cast<unsigned int>(i);
m_owned.push_back(MakeBuffer(GL_ARRAY_BUFFER, payload.data(),
static_cast<GLsizeiptr>(payload.size() * sizeof(unsigned int))));
}
}
// The draw path first (Espryt walks the whole storage window at every draw), then the
// frame must show the quad.
void DrawTheQuadAndExpectIt(const char* when) {
glViewport(0, 0, Gl().Width(), Gl().Height());
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_quad);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 6);
unsigned char px[4] = {0, 0, 0, 0};
glReadPixels(Gl().Width() / 2, Gl().Height() / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, px);
EXPECT_GT(px[1], 200) << "the quad was not drawn " << when;
EXPECT_LT(px[0], 50) << when;
}
// Dispatch the target's program once; returns {marker, value}.
std::array<unsigned int, 2> Dispatch(unsigned int read) {
const unsigned int zero[2] = {0u, 0u};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(zero), zero);
glUseProgram(m_compute);
glUniform1ui(glGetUniformLocation(m_compute, "u_read"), read);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT |
GL_ATOMIC_COUNTER_BARRIER_BIT);
std::array<unsigned int, 2> out = {0xDEADBEEFu, 0xDEADBEEFu};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(out), out.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
return out;
}
// The three shader-buffer targets share one shape.
void RunShaderBufferTarget(GLenum target, const char* declaration, const char* read,
unsigned int freshSeed, unsigned int expectedRead) {
m_compute = LinkCompute(ComputeSource(declaration, read));
ASSERT_NE(m_compute, 0u) << m_log;
// 1. A buffer at point 0, used once so the window naming it crosses.
const unsigned int seed[4] = {freshSeed, 0u, 0u, 0u};
GLuint doomed = MakeBuffer(target, seed, sizeof(seed));
glBindBufferBase(target, 0, doomed);
auto first = Dispatch(0);
EXPECT_EQ(first[0], kMarker) << "the dispatch with the buffer bound did not run";
ASSERT_EQ(FirstGLError(), 0u);
// 2. Deleted while bound; its slot recycled at a newer generation.
DeleteWhileBoundAndRecycle(doomed);
GLint stillBound = -1;
glGetIntegeri_v(target == GL_SHADER_STORAGE_BUFFER ? GL_SHADER_STORAGE_BUFFER_BINDING
: target == GL_UNIFORM_BUFFER ? GL_UNIFORM_BUFFER_BINDING
: GL_ATOMIC_COUNTER_BUFFER_BINDING,
0, &stillBound);
EXPECT_EQ(stillBound, 0) << "glDeleteBuffers did not unbind indexed point 0";
// 3. The SAME program dispatched again - no glUseProgram of another program in
// between, because the uniform window's shutter mixes the program identity in and a
// program change would re-send the window anyway - then a draw. Both must run.
auto after = Dispatch(0);
EXPECT_EQ(after[0], kMarker)
<< "the dispatch that declares the deleted buffer's point did not run: its window still named "
"the dead handle (Magma drops the dispatch; Espryt would have died with "
"Fatal{ProtocolCorruption, \"BackendSlotTable.Generation\"} before this line)";
DrawTheQuadAndExpectIt("after the bound buffer was deleted and its slot reused");
EXPECT_EQ(FirstGLError(), 0u);
// 4. A fresh buffer at the same point reads correctly.
const unsigned int fresh[4] = {freshSeed, 0u, 0u, 0u};
const GLuint replacement = MakeBuffer(target, fresh, sizeof(fresh));
m_owned.push_back(replacement);
glBindBufferBase(target, 0, replacement);
auto read1 = Dispatch(1);
EXPECT_EQ(read1[0], kMarker);
EXPECT_EQ(read1[1], expectedRead) << "the buffer bound after the delete was not the one read";
EXPECT_EQ(FirstGLError(), 0u);
glBindBufferBase(target, 0, 0);
}
GLuint m_quad = 0;
GLuint m_compute = 0;
GLuint m_capture = 0;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_result = 0;
std::vector<GLuint> m_owned;
std::string m_log;
};
} // namespace
TEST_F(DeletedBoundBufferScenario, AStorageBufferDeletedWhileBoundLeavesNoStaleBinding) {
if (!Ready() || IsSkipped()) return;
RunShaderBufferTarget(GL_SHADER_STORAGE_BUFFER,
"layout(std430, binding = 0) buffer Target { uint word; };", "word",
kFreshWord, kFreshWord);
}
TEST_F(DeletedBoundBufferScenario, AUniformBufferDeletedWhileBoundLeavesNoStaleBinding) {
if (!Ready() || IsSkipped()) return;
RunShaderBufferTarget(GL_UNIFORM_BUFFER, "layout(std140, binding = 0) uniform Target { uint word; };",
"word", kFreshWord, kFreshWord);
}
// The counter's pre-increment value is what the shader reads, and the buffer then holds seed + 1.
TEST_F(DeletedBoundBufferScenario, AnAtomicCounterBufferDeletedWhileBoundLeavesNoStaleBinding) {
if (!Ready() || IsSkipped()) return;
GLint counters = 0;
glGetIntegerv(GL_MAX_COMPUTE_ATOMIC_COUNTERS, &counters);
if (counters < 1) GTEST_SKIP() << "GL_MAX_COMPUTE_ATOMIC_COUNTERS is " << counters;
RunShaderBufferTarget(GL_ATOMIC_COUNTER_BUFFER, "layout(binding = 0, offset = 0) uniform atomic_uint counter;",
"atomicCounterIncrement(counter)", 41u, 41u);
}
// Transform feedback: nothing re-sends a capture window at a draw today (bit 17 is computed and
// not emitted), so the stale-handle failure cannot reach the backend through this target; the
// case pins the behaviour anyway - the delete unbinds, a draw after the slot is reused runs,
// and a fresh capture buffer at the same point receives exactly the captured values.
TEST_F(DeletedBoundBufferScenario, ATransformFeedbackBufferDeletedWhileBoundLeavesNoStaleBinding) {
if (!Ready() || IsSkipped()) return;
m_capture = Link(kCaptureVertexSource, kQuadFragmentSource, "vs_value");
ASSERT_NE(m_capture, 0u) << m_log;
constexpr int kPoints = 4;
const float poison[kPoints + 2] = {-7.f, -7.f, -7.f, -7.f, -7.f, -7.f};
auto capture = [&](GLuint into) {
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, into);
glUseProgram(m_capture);
glBindVertexArray(m_vao);
glEnable(GL_RASTERIZER_DISCARD);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, kPoints);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
};
GLuint doomed = MakeBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, poison, sizeof(poison));
capture(doomed);
ASSERT_EQ(FirstGLError(), 0u);
DeleteWhileBoundAndRecycle(doomed);
GLint stillBound = -1;
glGetIntegeri_v(GL_TRANSFORM_FEEDBACK_BUFFER_BINDING, 0, &stillBound);
EXPECT_EQ(stillBound, 0) << "glDeleteBuffers did not unbind transform feedback point 0";
DrawTheQuadAndExpectIt("after the bound capture buffer was deleted and its slot reused");
EXPECT_EQ(FirstGLError(), 0u);
const GLuint fresh = MakeBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, poison, sizeof(poison));
m_owned.push_back(fresh);
capture(fresh);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
float captured[kPoints + 2] = {};
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, fresh);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, sizeof(captured), captured);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
EXPECT_EQ(FirstGLError(), 0u);
for (int i = 0; i < kPoints + 2; ++i) {
const float expected = i < kPoints ? static_cast<float>(i) * 2.f + 1.f : -7.f;
EXPECT_EQ(captured[i], expected) << "capture record " << i;
}
}
} // namespace MGITest
@@ -0,0 +1,378 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DescriptorPoolGrowthScenario.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
//
// Scenario - ONE FRAME THAT NEEDS THOUSANDS OF DISTINCT DESCRIPTOR SETS.
//
// DirectVulkan takes a draw's descriptor set from a per-frame-slot cache and adds a pool to the
// slot when the cache runs dry (UniformManager). Two things went wrong with a burst like
// Minecraft rd12's loading frame (15,016 sets in one slot):
// - Growth was linear. The next pool was sized at twice the slot's FIRST pool of the flavour,
// which is always its oldest and smallest, so every grown pool came out the same size: the
// loading slot ended with 59 pools of 256 sets.
// - The burst was kept forever. The cache only rewinds, so the slot held all 15k sets (and the
// pools behind them) for the life of the process, while steady frames used a few dozen.
// Now each pool doubles the slot's largest one (capped), and a slot whose pools stay under a
// quarter full for MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES frames is handed back whole and re-grows.
//
// Each of kDraws draws paints its own pixel of a 64x64 target and samples one of kTextures
// textures, a different one from each of the previous draws' so neither the renderer's per-draw
// descriptor reuse memo (four entries) nor its dynamic-offset-only rebind can let two draws share
// a set: one frame needs kDraws sets. Every pixel must hold its own draw's texture colour, on both
// backends. On DirectVulkan the cases also read the pools through GetDescriptorPoolCensus - which
// only the desktop build can reach, since the integration harness links the static library there
// and the hidden-visibility shipping library on Android.
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#if !defined(__ANDROID__)
#include <MG_Backend/DirectVulkan/DescriptorPoolCensus.h>
#endif
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kSide = 64;
constexpr int kDraws = kSide * kSide; // well under the 16384-draw command-buffer split budget
// More than the descriptor reuse memo's four entries, so the texture a draw samples is never
// one of the last four draws'.
constexpr int kTextures = 8;
// The base pool size (VulkanRenderer's kDescriptorSetsPerFrame) and the growth cap
// (UniformManager::kMaxDescriptorPoolSets).
constexpr std::uint64_t kBaseSets = 64;
constexpr std::uint64_t kMaxPoolSets = 4096;
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
uniform int uIndex;
void main() {
vec2 cell = vec2(float(uIndex % 64), float(uIndex / 64));
gl_Position = vec4((cell + aPos) / 32.0 - 1.0, 0.0, 1.0);
}
)";
constexpr const char* kFS = R"(#version 330 core
uniform sampler2D uTex;
out vec4 o_color;
void main() { o_color = texture(uTex, vec2(0.5)); }
)";
// Texture t's single texel; every channel differs between any two textures.
Rgba8 TextureColor(int t) {
return Rgba8{static_cast<std::uint8_t>(16 + 32 * t), static_cast<std::uint8_t>(240 - 32 * t),
static_cast<std::uint8_t>(8 + 29 * t), 255};
}
std::string Describe(const Rgba8& c) {
std::ostringstream os;
os << "rgba(" << static_cast<int>(c.r) << ", " << static_cast<int>(c.g) << ", " << static_cast<int>(c.b)
<< ", " << static_cast<int>(c.a) << ")";
return os.str();
}
unsigned long EnvUnsigned(const char* name, unsigned long fallback) {
const char* value = std::getenv(name);
if (value == nullptr || *value == '\0') return fallback;
char* end = nullptr;
const unsigned long parsed = std::strtoul(value, &end, 10);
return (end != nullptr && *end == '\0') ? parsed : fallback;
}
// P11 M2: HOW MANY OF THE BURST'S SETS ONE FRAME SLOT HOLDS AT ONCE. All kDraws of them on
// the monolith arm. Under a split transport the server draws through the WIRE arm, which
// rewinds a slot's set cursors every UniformManager::kWireDescriptorSetBudget (2048) sets once
// it has proved the GPU done with them (VulkanRenderer::RewindWireDescriptorSetsIfDue), so its
// cache holds the burst 2048 sets at a time: the burst still needs a set per draw, grows the
// slot geometrically to that and is given back by the same trim, only the census counts the
// sets held rather than the draws. (The census itself is in-process - inproc it is read on the
// server's apply thread; spawn and tcp answer unavailable - see GetDescriptorPoolCensus.)
constexpr std::uint64_t kWireDescriptorSetBudget = 2048;
std::uint64_t BurstSetsHeldAtOnce() {
const std::uint64_t draws = static_cast<std::uint64_t>(kDraws);
return PeekSplitRuntime().transportResolved ? std::min(draws, kWireDescriptorSetBudget) : draws;
}
// Pools a geometric policy adds for n sets from an empty flavour: 64, 128, ... up to the cap,
// then one per cap. The old first-pool rule needed n/128 or n/256 of them.
std::uint64_t GeometricPoolBound(std::uint64_t n) {
std::uint64_t pools = 0, capacity = 0, size = kBaseSets;
while (capacity < n) {
capacity += size;
++pools;
size = std::min(size * 2, kMaxPoolSets);
}
return pools;
}
struct Census {
bool available = false;
std::uint64_t pools = 0;
std::uint64_t capacitySets = 0;
std::uint64_t cachedSets = 0;
std::uint64_t slotTrims = 0;
};
Census ReadCensus() {
Census out;
#if !defined(__ANDROID__)
// P11 M2: THE POOLS ARE THE SERVER RENDERER'S under a split transport, and under
// run-ahead a swap - or a draw - returns here before the server has applied it: a census
// taken right after the burst's swap read the pools as they were before the burst
// (cachedSets=0, measured under load). So first the harness's applied-seq fence (the
// TriangleScenario / MagmaWireReclaimScenario shape): every record this client emitted
// is applied. Inproc only - the census is in-process, spawn and tcp answer unavailable
// - and the read itself then runs on the apply thread (GetDescriptorPoolCensus).
// The fence's default 5 s is not enough here: the burst is 4096 draws with a queue-idle
// rewind in the middle, and on a loaded host lavapipe applies it in more than that
// (measured: 21 of 40 runs past 5 s beside the retrace lane).
if (const SplitRuntimeState runtime = PeekSplitRuntime();
runtime.transportResolved && runtime.transportName == "inproc") {
EXPECT_TRUE(WaitForSplitAppliedForTesting(runtime.emitSeq, 120000))
<< "the server never applied record " << runtime.emitSeq << " before the census";
}
const auto census = MobileGL::MG_Backend::DirectVulkan::GetDescriptorPoolCensus();
out.available = census.available;
out.pools = census.pools;
out.capacitySets = census.capacitySets;
out.cachedSets = census.cachedSets;
out.slotTrims = census.slotTrims;
#endif
return out;
}
std::string Describe(const Census& c) {
std::ostringstream os;
os << "pools=" << c.pools << " capacity=" << c.capacitySets << " cachedSets=" << c.cachedSets
<< " slotTrims=" << c.slotTrims;
return os.str();
}
::testing::AssertionResult EveryPixelHoldsItsDrawsTexture(const Image& image, const char* when) {
int wrong = 0;
int firstX = -1, firstY = -1;
Rgba8 firstSeen{};
for (int y = 0; y < kSide; ++y) {
for (int x = 0; x < kSide; ++x) {
const Rgba8 expected = TextureColor((y * kSide + x) % kTextures);
const Rgba8 seen = image.At(x, y);
if (seen != expected) {
if (wrong == 0) {
firstX = x;
firstY = y;
firstSeen = seen;
}
++wrong;
}
}
}
if (wrong == 0) return ::testing::AssertionSuccess();
const int draw = firstY * kSide + firstX;
return ::testing::AssertionFailure()
<< when << ": " << wrong << " of " << kDraws << " pixels do not hold their own draw's texture. "
<< "First: (" << firstX << ", " << firstY << ") = " << Describe(firstSeen) << ", drawn by draw "
<< draw << " with texture " << (draw % kTextures) << " = "
<< Describe(TextureColor(draw % kTextures)) << " (a pixel no draw reached holds 0,0,0,0)";
}
class DescriptorPoolGrowthScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
static const float kVertices[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f};
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
m_textures.resize(kTextures);
glGenTextures(kTextures, m_textures.data());
for (int t = 0; t < kTextures; ++t) {
const Rgba8 c = TextureColor(t);
const std::uint8_t texel[4] = {c.r, c.g, c.b, c.a};
glBindTexture(GL_TEXTURE_2D, m_textures[t]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
glBindTexture(GL_TEXTURE_2D, 0);
}
void TearDown() override {
if (Ready()) {
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
BindDefaultFramebuffer();
if (m_program != 0) glDeleteProgram(m_program);
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
DestroyColorFbo(m_target);
}
ScenarioTest::TearDown();
}
::testing::AssertionResult Prepare() {
std::string error;
m_program = CompileProgram(kVS, kFS, &error);
if (m_program == 0) return ::testing::AssertionFailure() << error;
m_indexLocation = glGetUniformLocation(m_program, "uIndex");
m_target = MakeColorFbo(kSide, kSide);
if (m_target.fbo == 0) return ::testing::AssertionFailure() << "MakeColorFbo failed";
glUseProgram(m_program);
glUniform1i(glGetUniformLocation(m_program, "uTex"), 0);
glActiveTexture(GL_TEXTURE0);
glBindVertexArray(m_vao);
BindFbo(m_target);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
return ::testing::AssertionSuccess();
}
// Draw i paints pixel (i % 64, i / 64) with texture i % kTextures.
void Draw(int i) {
glBindTexture(GL_TEXTURE_2D, m_textures[i % kTextures]);
glUniform1i(m_indexLocation, i);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
// The burst: every pixel, one draw and one descriptor set each.
void DrawBurst() {
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
for (int i = 0; i < kDraws; ++i) Draw(i);
}
static bool IsVulkan() { return Gl().BackendName() == std::string("DirectVulkan"); }
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_program = 0;
GLint m_indexLocation = -1;
std::vector<GLuint> m_textures;
ColorFbo m_target;
};
} // namespace
// One frame, kDraws distinct sets. Every pixel lands, and the frame slot grows O(log N) pools
// for it, not N / 128.
TEST_F(DescriptorPoolGrowthScenario, AFrameOfDistinctSetsLandsWholeAndGrowsItsPoolsGeometrically) {
if (!Ready()) return;
ASSERT_TRUE(Prepare());
const Census before = ReadCensus();
DrawBurst();
const Image image = ReadPixels(kSide, kSide);
const Census after = ReadCensus();
EXPECT_TRUE(EveryPixelHoldsItsDrawsTexture(image, "one frame of distinct descriptor sets"));
EXPECT_EQ(FirstGLError(), 0u);
RecordProperty("census_checked", (IsVulkan() && after.available) ? "yes" : "no");
if (!IsVulkan() || !after.available) return;
// Falsifiability first: had the renderer's memos collapsed the draws onto shared sets, the
// slot would never have needed to grow and the bound below would pass on any policy.
const std::uint64_t held = BurstSetsHeldAtOnce();
ASSERT_GE(after.cachedSets, before.cachedSets + held)
<< "the frame did not allocate one descriptor set per draw, so it does not exercise growth. Before: "
<< Describe(before) << "; after: " << Describe(after);
const std::uint64_t added = after.pools - before.pools;
RecordProperty("census_before", Describe(before));
RecordProperty("census_after", Describe(after));
EXPECT_LE(added, GeometricPoolBound(held + 1))
<< "a frame needing " << held << " descriptor sets at once added " << added << " pools; doubling the "
<< "slot's largest pool needs at most " << GeometricPoolBound(held + 1) << ". Before: " << Describe(before)
<< "; after: " << Describe(after);
}
// Burst, then quiet frames until the renderer hands the burst back, then the burst again while
// the quiet frames are still in flight. Nothing waits between frames except where noted, so the
// trim runs with other frame slots' work outstanding.
TEST_F(DescriptorPoolGrowthScenario, AGivenBackBurstLeavesNoStaleSetBehind) {
if (!Ready()) return;
ASSERT_TRUE(Prepare());
const unsigned long trimFrames = EnvUnsigned("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120);
const unsigned long framesInFlight = EnvUnsigned("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3);
// The trim fires once trimFrames quiet frames have followed the burst, at the burst slot's next
// turn; past this many frames it is not coming.
const unsigned long quietFrameLimit = trimFrames + 2 * framesInFlight + 16;
DrawBurst();
Gl().EndFrame();
const Census afterBurst = ReadCensus();
// Quiet frames: one draw each, the same pixel and texture the burst gave draw 0.
const bool observe = IsVulkan() && afterBurst.available && trimFrames != 0;
unsigned long quietFrames = 0;
Census afterQuiet = afterBurst;
for (; quietFrames < quietFrameLimit; ++quietFrames) {
Draw(0);
Gl().EndFrame();
if (observe) {
afterQuiet = ReadCensus();
if (afterQuiet.slotTrims > afterBurst.slotTrims) {
++quietFrames;
break;
}
}
}
// The burst again, straight away. The slot it lands in re-grows from its base pool.
DrawBurst();
const Image image = ReadPixels(kSide, kSide);
const Census afterSecondBurst = ReadCensus();
EXPECT_TRUE(EveryPixelHoldsItsDrawsTexture(image, "the burst drawn after the first one was given back"));
EXPECT_EQ(FirstGLError(), 0u);
RecordProperty("census_checked", observe ? "yes" : "no");
if (!observe) return;
RecordProperty("census_after_burst", Describe(afterBurst));
RecordProperty("quiet_frames", std::to_string(quietFrames));
RecordProperty("census_after_quiet", Describe(afterQuiet));
RecordProperty("census_after_second_burst", Describe(afterSecondBurst));
const std::uint64_t held = BurstSetsHeldAtOnce();
ASSERT_GE(afterBurst.cachedSets, held)
<< "the burst did not allocate one descriptor set per draw: " << Describe(afterBurst);
EXPECT_GT(afterQuiet.slotTrims, afterBurst.slotTrims)
<< quietFrames << " frames of one draw each followed a " << kDraws << "-set frame, but no frame slot was "
<< "trimmed (MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES=" << trimFrames << "). After the burst: "
<< Describe(afterBurst) << "; after the quiet frames: " << Describe(afterQuiet);
EXPECT_LE(afterQuiet.cachedSets + held / 2, afterBurst.cachedSets)
<< "the trim did not give the burst's sets back. After the burst: " << Describe(afterBurst)
<< "; after the quiet frames: " << Describe(afterQuiet);
EXPECT_LT(afterQuiet.pools, afterBurst.pools)
<< "the trim did not give the burst's pools back. After the burst: " << Describe(afterBurst)
<< "; after the quiet frames: " << Describe(afterQuiet);
EXPECT_GE(afterSecondBurst.cachedSets, afterQuiet.cachedSets + held)
<< "the second burst was not served by freshly allocated sets: " << Describe(afterSecondBurst);
}
} // namespace MGITest
@@ -3,6 +3,7 @@
#include "../Harness/SplitRuntimePeek.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/SplitLane.h"
#include "../Harness/WireDeclinePeek.h"
#include <array>
#include <algorithm>
#include <cstring>
@@ -659,6 +660,12 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) {
glDisable(GL_SCISSOR_TEST);
glClearDepth(0.5);
glClear(GL_DEPTH_BUFFER_BIT);
// P8-SV: the decline is a WireDeclines.def row now (MipmapDepthStencilAspect), so the server's
// MGWIRE-DECLINES dump sees it. Readable where the server is this process: the inproc arm.
// Red once (executed, reverted): the site back to a bare MGLOG_E_ONCE - the count stays 0.
unsigned long long declinesBefore = 0;
const bool counted = PeekSplitRuntime().transportName == "inproc" &&
PeekWireDeclineCount("MipmapDepthStencilAspect", &declinesBefore);
glGenerateMipmap(GL_TEXTURE_2D);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.DepthStencilMip.error";
// The session is what the retired Fatal used to take with it. A readback is a round trip
@@ -669,12 +676,77 @@ TEST_F(F1WireScenario, GenerateMipmapDepthStencilDeclinesAndKeepsTheSession) {
glReadPixels(1, 1, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &level0);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.DepthStencilMip.readback";
EXPECT_NEAR(level0, 0.5f, 0.002f) << "F1.DepthStencilMip.level0";
if (counted) {
unsigned long long declinesAfter = 0;
ASSERT_TRUE(PeekWireDeclineCount("MipmapDepthStencilAspect", &declinesAfter));
EXPECT_EQ(declinesAfter - declinesBefore, 1u) << "F1.DepthStencilMip.tally: one named decline per refused "
"glGenerateMipmap (WireDeclines.def MipmapDepthStencilAspect)";
}
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, 0, 0);
glDeleteTextures(1, &depthStencil);
glBindTexture(GL_TEXTURE_2D, texture);
glClearDepth(1.0);
}
// P8-F: `Magma:mipmap-shader-format-or-shape` IS A NAMED DECLINE ON THE WIRE ARM NOW, which is
// the monolith arm's answer for a chain it cannot build (VulkanRenderer.cpp's GenerateMipmap
// declines a format without blit support and leaves the chain unwritten). It used to end the
// session. A 3D texture is a shape neither wire shader arm takes (the colour pass samples a
// sampler2D), so with the native blit forced off (MGITEST_MAGMA_FORCE_SHADER_MIPMAP) it is what
// reaches the refusal on this host - lavapipe blits a 3D RGBA8 chain natively otherwise.
//
// What this case pins: no GL error, the session still answering (two readback round trips), level
// 0 untouched, and level 1 still holding the application's own texels - the decline wrote nothing.
// Magma only, and knob-gated, like GenerateMipmapWithoutNativeBlitPixels above.
// Red once (executed, reverted): restore the MagmaWireFatal and this case dies with
// Fatal{UnmigratedVerb, "Magma:mipmap-shader-format-or-shape ..."}.
TEST_F(F1WireScenario, GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession) {
if (!Ready()) return;
if (Gl().BackendName() != "DirectVulkan")
GTEST_SKIP() << "Magma wire-arm decline (P8-F, mipmap-shader-format-or-shape): the knob and "
"the shader mip arms it forces are Magma's; Espryt ("
<< Gl().BackendName() << ") generates this chain itself";
const std::string tier = SplitLane::MarkerValue("MGITEST_MAGMA_FORCE_SHADER_MIPMAP");
if (tier.empty())
GTEST_SKIP() << "MGITEST_MAGMA_FORCE_SHADER_MIPMAP is unset: this driver blits a 3D RGBA8 "
"chain natively, so the decline under test is not entered";
GLuint volume = 0;
glGenTextures(1, &volume);
glBindTexture(GL_TEXTURE_3D, volume);
glTexStorage3D(GL_TEXTURE_3D, 3, GL_RGBA8, 4, 4, 4);
const std::array<GLubyte, 4> base{200, 40, 80, 255};
const std::array<GLubyte, 4> sentinel{10, 220, 30, 255};
const auto fill = [](int side, const std::array<GLubyte, 4>& texel) {
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4));
for (size_t at = 0; at < pixels.size(); at += 4) std::copy(texel.begin(), texel.end(), pixels.begin() + at);
return pixels;
};
const auto level0 = fill(4, base);
const auto level1 = fill(2, sentinel);
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, 4, 4, 4, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
glTexSubImage3D(GL_TEXTURE_3D, 1, 0, 0, 0, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, level1.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.setup";
const auto before = PeekSplitRuntime().emitSeq;
glGenerateMipmap(GL_TEXTURE_3D);
ASSERT_GT(PeekSplitRuntime().emitSeq, before) << "F1.ShaderMipDecline.wire";
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.error";
for (const int level : {0, 1}) {
const int side = 4 >> level;
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4), 0x5a);
glGetTexImage(GL_TEXTURE_3D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "F1.ShaderMipDecline.readback level " << level;
const std::array<GLubyte, 4>& expected = level == 0 ? base : sentinel;
size_t wrong = 0;
for (size_t at = 0; at < pixels.size(); at += 4)
if (!std::equal(expected.begin(), expected.end(), pixels.begin() + at)) ++wrong;
EXPECT_EQ(wrong, 0u) << "F1.ShaderMipDecline.level" << level << " tier=" << tier
<< ": a declined mip must leave every level as the application wrote it";
}
glBindTexture(GL_TEXTURE_3D, 0);
glDeleteTextures(1, &volume);
glBindTexture(GL_TEXTURE_2D, texture);
}
TEST_F(F1WireScenario, GenerateMipmapHonorsMutableBaseAndMaxWithoutChangingOtherLevels) {
if (!Ready()) return;
const std::array<GLubyte, 4> colors[5] = {
@@ -0,0 +1,199 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/FencePollScenario.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
//
// P10 (A, MG_Remote/CONTRACT-P10.md §1): FENCE POLLS ANSWERED FROM THE REVERSE CHANNEL.
//
// A glClientWaitSync / glGetSynciv(GL_SYNC_STATUS) used to be a kWaitReply record: its answer
// came back only after the server had applied every record before it, so a poll was a full
// pipeline drain and, over TCP, a round trip - once per frame in Minecraft's fence use. Now the
// server reports each fence the GPU finishes (kEventFenceSignaled) and the client answers polls
// from those reports; the Nth unanswered zero-timeout poll in a row (MOBILEGL_IPC_POLL_ESCALATE)
// still crosses, which is what bounds a loop whose report is late.
//
// Three cases. The first is ordinary GL - the idiom's two poll loops end - and its flush-bit arm
// runs on the monolith lanes too. The other two are about the wire and skip where there is none.
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <string>
#include <thread>
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
using Clock = std::chrono::steady_clock;
constexpr auto kLoopBound = std::chrono::seconds(10);
class FencePollScenario : public ScenarioTest {
protected:
// Some GPU work for a fence to wait behind.
static void QueueWork() {
glClearColor(0.25f, 0.5f, 0.75f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
};
// The GL idiom: `while (glClientWaitSync(s, flags, 0) == GL_TIMEOUT_EXPIRED) {}` and the
// glGetSynciv twin. With nothing else in the loop, no record would ever be published if
// the poll were not a doorbell point, and a local "not yet" would be repeated for ever if
// nothing made it true - so the loop ending, bounded, is the property.
//
// WITHOUT GL_SYNC_FLUSH_COMMANDS_BIT GL itself does not promise the loop ends (the fence
// may sit unflushed in the driver), so that arm is split-only: there the transport
// promises it, because the poll flushes the link and an idle server submits.
TEST_F(FencePollScenario, ZeroTimeoutPollLoopsEndInBoundedTime) {
if (!Ready()) return;
const bool split = SplitRuntimeSkipReason().empty();
for (const GLbitfield flags : {GLbitfield(GL_SYNC_FLUSH_COMMANDS_BIT), GLbitfield(0)}) {
if (flags == 0 && !split) continue;
QueueWork();
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
ASSERT_NE(sync, nullptr);
const auto start = Clock::now();
GLenum status = GL_TIMEOUT_EXPIRED;
unsigned long long polls = 0;
while ((status = glClientWaitSync(sync, flags, 0)) == GL_TIMEOUT_EXPIRED &&
Clock::now() - start < kLoopBound) {
++polls;
}
EXPECT_TRUE(status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED)
<< "glClientWaitSync(flags 0x" << std::hex << flags << std::dec << ", 0) still "
<< status << " after " << polls << " polls";
glDeleteSync(sync);
}
QueueWork();
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
ASSERT_NE(sync, nullptr);
const auto start = Clock::now();
GLint value = GL_UNSIGNALED;
while (value != GL_SIGNALED && Clock::now() - start < kLoopBound) {
glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value);
}
EXPECT_EQ(value, GL_SIGNALED) << "glGetSynciv(GL_SYNC_STATUS) never read signaled";
glDeleteSync(sync);
EXPECT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
}
// THE WIRE HALF: once a fence is known signaled, polling it costs nothing - no reply-slot
// record, no round trip - however often it is asked. MOBILEGL_IPC_POLL_ESCALATE=0 puts the
// round trip back and must fail both counters (its red-once).
TEST_F(FencePollScenario, PollsOfASignaledFenceCrossNothing) {
if (!Ready()) return;
const std::string why = SplitRuntimeSkipReason();
if (!why.empty()) GTEST_SKIP() << why;
QueueWork();
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
ASSERT_NE(sync, nullptr);
const GLenum waited = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 5000000000ull);
ASSERT_TRUE(waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED) << waited;
constexpr unsigned kPolls = 100;
const SplitRuntimeState before = PeekSplitRuntime();
for (unsigned i = 0; i < kPolls; ++i) {
EXPECT_EQ(glClientWaitSync(sync, 0, 0), static_cast<GLenum>(GL_ALREADY_SIGNALED));
GLint value = GL_UNSIGNALED;
glGetSynciv(sync, GL_SYNC_STATUS, 1, nullptr, &value);
EXPECT_EQ(value, GL_SIGNALED);
}
const SplitRuntimeState after = PeekSplitRuntime();
glDeleteSync(sync);
EXPECT_EQ(after.replyPostings - before.replyPostings, 0u)
<< "polling a signaled fence published reply-slot records (MOBILEGL_IPC_POLL_ESCALATE=0 "
"does this on purpose)";
EXPECT_EQ(after.fenceRoundTrips - before.fenceRoundTrips, 0u);
EXPECT_EQ(after.fenceLocalAnswers - before.fenceLocalAnswers, 2ull * kPolls);
}
// THE SERVER'S HALF: a fence the GPU finishes while the client sends nothing - it only
// polls - is still reported. The apply thread is idle then; it must submit what the driver
// holds and keep re-checking rather than park for ever, or the only way out of the loop is
// the escalation. The polls are paced (10 ms apart) so the escalation cannot come first by
// rate: 64 of them is 640 ms, far more than a clear takes. Red-once: take the idle
// re-check out of ServerLoop and this case escalates.
TEST_F(FencePollScenario, AFenceThatFinishesWhileTheClientOnlyPollsIsReported) {
if (!Ready()) return;
const std::string why = SplitRuntimeSkipReason();
if (!why.empty()) GTEST_SKIP() << why;
QueueWork();
GLsync sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
ASSERT_NE(sync, nullptr);
const SplitRuntimeState before = PeekSplitRuntime();
const auto start = Clock::now();
GLenum status = GL_TIMEOUT_EXPIRED;
while ((status = glClientWaitSync(sync, 0, 0)) == GL_TIMEOUT_EXPIRED &&
Clock::now() - start < kLoopBound) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
const SplitRuntimeState after = PeekSplitRuntime();
glDeleteSync(sync);
EXPECT_TRUE(status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) << status;
EXPECT_EQ(after.fenceEscalations - before.fenceEscalations, 0u)
<< "the loop needed an escalated round trip: the server did not report the fence on its "
"own while the client only polled";
EXPECT_GE(after.fenceServerReports - before.fenceServerReports, 1u);
}
// P10 A's PAIRED RECORD - a measurement, not a gate: it skips unless
// MGITEST_FENCE_PACE_BENCH=1. Minecraft's shape, isolated (in the replay corpus only
// improved-transparency-minecraft-26.3 carries timed fence waits): every frame queues work
// and a fence, swaps, and waits - with a timeout - on the fence it made two frames earlier.
// The same entry run with MOBILEGL_IPC_POLL_ESCALATE=0 and with the default is the A/B; it
// prints frames, wall time, reply-slot records and the fence counters.
TEST_F(FencePollScenario, PacedFrameFenceWaitsBench) {
if (!Ready()) return;
const char* bench = std::getenv("MGITEST_FENCE_PACE_BENCH");
if (bench == nullptr || std::string(bench) != "1") {
GTEST_SKIP() << "a measurement: set MGITEST_FENCE_PACE_BENCH=1";
}
const std::string why = SplitRuntimeSkipReason();
if (!why.empty()) GTEST_SKIP() << why;
constexpr int kFrames = 600;
// MGITEST_FENCE_PACE_LAG: how many frames back the waited fence is (default 2).
const char* lagEnv = std::getenv("MGITEST_FENCE_PACE_LAG");
const size_t lag = lagEnv != nullptr && std::atoi(lagEnv) > 0 ? static_cast<size_t>(std::atoi(lagEnv)) : 2;
std::deque<GLsync> inFlight;
const SplitRuntimeState before = PeekSplitRuntime();
const auto start = Clock::now();
for (int frame = 0; frame < kFrames; ++frame) {
QueueWork();
inFlight.push_back(glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0));
Gl().EndFrame();
if (inFlight.size() > lag) {
GLsync old = inFlight.front();
inFlight.pop_front();
const GLenum waited = glClientWaitSync(old, GL_SYNC_FLUSH_COMMANDS_BIT, 1000000000ull);
EXPECT_TRUE(waited == GL_ALREADY_SIGNALED || waited == GL_CONDITION_SATISFIED) << waited;
glDeleteSync(old);
}
}
const double ms = std::chrono::duration<double, std::milli>(Clock::now() - start).count();
const SplitRuntimeState after = PeekSplitRuntime();
for (GLsync sync : inFlight) glDeleteSync(sync);
const char* escalate = std::getenv("MOBILEGL_IPC_POLL_ESCALATE");
std::printf("P10BENCH transport=%s poll-escalate=%s lag=%zu frames=%d ms=%.1f ms/frame=%.3f "
"reply-records=%llu fence-round-trips=%llu fence-local=%llu fence-reports=%llu\n",
after.transportName.c_str(), escalate != nullptr ? escalate : "(default)", lag, kFrames, ms,
ms / kFrames, after.replyPostings - before.replyPostings,
after.fenceRoundTrips - before.fenceRoundTrips,
after.fenceLocalAnswers - before.fenceLocalAnswers,
after.fenceServerReports - before.fenceServerReports);
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,555 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GenerateMipmapServerScenario.cpp
// Copyright (c) 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
//
// Scenario - glGenerateMipmap ON THE FORMATS ESPRYT DOES NOT HAND STRAIGHT TO THE DRIVER
// (P8-B, docs/Disaggregated/notes/p8/B.md).
//
// Espryt generates R11F_G11F_B10F and depth chains with per-level blits, and RGB16F / RGB32F
// chains outside glGenerateMipmap because ES refuses a three-channel float format there. Until
// P8-B the split arms of both were named refusals:
// B1 Fatal{UnmigratedEmulation, "generate-mipmap-storage"} whenever the texture's descriptor
// held fewer levels than the full chain from level 0 - a MAX_LEVEL below the chain, or a
// glTexStorage2D with fewer levels - which is legal GL (the generated window is
// BASE_LEVEL+1 .. min(p, MAX_LEVEL), and immutable storage bounds it too);
// B2 Fatal{UnmigratedEmulation, "generate-mipmap-cpu-filter"} on every RGB16F / RGB32F
// generation, because the monolith filters the frontend's level shadows on the CPU.
//
// EVERY CASE CHECKS THE GL ANSWER BY SAMPLING. The base level is split into two halves (bottom
// rows A, top rows B) so a generation that loses the row order, or reads another level, cannot
// average to the right colour. Each generated level is read back through texelFetch into an RGBA8
// target, and the levels outside the window are checked unchanged (or still undefined).
//
// ARMS. Both backends, monolith and Split / Spawn / Tcp; Magma is the control. The forced-CPU arm
// (MGITEST_ESPRYT_FORCE_CPU_MIPMAP=1, read by the SERVER) runs the three-channel cases through
// Espryt's server-side CPU filter, which a host driver never needs: llvmpipe generates both
// formats natively. Under the knob the server log must name that arm, or the entry proved nothing.
//
// THE CPU ARM'S STORE ROUTE (P8-SE). When the driver cannot read the base level back, the server's
// CPU arm filters the staged store's copy of it - but only for a texture the driver never wrote
// (P8-E's sticky mark), because a render target's level 0 exists only on the driver and the store
// still holds what crossed before the draw. The `CpuMipRefused.` entries add
// MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=16x16 (P8-E's knob, also read by the SERVER) to the
// forced-CPU knob: a render-target base must decline by name (generate-mipmap-store-declined) and
// never filter the stale store, and an uploaded-only base must still be filtered from the store.
//
// MONOLITH DEFECTS, recorded for dev (ID-P8-3) and skipped by name on the monolith arm they live
// on unless MGITEST_RUN_MONOLITH_MIP_DEFECTS=1 is set (which is how each was shown red):
// M1 Espryt: the RGB16F / RGB32F CPU filter reads the frontend's level shadow, which a GPU
// write to level 0 leaves stale (DirectGLES.cpp GenerateThreeChannelFloatMipmapOnCpu);
// M2 Espryt: the R11F / depth blit chains start at level 1 and run to the end of the chain,
// ignoring BASE_LEVEL and MAX_LEVEL, and the storage grow before them defines levels past
// MAX_LEVEL (DirectGLES.cpp GenerateColorTexture2DMipmap / GenerateDepthTexture2DMipmap /
// EnsureGenerateMipmapStorageAllocated);
// M3 Magma: a colour format without BLIT_SRC|BLIT_DST (R11F_G11F_B10F on lavapipe) is skipped
// with a warning and its levels stay undefined texels - the monolith has no shader arm,
// which the wire arm has had since P7 (VulkanRenderer.cpp GenerateMipmap).
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdlib>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kEdge = 16;
constexpr const char* kForceCpuMipmap = "MGITEST_ESPRYT_FORCE_CPU_MIPMAP";
constexpr const char* kCpuMipmapMarker = "generate-mipmap-server-cpu";
constexpr const char* kRunMonolithDefects = "MGITEST_RUN_MONOLITH_MIP_DEFECTS";
// P8-SE: E's server-read knob; the entries set it to kEdge x kEdge, the base level's extent.
constexpr const char* kRefuseReadback = "MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT";
constexpr const char* kStoreDeclinedMarker = "generate-mipmap-store-declined";
constexpr const char* kStoreFilteredMarker = "filtering the staged store's copy";
constexpr const char* kVertexSource = R"(#version 430 core
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
)";
constexpr const char* kSampleSource = R"(#version 430 core
uniform sampler2D u_texture;
uniform int u_level;
out vec4 o_color;
void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy), u_level); }
)";
constexpr const char* kPaintSource = R"(#version 430 core
uniform vec4 u_bottom;
uniform vec4 u_top;
uniform float u_split;
out vec4 o_color;
void main() { o_color = gl_FragCoord.y < u_split ? u_bottom : u_top; }
)";
using Rgb = std::array<float, 3>;
// Exact in R11F_G11F_B10F, RGB16F and RGB32F alike, and apart in every channel.
constexpr Rgb kBottom = {0.25f, 0.5f, 0.75f};
constexpr Rgb kTop = {0.875f, 0.375f, 0.125f};
constexpr Rgb kSentinel = {1.0f, 0.0f, 1.0f};
constexpr Rgb kLevelZero = {1.0f, 0.0f, 0.0f};
constexpr Rgb kLevelTwo = {0.0f, 0.0f, 1.0f};
constexpr Rgb kLevelThree = {1.0f, 1.0f, 0.0f};
constexpr float kDepthBottom = 0.25f;
constexpr float kDepthTop = 0.75f;
constexpr float kDepthLevelZero = 0.125f;
constexpr float kDepthLevelTwo = 0.5f;
constexpr float kDepthLevelThree = 1.0f;
struct Format {
GLenum internalFormat;
bool depth;
const char* name;
};
constexpr Format kPackedFloat = {GL_R11F_G11F_B10F, false, "R11F_G11F_B10F"};
constexpr Format kDepth32F = {GL_DEPTH_COMPONENT32F, true, "DEPTH_COMPONENT32F"};
constexpr Format kRgb16F = {GL_RGB16F, false, "RGB16F"};
constexpr Format kRgb32F = {GL_RGB32F, false, "RGB32F"};
int ToByte(float value) { return static_cast<int>(std::lround(value * 255.0f)); }
class GenerateMipmapServerScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_wire = SplitRuntimeSkipReason().empty();
m_espryt = Gl().BackendName() == "DirectGLES";
m_forcedCpu = std::getenv(kForceCpuMipmap) != nullptr;
m_refusedReadback = std::getenv(kRefuseReadback) != nullptr;
std::string error;
m_sampleProgram = CompileProgram(kVertexSource, kSampleSource, &error);
ASSERT_NE(m_sampleProgram, 0u) << error;
m_paintProgram = CompileProgram(kVertexSource, kPaintSource, &error);
ASSERT_NE(m_paintProgram, 0u) << error;
m_target = MakeColorFbo(kEdge, kEdge);
ASSERT_NE(m_target.fbo, 0u) << "could not create the sampling target";
glGenVertexArrays(1, &m_vao);
glGenFramebuffers(1, &m_fbo);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
ASSERT_EQ(FirstGLError(), 0u);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
if (m_sampleProgram != 0) glDeleteProgram(m_sampleProgram);
if (m_paintProgram != 0) glDeleteProgram(m_paintProgram);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
BindDefaultFramebuffer();
if (m_target.fbo != 0) DestroyColorFbo(m_target);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
// Whether this case meets a recorded monolith defect on this arm (see the header). The
// knob runs it anyway, which is how each defect is shown red and how dev's fix is checked.
bool MonolithDefect(bool onEspryt, bool onMagma) const {
if (m_wire || std::getenv(kRunMonolithDefects) != nullptr) return false;
return m_espryt ? onEspryt : onMagma;
}
static constexpr const char* kM1 =
"monolith defect M1 (recorded for dev, ID-P8-3): Espryt's CPU filter reads the frontend's "
"level shadow, which the draw into level 0 left stale";
static constexpr const char* kM2 =
"monolith defect M2 (recorded for dev, ID-P8-3): Espryt's blit chain ignores BASE_LEVEL and "
"MAX_LEVEL and its storage grow defines levels past MAX_LEVEL";
static constexpr const char* kM3 =
"monolith defect M3 (recorded for dev, ID-P8-3): Magma skips a format without native blit "
"and leaves the generated levels empty";
GLuint NewTexture() {
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_2D, m_texture);
return m_texture;
}
// A level `edge` x `edge` whose rows below the middle are `bottom` and above it `top`.
static std::vector<float> Halves(const Format& format, int edge, const Rgb& bottom, const Rgb& top,
float depthBottom, float depthTop) {
const int channels = format.depth ? 1 : 3;
std::vector<float> texels(static_cast<std::size_t>(edge * edge * channels));
for (int y = 0; y < edge; ++y) {
for (int x = 0; x < edge; ++x) {
float* texel = texels.data() + static_cast<std::size_t>((y * edge + x) * channels);
const bool low = y < edge / 2;
if (format.depth) {
texel[0] = low ? depthBottom : depthTop;
} else {
for (int c = 0; c < 3; ++c) texel[c] = low ? bottom[c] : top[c];
}
}
}
return texels;
}
static std::vector<float> Flat(const Format& format, int edge, const Rgb& colour, float depth) {
return Halves(format, edge, colour, colour, depth, depth);
}
static void DefineLevel(const Format& format, int level, int edge, const std::vector<float>& texels) {
glTexImage2D(GL_TEXTURE_2D, level, static_cast<GLint>(format.internalFormat), edge, edge, 0,
format.depth ? GL_DEPTH_COMPONENT : GL_RGB, GL_FLOAT, texels.data());
}
static void UploadLevel(const Format& format, int level, int edge, const std::vector<float>& texels) {
glTexSubImage2D(GL_TEXTURE_2D, level, 0, 0, edge, edge, format.depth ? GL_DEPTH_COMPONENT : GL_RGB,
GL_FLOAT, texels.data());
}
static int LevelWidth(int level) {
GLint width = 0;
glGetTexLevelParameteriv(GL_TEXTURE_2D, level, GL_TEXTURE_WIDTH, &width);
return width;
}
// texelFetch's level is relative to BASE_LEVEL and bounded by MAX_LEVEL, so the window is
// reopened over every defined level before any of them is read.
Image SampleLevel(int level, int maxLevel) {
const int edge = std::max(kEdge >> level, 1);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glUseProgram(m_sampleProgram);
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_texture"), 0);
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_level"), level);
BindFbo(m_target);
glViewport(0, 0, edge, edge);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
return ReadPixelsRect(0, 0, edge, edge);
}
// Every row of `image` against the half it belongs to. A depth level is read in its red
// channel only, and only in its bottom and top rows: the rows next to the boundary are
// where a shader-based depth mip is entitled to a half-texel offset.
static void ExpectHalves(const Image& image, const Format& format, const Rgb& bottom, const Rgb& top,
float depthBottom, float depthTop, const std::string& what) {
const int height = image.Height();
for (int y = 0; y < height; ++y) {
if (format.depth && y != 0 && y != height - 1) continue;
const bool low = y < height / 2;
for (int x = 0; x < image.Width(); ++x) {
const Rgba8 got = image.At(x, y);
if (format.depth) {
const int want = ToByte(low ? depthBottom : depthTop);
EXPECT_NEAR(got.r, want, 2) << what << " depth at (" << x << "," << y << ") got " << got;
if (std::abs(got.r - want) > 2) return;
continue;
}
const Rgb& colour = low ? bottom : top;
const int want[3] = {ToByte(colour[0]), ToByte(colour[1]), ToByte(colour[2])};
const int have[3] = {got.r, got.g, got.b};
for (int c = 0; c < 3; ++c) {
EXPECT_NEAR(have[c], want[c], 2) << what << " channel " << c << " at (" << x << ","
<< y << ") got " << got;
if (std::abs(have[c] - want[c]) > 2) return;
}
}
}
}
static void ExpectFlat(const Image& image, const Format& format, const Rgb& colour, float depth,
const std::string& what) {
ExpectHalves(image, format, colour, colour, depth, depth, what);
}
void Generate() {
m_mark = PipeStatsWindow::MarkLaneLog();
glBindTexture(GL_TEXTURE_2D, m_texture);
glGenerateMipmap(GL_TEXTURE_2D);
ASSERT_EQ(FirstGLError(), 0u) << "glGenerateMipmap";
}
// Under the knob the SERVER must say it filtered on the CPU; without that line the entry
// ran the native arm and its green says nothing about the arm it is named for.
void ExpectCpuArmNamed() {
if (!m_forcedCpu || !m_wire || !m_espryt) return;
const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark);
EXPECT_NE(log.find(kCpuMipmapMarker), std::string::npos)
<< kForceCpuMipmap << " is set but the server log after glGenerateMipmap carries no '"
<< kCpuMipmapMarker << "' line: the knob did not reach the server, or the three-channel "
<< "generation never took the server's CPU arm";
}
// ---- B1: the generated window, on the two formats Espryt blits level by level ----------
// Levels 0..2 defined, MAX_LEVEL = 2: the descriptor holds three of the chain's five levels.
void MutableMaxLevelTwo(const Format& format) {
if (MonolithDefect(true, !format.depth)) GTEST_SKIP() << (m_espryt ? kM2 : kM3);
NewTexture();
DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, kDepthBottom, kDepthTop));
DefineLevel(format, 1, kEdge / 2, Flat(format, kEdge / 2, kSentinel, kDepthLevelZero));
DefineLevel(format, 2, kEdge / 4, Flat(format, kEdge / 4, kSentinel, kDepthLevelZero));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
ASSERT_EQ(FirstGLError(), 0u) << "define levels 0..2";
Generate();
EXPECT_EQ(LevelWidth(3), 0) << format.name << ": level 3 is past MAX_LEVEL and must stay undefined";
for (int level = 1; level <= 2; ++level)
ExpectHalves(SampleLevel(level, 2), format, kBottom, kTop, kDepthBottom, kDepthTop,
std::string(format.name) + " MAX_LEVEL=2 level " + std::to_string(level));
ExpectHalves(SampleLevel(0, 2), format, kBottom, kTop, kDepthBottom, kDepthTop,
std::string(format.name) + " MAX_LEVEL=2 level 0 (the base, unchanged)");
EXPECT_EQ(FirstGLError(), 0u);
}
// glTexStorage2D with three of the chain's five levels.
void PartialStorage(const Format& format) {
if (MonolithDefect(false, !format.depth)) GTEST_SKIP() << kM3;
NewTexture();
glTexStorage2D(GL_TEXTURE_2D, 3, format.internalFormat, kEdge, kEdge);
UploadLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, kDepthBottom, kDepthTop));
ASSERT_EQ(FirstGLError(), 0u) << "partial storage";
Generate();
for (int level = 1; level <= 2; ++level)
ExpectHalves(SampleLevel(level, 2), format, kBottom, kTop, kDepthBottom, kDepthTop,
std::string(format.name) + " three-level storage level " + std::to_string(level));
EXPECT_EQ(FirstGLError(), 0u);
}
// BASE_LEVEL = 1, MAX_LEVEL = 2 over four defined levels: only level 2 is generated, from
// level 1; levels 0 and 3 keep what they held and level 4 is never defined.
void MutableBaseLevelOne(const Format& format) {
if (MonolithDefect(true, !format.depth)) GTEST_SKIP() << (m_espryt ? kM2 : kM3);
NewTexture();
DefineLevel(format, 0, kEdge, Flat(format, kEdge, kLevelZero, kDepthLevelZero));
DefineLevel(format, 1, kEdge / 2,
Halves(format, kEdge / 2, kBottom, kTop, kDepthBottom, kDepthTop));
DefineLevel(format, 2, kEdge / 4, Flat(format, kEdge / 4, kLevelTwo, kDepthLevelTwo));
DefineLevel(format, 3, kEdge / 8, Flat(format, kEdge / 8, kLevelThree, kDepthLevelThree));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
ASSERT_EQ(FirstGLError(), 0u) << "define levels 0..3";
Generate();
EXPECT_EQ(LevelWidth(4), 0) << format.name << ": level 4 is past MAX_LEVEL and must stay undefined";
const std::string name(format.name);
ExpectFlat(SampleLevel(0, 3), format, kLevelZero, kDepthLevelZero, name + " level 0 (below BASE_LEVEL)");
ExpectHalves(SampleLevel(1, 3), format, kBottom, kTop, kDepthBottom, kDepthTop, name + " level 1 (base)");
ExpectHalves(SampleLevel(2, 3), format, kBottom, kTop, kDepthBottom, kDepthTop,
name + " level 2 (generated from level 1)");
ExpectFlat(SampleLevel(3, 3), format, kLevelThree, kDepthLevelThree,
name + " level 3 (past MAX_LEVEL)");
EXPECT_EQ(FirstGLError(), 0u);
}
// ---- B2: RGB16F / RGB32F ----------------------------------------------------------------
void UploadedBase(const Format& format) {
NewTexture();
DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, 0, 0));
ASSERT_EQ(FirstGLError(), 0u) << "define level 0";
Generate();
const int last = 4;
for (int level = 1; level < last; ++level)
ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0,
std::string(format.name) + " uploaded base, level " + std::to_string(level));
EXPECT_EQ(LevelWidth(last), 1) << format.name << ": the generated chain ends at 1x1";
ExpectCpuArmNamed();
EXPECT_EQ(FirstGLError(), 0u);
}
// Complementary's colortex2 shape: the base is a render target the GPU wrote after its one
// upload, so the chain must come from the GPU's level 0 and never from the uploaded texels.
void RenderTargetBase(const Format& format) {
if (MonolithDefect(true, false)) GTEST_SKIP() << kM1;
PaintSentinelBaseAsRenderTarget(format);
if (HasFatalFailure() || IsSkipped()) return;
Generate();
const int last = 4;
for (int level = 0; level < last; ++level)
ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0,
std::string(format.name) + " render-target base, level " + std::to_string(level));
ExpectCpuArmNamed();
EXPECT_EQ(FirstGLError(), 0u);
}
// ---- P8-SE: the CPU arm's store route under a refused readback ----------------------
// Only the `CpuMipRefused.` entries carry both server knobs; anywhere else the store route
// is unreachable on a host (its driver reads every level back) and the case says so.
void SkipUnlessTheStoreRouteIsArmed() {
if (m_wire && m_espryt && m_forcedCpu && m_refusedReadback) return;
GTEST_SKIP() << "the store route needs Espryt's split server with " << kForceCpuMipmap << " and "
<< kRefuseReadback << "=" << kEdge << "x" << kEdge << " (the CpuMipRefused. entries)";
}
// The render-target base again, with the driver refusing to read level 0 back: the store
// holds the uploaded sentinel, the driver the drawn halves, and P8-E's mark says so. The
// arm must decline by name - a stale chain of sentinel texels is the defect.
void RefusedReadbackOfADrawnBase(const Format& format) {
SkipUnlessTheStoreRouteIsArmed();
if (IsSkipped()) return;
PaintSentinelBaseAsRenderTarget(format);
if (HasFatalFailure() || IsSkipped()) return;
Generate();
const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark);
EXPECT_NE(log.find(kStoreDeclinedMarker), std::string::npos)
<< "the base level was drawn and its readback refused, but the server log after glGenerateMipmap "
"has no '" << kStoreDeclinedMarker << "' line";
EXPECT_EQ(log.find(kStoreFilteredMarker), std::string::npos)
<< "the CPU arm filtered the staged store's copy of a level the driver has since drawn over";
const int last = 4;
ExpectHalves(SampleLevel(0, last), format, kBottom, kTop, 0, 0,
std::string(format.name) + " drawn base (untouched by the decline)");
for (int level = 1; level < last; ++level) {
const Image image = SampleLevel(level, last);
const Rgba8 got = image.At(0, 0);
EXPECT_FALSE(std::abs(got.r - ToByte(kSentinel[0])) <= 2 && std::abs(got.g - ToByte(kSentinel[1])) <= 2 &&
std::abs(got.b - ToByte(kSentinel[2])) <= 2)
<< format.name << " level " << level << " is the uploaded sentinel " << got
<< ": the chain was filtered from the stale staged store";
}
ExpectCpuArmNamed();
FirstGLError();
}
// The control: a base that only ever crossed as an upload IS what the store holds, so a
// refused readback is answered from the store and the chain is right. This is what tells
// the check above from "the store route always declines" - the generation's own
// driver-write note must not count against the level it reads.
void RefusedReadbackOfAnUploadedBase(const Format& format) {
SkipUnlessTheStoreRouteIsArmed();
if (IsSkipped()) return;
NewTexture();
DefineLevel(format, 0, kEdge, Halves(format, kEdge, kBottom, kTop, 0, 0));
ASSERT_EQ(FirstGLError(), 0u) << "define level 0";
Generate();
const std::string log = PipeStatsWindow::ReadServerLogSince(m_mark);
EXPECT_NE(log.find(kStoreFilteredMarker), std::string::npos)
<< "the uploaded base's readback was refused, but the server log has no '" << kStoreFilteredMarker
<< "' line: the store route did not answer";
EXPECT_EQ(log.find(kStoreDeclinedMarker), std::string::npos)
<< "the store route declined a level that only ever crossed as an upload";
const int last = 4;
for (int level = 1; level < last; ++level)
ExpectHalves(SampleLevel(level, last), format, kBottom, kTop, 0, 0,
std::string(format.name) + " uploaded base, store-filtered level " +
std::to_string(level));
EXPECT_EQ(FirstGLError(), 0u);
}
// Level 0 uploaded as the sentinel, then drawn over as a render target (bottom / top halves).
void PaintSentinelBaseAsRenderTarget(const Format& format) {
NewTexture();
DefineLevel(format, 0, kEdge, Flat(format, kEdge, kSentinel, 0));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_texture, 0);
const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
glBindFramebuffer(GL_FRAMEBUFFER, 0);
FirstGLError();
GTEST_SKIP() << format.name << " is not colour-renderable on " << Gl().BackendName()
<< " (status 0x" << std::hex << status << ")";
}
glViewport(0, 0, kEdge, kEdge);
glUseProgram(m_paintProgram);
glUniform4f(glGetUniformLocation(m_paintProgram, "u_bottom"), kBottom[0], kBottom[1], kBottom[2], 1);
glUniform4f(glGetUniformLocation(m_paintProgram, "u_top"), kTop[0], kTop[1], kTop[2], 1);
glUniform1f(glGetUniformLocation(m_paintProgram, "u_split"), kEdge / 2.0f);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ASSERT_EQ(FirstGLError(), 0u) << "draw into level 0";
}
bool m_wire = false;
bool m_espryt = false;
bool m_forcedCpu = false;
bool m_refusedReadback = false;
GLuint m_sampleProgram = 0;
GLuint m_paintProgram = 0;
GLuint m_vao = 0;
GLuint m_fbo = 0;
GLuint m_texture = 0;
ColorFbo m_target{};
PipeStatsWindow::LogMark m_mark{};
};
} // namespace
// B1.
TEST_F(GenerateMipmapServerScenario, PackedFloatMutableMaxLevelBelowTheChain) {
if (!Ready()) return;
MutableMaxLevelTwo(kPackedFloat);
}
TEST_F(GenerateMipmapServerScenario, PackedFloatStorageWithFewerLevelsThanTheChain) {
if (!Ready()) return;
PartialStorage(kPackedFloat);
}
TEST_F(GenerateMipmapServerScenario, PackedFloatMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow) {
if (!Ready()) return;
MutableBaseLevelOne(kPackedFloat);
}
TEST_F(GenerateMipmapServerScenario, DepthMutableMaxLevelBelowTheChain) {
if (!Ready()) return;
MutableMaxLevelTwo(kDepth32F);
}
TEST_F(GenerateMipmapServerScenario, DepthStorageWithFewerLevelsThanTheChain) {
if (!Ready()) return;
PartialStorage(kDepth32F);
}
TEST_F(GenerateMipmapServerScenario, DepthMutableBaseLevelOneKeepsTheLevelsOutsideItsWindow) {
if (!Ready()) return;
MutableBaseLevelOne(kDepth32F);
}
// B2.
TEST_F(GenerateMipmapServerScenario, Rgb16fUploadedBaseGeneratesItsChain) {
if (!Ready()) return;
UploadedBase(kRgb16F);
}
TEST_F(GenerateMipmapServerScenario, Rgb16fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) {
if (!Ready()) return;
RenderTargetBase(kRgb16F);
}
TEST_F(GenerateMipmapServerScenario, Rgb32fUploadedBaseGeneratesItsChain) {
if (!Ready()) return;
UploadedBase(kRgb32F);
}
TEST_F(GenerateMipmapServerScenario, Rgb32fRenderTargetBaseGeneratesFromTheGpuWrittenLevel) {
if (!Ready()) return;
RenderTargetBase(kRgb32F);
}
// P8-SE: the CPU arm's store route (header; the CpuMipRefused. entries).
TEST_F(GenerateMipmapServerScenario, Rgb16fDrawnBaseWithARefusedReadbackDeclinesByName) {
if (!Ready()) return;
RefusedReadbackOfADrawnBase(kRgb16F);
}
TEST_F(GenerateMipmapServerScenario, Rgb16fUploadedBaseWithARefusedReadbackIsFilteredFromTheStore) {
if (!Ready()) return;
RefusedReadbackOfAnUploadedBase(kRgb16F);
}
} // namespace MGITest
@@ -0,0 +1,800 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/GpuWrittenDrawInputScenario.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
//
// Scenario - DRAW INPUTS A SHADER WROTE, READ BY A BACKEND ON THE CPU (P8-C).
//
// Espryt emulates five desktop-GL draw features on the CPU, and each one reads a buffer's bytes
// on the side that applies the draw:
//
// * the primitive-restart rewrite (an application restart index that is not the all-ones value
// of the index type): the whole element array buffer, rewritten into a scratch copy;
// * the multi-draw rebase tier, which a batch with such a restart index is forced onto;
// * *IndirectCount: the count word of the parameter buffer (GLES has no counted indirect draw);
// * a native indirect draw's gl_BaseVertex, fed to the shader as a uniform from the command;
// * a GL_DOUBLE vertex array, narrowed to float from the source buffer's bytes.
//
// Under split those bytes are the server's R-11 staged copy, which only uploads fill. A buffer a
// shader WROTE on the server - storage block, atomic counter, buffer image, transform feedback -
// is therefore read stale unless the server refreshes its copy first. Every case below writes
// the buffer on the GPU only (the staged copy holds a poison the correct picture cannot come
// from), so a stale read is a wrong picture, never a lucky one.
//
// MONOLITH ARMS KNOWN BROKEN HERE SKIP BY NAME (SkipBrokenMonolith). Espryt reads three from the
// frontend shadow without SyncGpuWrites: *IndirectCount (OPEN-QUESTIONS 15), an indirect
// gl_BaseVertex (the comment beside it in DirectGLES.cpp says so) and a GL_DOUBLE vertex array
// (the M-3 deviation declared in Managers.cpp). Magma reads MultiDrawArraysIndirectCount's count
// the same way, and its restart rewrite and GL_DOUBLE conversion call SyncGpuWrites while the
// draw is being recorded, so the draw then records into an ended command buffer (SIGSEGV in
// lavapipe). Those arms are dev's to fix (ID-P8-3); the skip keeps the ambient monolith lane
// honest without hiding the split arms, which run the same cases under DirectGLES.Split/Spawn/Tcp,
// DirectVulkan.*.Full and the inproc A/B.
#include <cstdint>
#include <iterator>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kSurface = 64;
constexpr const char* kFlatVertex = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFlatFragment = R"(#version 430 core
out vec4 oColor;
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
)";
// Green when the shader saw gl_BaseVertex == 9 (the value the compute shader wrote),
// red for anything else - in particular 3, the value the staged copy still holds.
constexpr const char* kBaseVertexVertex = R"(#version 450 core
#extension GL_ARB_shader_draw_parameters : require
layout(location = 0) in vec2 aPos;
flat out vec4 vColor;
void main() {
vColor = gl_BaseVertexARB == 9 ? vec4(0.0, 1.0, 0.0, 1.0) : vec4(1.0, 0.0, 0.0, 1.0);
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
constexpr const char* kBaseVertexFragment = R"(#version 450 core
flat in vec4 vColor;
out vec4 oColor;
void main() { oColor = vColor; }
)";
// ---- the restart scene (PrimitiveRestartScenario's geometry) ----
//
// Two triangles with a gap between them, plus two spare vertices at the origin. Index 7 is
// the application's restart index and a legal vertex, so a dropped restart welds the strip
// across the gap (caught by the gap probe) instead of reading out of bounds.
constexpr GLfloat kRestartVertices[] = {
-0.9f, -0.9f, -0.1f, -0.9f, -0.9f, 0.9f, // 0-2: left triangle
0.1f, -0.9f, 0.9f, -0.9f, 0.9f, 0.9f, // 3-5: right triangle
0.0f, 0.0f, 0.0f, 0.0f, // 6, 7: spares
};
constexpr GLuint kRestartIndex = 7;
constexpr GLuint kRestartIndexCount = 7;
// What the compute shader writes: {0, 1, 2, 7, 3, 4, 5}.
constexpr const char* kWriteRestartIndices = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Indices { uint value[]; };
void main() {
value[0] = 0u; value[1] = 1u; value[2] = 2u; value[3] = 7u;
value[4] = 3u; value[5] = 4u; value[6] = 5u;
}
)";
// The staged poison: every index names spare vertex 6, a strip of degenerate triangles
// that paints nothing.
const std::vector<GLuint> kRestartPoison(kRestartIndexCount, 6u);
// The same seven indices, produced by a TRANSFORM FEEDBACK capture: one point per index.
constexpr const char* kCaptureIndicesVertex = R"(#version 430 core
flat out uint idx;
const uint kTable[7] = uint[7](0u, 1u, 2u, 7u, 3u, 4u, 5u);
void main() {
idx = kTable[gl_VertexID];
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 1.0;
}
)";
// ...and by a capture with a HOLE after every index (gl_SkipComponents1), which ES cannot
// express and Espryt therefore scatters itself. The captured words land at the even
// slots; the odd slots keep what the application staged there.
constexpr const char* kCaptureScatteredVertex = R"(#version 430 core
flat out uint idx;
const uint kTable[4] = uint[4](0u, 2u, 3u, 5u);
void main() {
idx = kTable[gl_VertexID];
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 1.0;
}
)";
constexpr const char* kCaptureFragment = R"(#version 430 core
out vec4 oColor;
void main() { oColor = vec4(1.0); }
)";
// Even slots are the capture's (poison 6 until it lands), odd slots the holes: after the
// scatter the first seven words are {0, 1, 2, 7, 3, 4, 5}; before it, a strip of
// degenerate triangles.
constexpr GLuint kScatterStaged[8] = {6u, 1u, 6u, 7u, 6u, 4u, 6u, 6u};
// ...and by IMAGE STORES into a buffer texture over the element array buffer.
constexpr const char* kImageStoreRestartIndices = R"(#version 430 core
layout(local_size_x = 1) in;
layout(r32ui, binding = 0) uniform writeonly uimageBuffer indices;
const uint kTable[7] = uint[7](0u, 1u, 2u, 7u, 3u, 4u, 5u);
void main() {
for (int i = 0; i < 7; ++i) imageStore(indices, i, uvec4(kTable[i]));
}
)";
// NDC (-0.5, -0.5), (0.6, -0.5), (0.2, -0.5) on the 64x64 surface.
constexpr int kLeftX = 16, kRightX = 51, kGapX = 38, kProbeY = 16;
// ---- the indirect scene (DrawParametersScenario's geometry) ----
//
// Three pad vertices at the origin, then the left half of the surface as two triangles
// (vertices 3-8), then the right half (9-14). Element indices 0..5 address one half
// through the command's baseVertex.
constexpr int kLeftFirst = 3;
constexpr int kRightFirst = 9;
constexpr int kHalfCount = 6;
std::vector<GLfloat> HalvesVertices() {
std::vector<GLfloat> v(6, 0.0f);
const float bounds[2][2] = {{-1.0f, 0.0f}, {0.0f, 1.0f}};
for (const auto& half : bounds) {
const float x0 = half[0];
const float x1 = half[1];
const float quad[] = {x0, -1.0f, x1, -1.0f, x1, 1.0f, x0, -1.0f, x1, 1.0f, x0, 1.0f};
v.insert(v.end(), std::begin(quad), std::end(quad));
}
return v;
}
struct ElementsCommand {
std::uint32_t count, instanceCount, firstIndex;
std::int32_t baseVertex;
std::uint32_t baseInstance;
};
struct ArraysCommand {
std::uint32_t count, instanceCount, first, baseInstance;
};
bool IsGreen(const Rgba8& p) { return p.g > 128 && p.r < 128; }
class GpuWrittenDrawInputScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_target = MakeColorFbo(kSurface, kSurface);
ASSERT_NE(m_target.fbo, 0u) << "the 64x64 colour target did not build";
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glDisable(GL_DEPTH_TEST);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_CULL_FACE);
DrainErrors();
}
void TearDown() override {
if (!Ready()) return;
glDisable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(0);
glUseProgram(0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, 0);
glBindBuffer(GL_PARAMETER_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
if (m_texture != 0) glDeleteTextures(1, &m_texture);
m_texture = 0;
for (GLuint* buffer : {&m_vbo, &m_ebo, &m_commands, &m_parameters}) {
if (*buffer != 0) glDeleteBuffers(1, buffer);
*buffer = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
for (GLuint program : m_programs) glDeleteProgram(program);
m_programs.clear();
BindDefaultFramebuffer();
DestroyColorFbo(m_target);
DrainErrors();
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
// THE ARMS THIS FILE SKIPS, BY NAME: the monolith arm of a backend whose reason is
// non-null. Every split arm, and the inproc A/B, runs the case.
bool SkipBrokenMonolith(const char* espryt, const char* magma) {
if (PeekSplitRuntime().transportName != "monolith") return false;
const std::string backend = Gl().BackendName();
const char* why = backend == "DirectGLES" ? espryt : backend == "DirectVulkan" ? magma : nullptr;
if (why == nullptr) return false;
m_skipReason = "monolith " + backend + ": " + why +
" (dev's fix, ID-P8-3) - this case gates the split arms";
return true;
}
static constexpr const char* kUnsyncedCount =
"the *IndirectCount count is read from the frontend shadow without SyncGpuWrites "
"(OPEN-QUESTIONS 15)";
static constexpr const char* kUnsyncedBaseVertex =
"an indirect command's gl_BaseVertex is read from the frontend shadow without "
"SyncGpuWrites";
static constexpr const char* kUnsyncedDoubles =
"a GL_DOUBLE vertex array is narrowed from the client shadow without SyncGpuWrites "
"(the M-3 deviation declared in Managers.cpp)";
static constexpr const char* kSyncMidRecording =
"SyncGpuWrites inside the draw (the restart rewrite, the GL_DOUBLE vertex conversion) "
"submits the frame mid-recording and the draw then records into the ended command "
"buffer (SIGSEGV)";
GLuint Program(const char* vertex, const char* fragment) {
std::string error;
const GLuint program = CompileProgram(vertex, fragment, &error);
EXPECT_NE(program, 0u) << error;
if (program != 0) m_programs.push_back(program);
return program;
}
GLuint ComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (!compiled) {
char log[4096]{};
glGetShaderInfoLog(shader, sizeof(log), nullptr, log);
ADD_FAILURE() << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) {
char log[4096]{};
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
ADD_FAILURE() << log;
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
// Buffer `name` at `target` with `bytes` bytes of `data` (null: an orphaned store).
static void Store(GLuint& name, GLenum target, GLsizeiptr bytes, const void* data) {
if (name == 0) glGenBuffers(1, &name);
glBindBuffer(target, name);
glBufferData(target, bytes, data, GL_DYNAMIC_COPY);
}
void BindVertices(const GLfloat* vertices, GLsizeiptr bytes) {
glBindVertexArray(m_vao);
Store(m_vbo, GL_ARRAY_BUFFER, bytes, vertices);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), nullptr);
}
// One dispatch of `source` with `first` (and `second`, if non-zero) bound as storage
// buffers 0 and 1, then the barrier the draw that consumes them needs.
void WriteWithCompute(const char* source, GLuint first, GLuint second, GLbitfield barrier) {
const GLuint compute = ComputeProgram(source);
ASSERT_NE(compute, 0u);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, first);
if (second != 0) glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, second);
glUseProgram(compute);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(barrier | GL_SHADER_STORAGE_BARRIER_BIT);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
glUseProgram(0);
}
template <typename DrawFn>
Image Render(GLuint program, DrawFn&& draw) {
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(program);
glBindVertexArray(m_vao);
draw();
return ReadPixels(kSurface, kSurface);
}
// The element array buffer of the restart scene, written by a compute shader over the
// poison (or over an orphaned store when `orphaned`).
void BuildRestartScene(bool orphaned) {
BindVertices(kRestartVertices, sizeof(kRestartVertices));
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint),
orphaned ? nullptr : kRestartPoison.data());
WriteWithCompute(kWriteRestartIndices, m_ebo, 0, GL_ELEMENT_ARRAY_BARRIER_BIT);
ArmRestart();
}
// The element array buffer bound to the scene's VAO, restart index 7 on.
void ArmRestart() {
glBindVertexArray(m_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glEnable(GL_PRIMITIVE_RESTART);
glPrimitiveRestartIndex(kRestartIndex);
}
// A vertex + fragment program whose `varyings` are captured INTERLEAVED.
GLuint XfbProgram(const char* vertex, const char* fragment, const char* const* varyings,
GLsizei count) {
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vs, 1, &vertex, nullptr);
glCompileShader(vs);
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fs, 1, &fragment, nullptr);
glCompileShader(fs);
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glTransformFeedbackVaryings(program, count, varyings, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) {
char log[4096]{};
glGetProgramInfoLog(program, sizeof(log), nullptr, log);
ADD_FAILURE() << "the capture program did not link: " << log;
glDeleteProgram(program);
return 0;
}
m_programs.push_back(program);
return program;
}
// `vertices` points drawn with rasterization off, captured into m_ebo from byte 0.
void CaptureIntoElementBuffer(GLuint program, GLsizei vertices) {
glBindVertexArray(m_vao);
glUseProgram(program);
glEnable(GL_RASTERIZER_DISCARD);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, m_ebo);
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, vertices);
glEndTransformFeedback();
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
}
static void ExpectSplitStrip(const Image& image, const char* what) {
EXPECT_TRUE(IsGreen(image.At(kLeftX, kProbeY)))
<< what << ": the left triangle did not render (" << image.At(kLeftX, kProbeY)
<< ") - the backend drew the indices it had staged, not the ones the compute "
"shader wrote";
EXPECT_TRUE(IsGreen(image.At(kRightX, kProbeY)))
<< what << ": the right triangle did not render (" << image.At(kRightX, kProbeY) << ")";
EXPECT_FALSE(IsGreen(image.At(kGapX, kProbeY)))
<< what << ": the gap is covered, so the restart at index 7 was dropped";
}
// Left and right half of the halves scene, sampled at their centres.
static Rgba8 Half(const Image& image, int half) {
return image.At(kSurface * (1 + 2 * half) / 4, kSurface / 2);
}
ColorFbo m_target;
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_ebo = 0;
GLuint m_commands = 0;
GLuint m_parameters = 0;
GLuint m_texture = 0;
std::vector<GLuint> m_programs;
std::string m_skipReason;
};
} // namespace
// ---- the primitive-restart rewrite (DirectGLES.cpp ScopedRestartIndexSubstitution) ----
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteSplitTheStrip) {
if (!Ready()) return;
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
BuildRestartScene(/*orphaned=*/false);
const Image image = Render(program, [] {
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
ExpectSplitStrip(image, "glDrawElements over compute-written restart indices");
}
// The same indices in a store the application ORPHANED (glBufferData(NULL)): nothing was
// ever staged for it, so the only copy of its bytes is the one the GPU holds.
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteIntoAnOrphanedStoreSplitTheStrip) {
if (!Ready()) return;
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
BuildRestartScene(/*orphaned=*/true);
const Image image = Render(program, [] {
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
ExpectSplitStrip(image, "glDrawElements over compute-written restart indices in an orphaned store");
}
// ---- the multi-draw rebase tier (MultiDraw.cpp RunRebasedDrawElements) ----
//
// An arbitrary restart index forces a multi-draw onto the one tier that rewrites the stream,
// and that tier reads the whole element array buffer on the CPU. Sub-draw 0 is {0,1,2} (the
// left triangle); sub-draw 1 is {7,3,4,5} (a restart, then the right triangle).
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAComputeShaderWroteSplitEveryMultiDrawStrip) {
if (!Ready()) return;
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
BuildRestartScene(/*orphaned=*/false);
const GLsizei counts[2] = {3, 4};
const void* offsets[2] = {nullptr, reinterpret_cast<const void*>(3 * sizeof(GLuint))};
const Image image = Render(program, [&] {
glMultiDrawElements(GL_TRIANGLE_STRIP, counts, GL_UNSIGNED_INT, offsets, 2);
});
EXPECT_EQ(FirstGLError(), 0u);
ExpectSplitStrip(image, "glMultiDrawElements over compute-written restart indices");
}
// ---- *IndirectCount (DirectGLES.cpp MultiDraw{Elements,Arrays}IndirectCount) ----
//
// The compute shader writes BOTH commands and the count; the staged copies hold zeroes, so a
// stale count draws nothing and a stale command (on a device without native indirect) draws
// nothing either.
TEST_F(GpuWrittenDrawInputScenario, ACountAComputeShaderWroteDrivesMultiDrawElementsIndirectCount) {
if (!Ready()) return;
if (SkipBrokenMonolith(kUnsyncedCount, nullptr)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const std::vector<GLfloat> vertices = HalvesVertices();
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
const ElementsCommand zeroCommands[2] = {};
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
const GLuint zeroCount = 0;
Store(m_parameters, GL_PARAMETER_BUFFER, sizeof(zeroCount), &zeroCount);
WriteWithCompute(R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Commands { uint command[]; };
layout(std430, binding = 1) buffer Parameters { uint count; };
void main() {
command[0] = 6u; command[1] = 1u; command[2] = 0u; command[3] = 3u; command[4] = 0u;
command[5] = 6u; command[6] = 1u; command[7] = 0u; command[8] = 9u; command[9] = 0u;
count = 2u;
}
)",
m_commands, m_parameters, GL_COMMAND_BARRIER_BIT);
glBindVertexArray(m_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
const Image image = Render(program, [] {
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 2, 0);
});
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 0)))
<< "command 0 did not draw (" << Half(image, 0)
<< "): the count was read from a copy the compute shader never reached";
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
}
TEST_F(GpuWrittenDrawInputScenario, ACountAComputeShaderWroteDrivesMultiDrawArraysIndirectCount) {
if (!Ready()) return;
if (SkipBrokenMonolith(kUnsyncedCount, kUnsyncedCount)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const std::vector<GLfloat> vertices = HalvesVertices();
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
const ArraysCommand zeroCommands[2] = {};
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
const GLuint zeroCount = 0;
Store(m_parameters, GL_PARAMETER_BUFFER, sizeof(zeroCount), &zeroCount);
WriteWithCompute(R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Commands { uint command[]; };
layout(std430, binding = 1) buffer Parameters { uint count; };
void main() {
command[0] = 6u; command[1] = 1u; command[2] = 3u; command[3] = 0u;
command[4] = 6u; command[5] = 1u; command[6] = 9u; command[7] = 0u;
count = 2u;
}
)",
m_commands, m_parameters, GL_COMMAND_BARRIER_BIT);
glBindVertexArray(m_vao);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
const Image image = Render(program, [] {
glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 0, 2, 0);
});
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 0)))
<< "command 0 did not draw (" << Half(image, 0)
<< "): the count was read from a copy the compute shader never reached";
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
}
// ---- a native indirect draw's gl_BaseVertex (DirectGLES.cpp ExecuteIndexedIndirectCommands) ----
//
// The GPU executes the command itself, so the geometry is right whatever the backend's copy
// says; gl_BaseVertex is the one field Espryt feeds from the CPU. The staged command names
// the left half (baseVertex 3), the compute shader moves it to the right half (9).
TEST_F(GpuWrittenDrawInputScenario, ABaseVertexAComputeShaderWroteReachesGlBaseVertex) {
if (!Ready()) return;
if (SkipBrokenMonolith(kUnsyncedBaseVertex, nullptr)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kBaseVertexVertex, kBaseVertexFragment);
ASSERT_NE(program, 0u);
const std::vector<GLfloat> vertices = HalvesVertices();
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
const ElementsCommand staged{kHalfCount, 1, 0, kLeftFirst, 0};
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(staged), &staged);
WriteWithCompute(R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Commands { int command[]; };
void main() { command[3] = 9; }
)",
m_commands, 0, GL_COMMAND_BARRIER_BIT);
glBindVertexArray(m_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
const Image image = Render(program, [] {
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 1)))
<< "the right half is " << Half(image, 1)
<< ": red means the draw ran with the compute-written baseVertex but gl_BaseVertex was "
"fed from the staged command; black means the draw did not run there at all";
EXPECT_FALSE(IsGreen(Half(image, 0))) << "the left half drew: the GPU saw the staged command";
}
// ---- indirect commands with no staged copy at all (DirectGLES.cpp ResolveIndirectCommandBytes) ----
//
// The command buffer is ORPHANED and only the compute shader ever writes it - the GPU-culling
// shape. Nothing was staged for it, so a backend that needs a CPU copy before it will issue
// the draw has to make one from its own GL buffer.
TEST_F(GpuWrittenDrawInputScenario, IndirectCommandsAComputeShaderWroteIntoAnOrphanedStoreDraw) {
if (!Ready()) return;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const std::vector<GLfloat> vertices = HalvesVertices();
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(ElementsCommand), nullptr);
WriteWithCompute(R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Commands { uint command[]; };
void main() { command[0] = 6u; command[1] = 1u; command[2] = 0u; command[3] = 3u; command[4] = 0u; }
)",
m_commands, 0, GL_COMMAND_BARRIER_BIT);
glBindVertexArray(m_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
const Image image = Render(program, [] {
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 0)))
<< "the compute-written command did not draw (" << Half(image, 0)
<< "): the backend refused a draw whose command buffer it had no CPU copy of";
EXPECT_FALSE(IsGreen(Half(image, 1))) << "the right half drew: the command was not the one written";
}
// ---- the other GPU writers: transform feedback, buffer images, atomic counters ----
//
// The same restart reader over indices a CAPTURE produced. Espryt maps the captured range
// back at glEndTransformFeedback for the client; the server's own copy has to take it too.
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesATransformFeedbackCapturedSplitTheStrip) {
if (!Ready()) return;
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const char* varyings[] = {"idx"};
const GLuint capture = XfbProgram(kCaptureIndicesVertex, kCaptureFragment, varyings, 1);
ASSERT_NE(capture, 0u);
BindVertices(kRestartVertices, sizeof(kRestartVertices));
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint), kRestartPoison.data());
CaptureIntoElementBuffer(capture, kRestartIndexCount);
ArmRestart();
const Image image = Render(program, [] {
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
ExpectSplitStrip(image, "glDrawElements over restart indices a transform feedback captured");
}
// ...through the SCATTER Espryt runs for a layout with holes: the server composes the range
// from its pre-capture bytes and the captured words, and its own copy has to keep the result.
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesAScatteredTransformFeedbackCapturedSplitTheStrip) {
if (!Ready()) return;
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const char* varyings[] = {"idx", "gl_SkipComponents1"};
const GLuint capture = XfbProgram(kCaptureScatteredVertex, kCaptureFragment, varyings, 2);
ASSERT_NE(capture, 0u);
BindVertices(kRestartVertices, sizeof(kRestartVertices));
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(kScatterStaged), kScatterStaged);
CaptureIntoElementBuffer(capture, 4);
ArmRestart();
const Image image = Render(program, [] {
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
ExpectSplitStrip(image, "glDrawElements over restart indices a scattered capture composed");
}
// ...and over indices IMAGE STORES wrote through a buffer texture on the element buffer.
TEST_F(GpuWrittenDrawInputScenario, RestartIndicesImageStoresWroteSplitTheStrip) {
if (!Ready()) return;
if (SkipBrokenMonolith(nullptr, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const GLuint compute = ComputeProgram(kImageStoreRestartIndices);
ASSERT_NE(compute, 0u);
BindVertices(kRestartVertices, sizeof(kRestartVertices));
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, kRestartIndexCount * sizeof(GLuint), kRestartPoison.data());
glGenTextures(1, &m_texture);
glBindTexture(GL_TEXTURE_BUFFER, m_texture);
glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_ebo);
glBindTexture(GL_TEXTURE_BUFFER, 0);
glBindImageTexture(0, m_texture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_R32UI);
glUseProgram(compute);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ELEMENT_ARRAY_BARRIER_BIT | GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R32UI);
ArmRestart();
const Image image = Render(program, [] {
glDrawElements(GL_TRIANGLE_STRIP, kRestartIndexCount, GL_UNSIGNED_INT, nullptr);
});
EXPECT_EQ(FirstGLError(), 0u);
ExpectSplitStrip(image, "glDrawElements over restart indices image stores wrote");
}
// The GPU-culling shape at its most common: the draw count is an ATOMIC COUNTER the compute
// shader bumped once per surviving command, and that counter buffer is the parameter buffer.
TEST_F(GpuWrittenDrawInputScenario, ACountAnAtomicCounterProducedDrivesMultiDrawElementsIndirectCount) {
if (!Ready()) return;
if (SkipBrokenMonolith(kUnsyncedCount, nullptr)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
const GLuint compute = ComputeProgram(R"(#version 430 core
layout(local_size_x = 1) in;
layout(binding = 0, offset = 0) uniform atomic_uint drawCount;
layout(std430, binding = 0) buffer Commands { uint command[]; };
void main() {
const uint baseVertex[2] = uint[2](3u, 9u);
for (int i = 0; i < 2; ++i) {
uint slot = atomicCounterIncrement(drawCount);
command[slot * 5u + 0u] = 6u;
command[slot * 5u + 1u] = 1u;
command[slot * 5u + 2u] = 0u;
command[slot * 5u + 3u] = baseVertex[i];
command[slot * 5u + 4u] = 0u;
}
}
)");
ASSERT_NE(compute, 0u);
const std::vector<GLfloat> vertices = HalvesVertices();
BindVertices(vertices.data(), static_cast<GLsizeiptr>(vertices.size() * sizeof(GLfloat)));
const GLuint indices[kHalfCount] = {0, 1, 2, 3, 4, 5};
Store(m_ebo, GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices);
const ElementsCommand zeroCommands[2] = {};
Store(m_commands, GL_DRAW_INDIRECT_BUFFER, sizeof(zeroCommands), zeroCommands);
const GLuint zeroCount = 0;
Store(m_parameters, GL_ATOMIC_COUNTER_BUFFER, sizeof(zeroCount), &zeroCount);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, m_parameters);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_commands);
glUseProgram(compute);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_ATOMIC_COUNTER_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
glUseProgram(0);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindVertexArray(m_vao);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_commands);
glBindBuffer(GL_PARAMETER_BUFFER, m_parameters);
const Image image = Render(program, [] {
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 2, 0);
});
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 0)))
<< "command 0 did not draw (" << Half(image, 0)
<< "): the count was read from a copy the atomic counter never reached";
EXPECT_TRUE(IsGreen(Half(image, 1))) << "command 1 did not draw (" << Half(image, 1) << ")";
}
// ---- fp64 vertex narrowing (Managers.cpp SyncFloat64AttributeAsFloat32ByHandle) ----
//
// A GL_DOUBLE vertex array is narrowed to float on the CPU from the source buffer's bytes,
// and the narrowed copy is memoised on the source's serial - which a GPU write never moves.
// Two rounds: the first catches a stale read, the second a stale memo. The compute shader
// writes each double as its two IEEE-754 words (low word 0), so it needs no fp64 itself.
TEST_F(GpuWrittenDrawInputScenario, DoubleVerticesAComputeShaderWroteAreNarrowedFromTheWrittenBytes) {
if (!Ready()) return;
if (SkipBrokenMonolith(kUnsyncedDoubles, kSyncMidRecording)) GTEST_SKIP() << m_skipReason;
const GLuint program = Program(kFlatVertex, kFlatFragment);
ASSERT_NE(program, 0u);
// Four vertices, x then y, all 100.0: a strip wholly off the surface.
const double poison[8] = {100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0};
glBindVertexArray(m_vao);
Store(m_vbo, GL_ARRAY_BUFFER, sizeof(poison), poison);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_DOUBLE, GL_FALSE, 2 * sizeof(double), nullptr);
// High words: -1.0 = 0xBFF00000, 1.0 = 0x3FF00000, 0.0 = 0.
const auto writeQuad = [&](const char* highWords) {
const std::string source = std::string(R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Positions { uint word[]; };
void main() {
const uint hi[8] = uint[8]()") + highWords + R"();
for (int i = 0; i < 8; ++i) { word[2 * i] = 0u; word[2 * i + 1] = hi[i]; }
}
)";
WriteWithCompute(source.c_str(), m_vbo, 0, GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT);
};
// Round 1: the whole surface, (-1,-1) (1,-1) (-1,1) (1,1).
writeQuad("0xBFF00000u, 0xBFF00000u, 0x3FF00000u, 0xBFF00000u, 0xBFF00000u, 0x3FF00000u, "
"0x3FF00000u, 0x3FF00000u");
Image image = Render(program, [] { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); });
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 0)) && IsGreen(Half(image, 1)))
<< "round 1: the quad is " << Half(image, 0) << " / " << Half(image, 1)
<< " - black means the doubles were narrowed from the staged poison, not the written bytes";
// Round 2: only the left half, (-1,-1) (0,-1) (-1,1) (0,1).
writeQuad("0xBFF00000u, 0xBFF00000u, 0u, 0xBFF00000u, 0xBFF00000u, 0x3FF00000u, 0u, 0x3FF00000u");
image = Render(program, [] { glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); });
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_TRUE(IsGreen(Half(image, 0))) << "round 2: the left half is " << Half(image, 0);
EXPECT_FALSE(IsGreen(Half(image, 1)))
<< "round 2: the right half still drew - the narrowed copy of round 1 was reused after the "
"second GPU write";
}
} // namespace MGITest
@@ -0,0 +1,591 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/ImageBindableRemintScenario.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
//
// Scenario - THE FIRST glBindImageTexture OF A TEXTURE THE GPU HAS ALREADY WRITTEN.
//
// ES only allows immutable storage on an image unit, and a few formats need a wider carrier on
// top (TextureImpl::GetImageBindableStorageWidening), so Espryt re-mints a texture the first time
// it is image-bound after it already has driver storage (RequireImageBindableStorage). The new
// storage has to hold what the old one held. The re-mint used to fill it from the frontend's
// shadow - the CLIENT's copy of each level - and the shadow never sees a GPU write: a render or
// clear into the level, a glGenerateMipmap, a null-data definition only the GPU ever filled. Every
// such level came back as the bytes the application last uploaded, or zeroes. That is the
// ~26.9k-pixel iris-derivative difference: an RGBA16F render target, first image-bound after it
// was rendered into.
//
// Every case gives a texture its content by one means, lets the backend create its storage (a
// sampling draw, or the FBO attach of a GPU clear), writes it on the GPU where the case says so,
// and then binds it to an image unit for the first time: a compute imageLoad copies the level into
// a destination that was image-bound before it was ever synced (so the destination itself never
// re-mints), and the destination is read back through an FBO. The expectation is the GL answer.
//
// The controls are the other half: levels the GPU never wrote (including formats whose driver
// readback may be refused), levels the driver does not have yet because they were defined after
// the last sync, and a pending client write that has to land on top of GPU content. Magma never
// re-mints from a shadow (it copies image to image) and is the control backend.
#include <array>
#include <cstdint>
#include <cstdlib>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kEdge = 8;
constexpr const char* kVS = R"(#version 430 core
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
)";
constexpr const char* kSampleFS = R"(#version 430 core
uniform sampler2D u_tex;
out vec4 o_color;
void main() { o_color = texelFetch(u_tex, ivec2(gl_FragCoord.xy) % 4, 0); }
)";
// Copies an image into an RGBA8 image with imageLoad; a signed-normalized source is mapped
// to [0, 1] so it survives the unsigned destination.
std::string CopyCS(const char* srcFormat, bool snorm) {
std::string s = "#version 430 core\nlayout(local_size_x = 8, local_size_y = 8) in;\n";
s += std::string("layout(") + srcFormat + ", binding = 0) readonly uniform image2D u_src;\n";
s += "layout(rgba8, binding = 1) writeonly uniform image2D u_dst;\n";
s += "void main() { ivec2 p = ivec2(gl_GlobalInvocationID.xy); vec4 v = imageLoad(u_src, p);\n";
if (snorm) s += " v = v * 0.5 + 0.5;\n";
s += " imageStore(u_dst, p, v); }\n";
return s;
}
std::vector<GLubyte> Pattern(int edge, GLubyte magic) {
std::vector<GLubyte> t(static_cast<std::size_t>(edge * edge) * 4u);
for (int y = 0; y < edge; ++y)
for (int x = 0; x < edge; ++x) {
const std::size_t at = static_cast<std::size_t>((y * edge + x) * 4);
t[at + 0] = magic;
t[at + 1] = static_cast<GLubyte>(16 * (x + 1));
t[at + 2] = static_cast<GLubyte>(16 * (y + 1));
t[at + 3] = 0xFF;
}
return t;
}
std::vector<GLubyte> Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) {
std::vector<GLubyte> t(static_cast<std::size_t>(edge * edge) * 4u);
for (std::size_t i = 0; i < t.size(); i += 4) {
t[i] = r;
t[i + 1] = g;
t[i + 2] = b;
t[i + 3] = a;
}
return t;
}
const GLfloat kGreen[4] = {0.0f, 1.0f, 0.0f, 1.0f};
class ImageBindableRemintScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
std::string error;
m_sampleProgram = CompileProgram(kVS, kSampleFS, &error);
ASSERT_NE(m_sampleProgram, 0u) << error;
m_scratch = MakeColorFbo(4, 4);
glGenVertexArrays(1, &m_vao);
FirstGLError();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
if (m_sampleProgram) glDeleteProgram(m_sampleProgram);
if (m_vao) glDeleteVertexArrays(1, &m_vao);
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
BindDefaultFramebuffer();
DestroyColorFbo(m_scratch);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
GLuint NewTexture() {
GLuint t = 0;
glGenTextures(1, &t);
m_textures.push_back(t);
return t;
}
static void Nearest(GLenum target, GLuint tex, int maxLevel) {
glBindTexture(target, tex);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, maxLevel);
glBindTexture(target, 0);
}
// A draw that samples the texture: the backend now holds storage for it.
void SyncBySampling(GLuint tex) {
BindFbo(m_scratch);
glUseProgram(m_sampleProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex);
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_tex"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
BindDefaultFramebuffer();
glFinish();
}
// A GPU write the frontend's shadow never sees. A layer >= 0 names one layer of an
// array texture.
void GpuClear(GLuint tex, int level, const GLfloat* color, int layer = -1) {
if (!m_fbo) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
if (layer >= 0) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, level, layer);
} else {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, level);
}
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE))
<< "the clear target is incomplete";
glClearBufferfv(GL_COLOR, 0, color);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
}
// The first image binding of (src, level[, layer]): an imageLoad copy into a fresh RGBA8
// destination, read back through an FBO.
std::vector<GLubyte> ImageCopy(GLuint src, int level, GLenum unitFormat, const char* glslFormat,
bool snorm = false, int layer = 0) {
const GLuint dst = NewTexture();
glBindTexture(GL_TEXTURE_2D, dst);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glBindTexture(GL_TEXTURE_2D, 0);
glBindImageTexture(1, dst, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
glBindImageTexture(0, src, level, GL_FALSE, layer, GL_READ_ONLY, unitFormat);
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture";
const std::string cs = CopyCS(glslFormat, snorm);
const char* csp = cs.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &csp, nullptr);
glCompileShader(shader);
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
EXPECT_EQ(linked, GL_TRUE) << "the copy program did not link";
glUseProgram(program);
// P11 M: A BARRIER THE SPEC DOES NOT ASK FOR, pinned for the reason
// TextureRemintPullScenario's twin gives (ID-P9-11, DEBTS "待归因"): on llvmpipe the
// kept-allocation case (GpuClearOverAnImmutableUploadSurvives - no re-mint to
// serialize anything) read the pre-clear upload once on the push-monolith arm under
// load, the same shape P9 measured 3/30 on the split arm. The cases ask what the
// storage HOLDS after the first image binding, not how a clear orders against an
// image load, so the ordering is pinned here.
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(0);
glDeleteProgram(program);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
if (!m_fbo) glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst, 0);
std::vector<GLubyte> out(static_cast<std::size_t>(kEdge * kEdge) * 4u, 0xA5);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, out.data());
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
EXPECT_EQ(FirstGLError(), 0u) << "copy/readback";
return out;
}
// The edge x edge corner of `got` against an RGBA8 expectation, the first `channels`
// channels only, within 2 codes (a normalized value's round trip).
static void ExpectImage(const std::vector<GLubyte>& got, int edge, const std::vector<GLubyte>& want,
int channels = 4) {
int bad = 0;
std::ostringstream first;
for (int y = 0; y < edge; ++y)
for (int x = 0; x < edge; ++x) {
const std::size_t g = static_cast<std::size_t>((y * kEdge + x) * 4);
const std::size_t w = static_cast<std::size_t>((y * edge + x) * 4);
bool texelBad = false;
for (int c = 0; c < channels; ++c)
texelBad |= std::abs(int(got[g + c]) - int(want[w + c])) > 2;
if (texelBad && bad++ == 0) {
first << "(" << x << "," << y << ") got " << int(got[g]) << "," << int(got[g + 1]) << ","
<< int(got[g + 2]) << "," << int(got[g + 3]) << " want " << int(want[w]) << ","
<< int(want[w + 1]) << "," << int(want[w + 2]) << "," << int(want[w + 3]);
}
}
EXPECT_EQ(bad, 0) << bad << " of " << edge * edge << " texels differ; first " << first.str();
}
// One glTexSubImage of opaque blue per {x, y, w, h} box, left pending.
static void WriteBlueBoxes(GLuint tex, const std::vector<std::array<int, 4>>& boxes) {
glBindTexture(GL_TEXTURE_2D, tex);
for (const auto& b : boxes) {
std::vector<GLubyte> blue(static_cast<std::size_t>(b[2] * b[3]) * 4u, 0);
for (std::size_t i = 0; i < blue.size(); i += 4) {
blue[i + 2] = 255;
blue[i + 3] = 255;
}
glTexSubImage2D(GL_TEXTURE_2D, 0, b[0], b[1], b[2], b[3], GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
}
// kEdge x kEdge green, blue inside the boxes.
static std::vector<GLubyte> GreenWithBlueBoxes(const std::vector<std::array<int, 4>>& boxes) {
auto want = Solid(kEdge, 0, 255, 0, 255);
for (const auto& b : boxes)
for (int y = b[1]; y < b[1] + b[3]; ++y)
for (int x = b[0]; x < b[0] + b[2]; ++x) {
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
want[at] = 0;
want[at + 1] = 0;
want[at + 2] = 255;
want[at + 3] = 255;
}
return want;
}
GLuint m_sampleProgram = 0, m_vao = 0, m_fbo = 0;
ColorFbo m_scratch{};
std::vector<GLuint> m_textures;
};
// Control: the shadow and the driver agree.
TEST_F(ImageBindableRemintScenario, UploadedLevelKeepsItsUpload) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, p);
}
TEST_F(ImageBindableRemintScenario, GpuClearOverAnUploadSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
// Immutable in a core image format: the storage is already what an image unit needs.
TEST_F(ImageBindableRemintScenario, GpuClearOverAnImmutableUploadSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
// A null-data definition only the GPU ever filled: the shadow holds zeroes.
TEST_F(ImageBindableRemintScenario, GpuClearOfANullDefinitionSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
// A client write still pending when the bind comes lands on top of the GPU's content,
// and only inside its own box.
TEST_F(ImageBindableRemintScenario, PendingSubImageLandsOnTheGpuClear) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
const auto blue = Solid(2, 0, 0, 255, 255);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
auto want = Solid(kEdge, 0, 255, 0, 255);
for (int y = 2; y < 4; ++y)
for (int x = 2; x < 4; ++x) {
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
want[at] = 0;
want[at + 1] = 0;
want[at + 2] = 255;
want[at + 3] = 255;
}
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, want);
}
// Several pending writes: only their texels land on the GPU's clear, never the gaps between
// them. The re-mint merged the union box of the shadow over the readback whenever the rect
// list was withheld, and kept the shadow outright when that box spanned the level.
// Two 3x7 columns with a one-texel gap: 42 of their union's 49 texels, past the 3/4 at which
// the storage withholds its rect list.
TEST_F(ImageBindableRemintScenario, PendingSubImagesFillingThreeQuartersOfTheirUnionLandOnTheGpuClear) {
if (!Ready()) return;
const std::vector<std::array<int, 4>> boxes = {{0, 0, 3, 7}, {4, 0, 3, 7}};
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
WriteBlueBoxes(t, boxes);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, GreenWithBlueBoxes(boxes));
}
// Three quadrants: the union box is the whole level; the fourth quadrant is the GPU's.
TEST_F(ImageBindableRemintScenario, PendingSubImagesWhoseUnionSpansTheLevelLandOnTheGpuClear) {
if (!Ready()) return;
const std::vector<std::array<int, 4>> boxes = {{0, 0, 4, 4}, {4, 0, 4, 4}, {0, 4, 4, 4}};
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
GpuClear(t, 0, kGreen);
SyncBySampling(t);
WriteBlueBoxes(t, boxes);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, GreenWithBlueBoxes(boxes));
}
TEST_F(ImageBindableRemintScenario, GeneratedMipLevelSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto s = Solid(kEdge, 200, 100, 50, 255);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, s.data());
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), kEdge / 2, Solid(kEdge / 2, 200, 100, 50, 255));
}
TEST_F(ImageBindableRemintScenario, GpuClearOfAnUploadedMipLevelSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto s0 = Solid(kEdge, 200, 100, 50, 255);
const auto s1 = Solid(kEdge / 2, 10, 20, 30, 255);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, s0.data());
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, s1.data());
Nearest(GL_TEXTURE_2D, t, 1);
SyncBySampling(t);
GpuClear(t, 1, kGreen);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), kEdge / 2, Solid(kEdge / 2, 0, 255, 0, 255));
}
// Control: a level defined (null data) after the last sync is not on the driver yet, so
// there is nothing to read back for it; the bound level 0 must still be the upload.
TEST_F(ImageBindableRemintScenario, LevelDefinedAfterTheSyncDoesNotDisturbTheOthers) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8"), kEdge, p);
}
// Control: the same late level, partly uploaded - the shadow is the only copy of it.
TEST_F(ImageBindableRemintScenario, LevelDefinedAndUploadedAfterTheSyncKeepsTheUpload) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const auto blue = Solid(2, 0, 0, 255, 255);
glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectImage(ImageCopy(t, 1, GL_RGBA8, "rgba8"), 2, blue);
}
// GL_RG8 is not an ESSL image format, so the re-mint also WIDENS the storage (to RGBA8)
// and cannot keep the old allocation even though it is immutable.
TEST_F(ImageBindableRemintScenario, GpuClearSurvivesAWideningRemint) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u, 0x33);
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
const GLfloat c[4] = {0.5f, 0.25f, 0.0f, 1.0f};
GpuClear(t, 0, c);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RG8, "rg8"), kEdge, Solid(kEdge, 128, 64, 0, 255), 2);
}
// Control: widened, uploaded only.
TEST_F(ImageBindableRemintScenario, UploadSurvivesAWideningRemint) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u);
for (std::size_t i = 0; i < rg.size(); i += 2) {
rg[i] = 0x33;
rg[i + 1] = 0x99;
}
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RG8, "rg8"), kEdge, Solid(kEdge, 0x33, 0x99, 0, 255), 2);
}
// Control: a signed-normalized level (not colour-renderable on every ES driver, so its
// readback may be refused and the shadow used instead), uploaded only.
TEST_F(ImageBindableRemintScenario, SnormUploadSurvives) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLbyte> s(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < s.size(); i += 4) {
s[i] = 127;
s[i + 1] = 0;
s[i + 2] = -127;
s[i + 3] = 127;
}
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, kEdge, kEdge, 0, GL_RGBA, GL_BYTE, s.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA8_SNORM, "rgba8_snorm", true), kEdge, Solid(kEdge, 255, 128, 0, 255),
3);
}
// Control: RGBA16_SNORM is desktop-only as an image format, so this re-mint widens too.
TEST_F(ImageBindableRemintScenario, Snorm16UploadSurvivesAWideningRemint) {
if (!Ready()) return;
const GLuint t = NewTexture();
std::vector<GLshort> s(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < s.size(); i += 4) {
s[i] = 32767;
s[i + 1] = 0;
s[i + 2] = -32767;
s[i + 3] = 32767;
}
glBindTexture(GL_TEXTURE_2D, t);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA16_SNORM, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_SHORT, s.data());
Nearest(GL_TEXTURE_2D, t, 0);
SyncBySampling(t);
ExpectImage(ImageCopy(t, 0, GL_RGBA16_SNORM, "rgba16_snorm", true), kEdge,
Solid(kEdge, 255, 128, 0, 255), 3);
}
// An array texture is read back one layer at a time: the GPU-cleared layer and the
// uploaded one both have to come through. The clear's FBO attach is what gives the
// texture its driver storage here.
TEST_F(ImageBindableRemintScenario, GpuClearOfOneArrayLayerSurvivesBesideTheUploadedLayer) {
if (!Ready()) return;
const GLuint t = NewTexture();
const auto p = Pattern(kEdge, 0x40);
std::vector<GLubyte> layers(p);
layers.insert(layers.end(), p.begin(), p.end());
glBindTexture(GL_TEXTURE_2D_ARRAY, t);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, kEdge, kEdge, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
layers.data());
glBindTexture(GL_TEXTURE_2D_ARRAY, 0);
Nearest(GL_TEXTURE_2D_ARRAY, t, 0);
GpuClear(t, 0, kGreen, /*layer=*/1);
glFinish();
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8", false, /*layer=*/1), kEdge, Solid(kEdge, 0, 255, 0, 255));
ExpectImage(ImageCopy(t, 0, GL_RGBA8, "rgba8", false, /*layer=*/0), kEdge, p);
}
// A view owns no storage, so its first image binding is the VIEWED texture's transition,
// and the GPU's texels there have to come through it. Needs glTextureView, which Espryt
// only has where the ES driver does (not on llvmpipe).
TEST_F(ImageBindableRemintScenario, GpuClearSurvivesTheFirstImageBindingOfAView) {
if (!Ready()) return;
const GLuint owner = NewTexture();
const auto p = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, owner);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, p.data());
Nearest(GL_TEXTURE_2D, owner, 0);
SyncBySampling(owner);
GpuClear(owner, 0, kGreen);
SyncBySampling(owner);
const GLuint view = NewTexture();
FirstGLError();
glTextureView(view, GL_TEXTURE_2D, owner, GL_RGBA8, 0, 1, 0, 1);
if (const GLenum error = FirstGLError()) {
GTEST_SKIP() << "glTextureView is unavailable on backend " << Gl().BackendName() << " ("
<< GLErrorName(error) << ")";
}
Nearest(GL_TEXTURE_2D, view, 0);
SyncBySampling(view);
ExpectImage(ImageCopy(view, 0, GL_RGBA8, "rgba8"), kEdge, Solid(kEdge, 0, 255, 0, 255));
}
} // namespace
} // namespace MGITest
@@ -49,8 +49,10 @@
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PersistentMapPeek.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitLane.h"
#ifdef GLAPI
#undef GLAPI
@@ -94,6 +96,12 @@ void main() { word = 0xC0FFEEu; }
// Set by the MapPersistentRoundtrips. ctest entry and by nothing else; a harness marker,
// never read by the library.
constexpr const char* kLaneMarker = "MGITEST_MPR_LANE";
// P11 A1: set (to the tier the lane names, "T0") only by the `.AdoptTier0.` tcp entries,
// which also set MOBILEGL_IPC_ADOPT_TIER=0; a harness marker, never read by the library.
constexpr const char* kAdoptTierRefusalMarker = "MGITEST_ADOPT_TIER_REFUSAL";
// P11 B2: set by the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries to the reason word the
// server gives for not granting T0 ("no AHardwareBuffer" on a host, "disallowed" with its allow switch off).
constexpr const char* kAdoptT0FallbackMarker = "MGITEST_ADOPT_T0_FALLBACK";
// Draws issued against the arena inside the counted window. One definition, many draws:
// "one per definition" (1) and "one per draw" (kDrawsInTheWindow) have to be different
// numbers or the assertion cannot tell them apart.
@@ -260,6 +268,27 @@ void main() { word = 0xC0FFEEu; }
return px;
}
// P11 A2: the arena's tier as the frontend object records it, against the lane's
// declaration. Adopted = the resource owner minted a persistent coherent mapping and
// the CPU shadow was released; emulated = the owner declined (T2) and the shadow is
// the truth. Pixels cannot tell them apart - every case above is green on both.
void ExpectDeclaredArm(const std::string& declared, const char* when) {
bool adopted = false;
ASSERT_TRUE(PeekBufferIsAdoptedPersistentMap(m_arena, &adopted))
<< "no frontend BufferObject behind the arena " << when;
const std::string landed = adopted ? "adopted" : "emulated";
RecordProperty(std::string("arena_arm_") + (m_armPeeks++ == 0 ? "defined" : "used"),
landed.c_str());
EXPECT_EQ(declared, landed)
<< "the lane declared the " << declared << " arm and the " << (kArenaBytes >> 20)
<< " MiB arena is " << landed << " " << when
<< ". Monolith lanes adopt on both backends (Espryt's handle arm mints through "
"Ops_H_MapPersistentTracked, Magma's legacy AcquirePersistentMap); split lanes "
"run T2, where every acquisition declines.";
}
int m_armPeeks = 0;
unsigned int m_program = 0;
unsigned int m_compute = 0;
unsigned int m_vao = 0;
@@ -293,6 +322,44 @@ void main() { word = 0xC0FFEEu; }
EXPECT_LT(px[0], 50) << "the draw still shows the previous frame's bytes";
}
// P11 B2: THE SAME REWRITE WITH NO READBACK BETWEEN THE TWO DRAWS. The case above reads
// pixels after the first draw, which retires it before the SubData, so a store that took the
// write IN PLACE (straight into pages the GPU reads - a T0 import is the client's
// AHardwareBuffer, an adopted store is a coherent map) would still pass it. Here the first
// draw is only queued when the arena is rewritten: it must still fetch the bytes it was
// issued against (red, left half) and the second draw the new ones (green, right half).
TEST_F(LargeArenaAdoptionScenario, ADrawQueuedBeforeASubDataKeepsItsOwnBytes) {
if (!Ready() || IsSkipped()) return;
const GLint w = Gl().Width();
const GLint h = Gl().Height();
UploadQuad(1.f, 0.f, 0.f);
glViewport(0, 0, w, h);
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, w / 2, h);
glDrawArrays(GL_TRIANGLES, 0, 6);
UploadQuad(0.f, 1.f, 0.f);
glScissor(w / 2, 0, w - w / 2, h);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisable(GL_SCISSOR_TEST);
std::array<unsigned char, 4> left = {0, 0, 0, 0};
std::array<unsigned char, 4> right = {0, 0, 0, 0};
glReadPixels(w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, left.data());
glReadPixels(w / 2 + w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, right.data());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_GT(left[0], 200) << "the draw queued BEFORE the rewrite lost its bytes: the SubData reached "
"pages that draw had not read yet (a write that landed in place instead "
"of GPU-ordered)";
EXPECT_LT(left[1], 50) << "the draw queued before the rewrite fetched the rewritten bytes";
EXPECT_GT(right[1], 200) << "the draw after the rewrite did not see the new bytes";
EXPECT_LT(right[0], 50) << "the draw after the rewrite still fetched the old bytes";
}
// Respecifying a frontend buffer preserves its VAO attachments even when the
// backend replaces the adopted store's GL name. Keep every attribute binding
// unchanged so a stale backend VAO cannot be repaired by a frontend rebind.
@@ -341,6 +408,44 @@ void main() { word = 0xC0FFEEu; }
glDeleteVertexArrays(1, &otherVao);
}
// P11 B2: A RESPECIFY WHILE A DRAW OF THE OLD STORE IS STILL QUEUED. The case above reads pixels
// before every respecify, so the old store is retired by then and a server that freed it at
// once would still pass. Here the first draw is only queued when the arena is redefined: the
// old store (a T0 import: the client's AHardwareBuffer, which the client lets go of at this
// very respecify) must outlive it, and the draw must show the bytes it was issued against.
TEST_F(LargeArenaAdoptionScenario, ARespecifyWithADrawQueuedKeepsThatDrawsStore) {
if (!Ready() || IsSkipped()) return;
const GLint w = Gl().Width();
const GLint h = Gl().Height();
UploadQuad(1.f, 0.f, 0.f);
glViewport(0, 0, w, h);
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, w / 2, h);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glBufferData(GL_ARRAY_BUFFER, kArenaBytes, nullptr, GL_DYNAMIC_DRAW);
UploadQuad(0.f, 1.f, 0.f);
glScissor(w / 2, 0, w - w / 2, h);
glDrawArrays(GL_TRIANGLES, 0, 6);
glDisable(GL_SCISSOR_TEST);
std::array<unsigned char, 4> left = {0, 0, 0, 0};
std::array<unsigned char, 4> right = {0, 0, 0, 0};
glReadPixels(w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, left.data());
glReadPixels(w / 2 + w / 4, h / 2, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, right.data());
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_GT(left[0], 200) << "the draw queued before the respecify lost the store it was issued "
"against (freed before that draw ran)";
EXPECT_LT(left[1], 50) << "the draw queued before the respecify fetched the new store's bytes";
EXPECT_GT(right[1], 200) << "the draw after the respecify did not fetch the new store";
EXPECT_LT(right[0], 50) << "the draw after the respecify still fetched the old store";
}
TEST_F(LargeArenaAdoptionScenario, RespecifiedIndexArenaKeepsVaoBinding) {
if (!Ready() || IsSkipped()) return;
@@ -505,4 +610,140 @@ void main() { word = 0xC0FFEEu; }
<< window.line;
}
// P11 A2 (MG_Remote/CONTRACT-P11.md §2): WHICH TIER THE ARENA REALLY LANDED IN. The cases above
// are green whether the arena adopted or stayed emulated, so the lane declares the arm
// (MGITEST_PERSISTENT_MAP_ARM) and this case asserts it, the way
// PersistentCoherentMapScenario.TheMapLandsInTheArmItsLaneDeclares does for a sub-16-MiB map.
// Asked twice: after the NULL-data definition (TryAdoptLargeStorage's door) and after a frame
// of use that ends in an SSBO binding and a dispatch (the storage-binding door,
// EnsureGpuResidentStorage, which Magma's server walk opens). Lanes that declare nothing - the
// ambient entries, which run under whatever transport the job exports - skip.
TEST_F(LargeArenaAdoptionScenario, TheArenaLandsInTheTierItsLaneDeclares) {
if (!Ready() || IsSkipped()) return;
const std::string declared = SplitLane::DeclaredPersistentMapArm();
if (declared.empty()) {
GTEST_SKIP() << "this lane declares no arm: MGITEST_PERSISTENT_MAP_ARM is set only by the "
"lanes that pin their transport (monolith: adopted; the split arms: "
"emulated)";
}
if (!PersistentMapPeekAvailable()) {
GTEST_SKIP() << "this lane declares the " << declared << " arm but Harness/PersistentMapPeek "
"cannot look in this build (on Android this module links the shipping "
"library, built -fvisibility=hidden). 'Could not look' is not 'it was "
<< declared << "'.";
}
ExpectDeclaredArm(declared, "right after its NULL-data definition");
if (HasFatalFailure()) return;
UploadQuad(1.f, 0.f, 0.f);
DrawQuad();
EXPECT_GT(CenterPixel()[0], 200) << "the draw from the arena never landed";
Gl().EndFrame();
GLint maxComputeStorageBlocks = 0;
glGetIntegerv(GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, &maxComputeStorageBlocks);
if (maxComputeStorageBlocks >= 1) {
const GLuint compute = CompileStage(GL_COMPUTE_SHADER, kMarkerComputeSource);
ASSERT_NE(compute, 0u) << m_buildLog;
m_compute = glCreateProgram();
glAttachShader(m_compute, compute);
glLinkProgram(m_compute);
glDeleteShader(compute);
glBindBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_arena, 0, sizeof(unsigned int));
glUseProgram(m_compute);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT | GL_BUFFER_UPDATE_BARRIER_BIT);
unsigned int marker = 0;
glBindBuffer(GL_ARRAY_BUFFER, m_arena);
glGetBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(marker), &marker);
EXPECT_EQ(marker, 0xC0FFEEu) << "the dispatch through the arena's SSBO binding never landed";
} else {
RecordProperty("arena_arm_used", "not asked: no compute shader storage blocks");
}
EXPECT_EQ(FirstGLError(), 0u);
ExpectDeclaredArm(declared, "after a frame of draws, an SSBO binding and a dispatch");
}
// P11 A1 (MG_Remote/CONTRACT-P11.md §1): THE TCP LANE WITH MOBILEGL_IPC_ADOPT_TIER=0. A stream
// data plane has no memory the two sides share, so the client refuses the knob by name at its
// handshake - exactly one `Refuse{AdoptTierOnStream, "T0"}` line in its log - and the session
// runs T2: the other cases in the same `.AdoptTier0.` lane are green and the arena is emulated.
// Before A1 this lane ran T2 silently. Everywhere else it skips.
TEST_F(LargeArenaAdoptionScenario, AStreamSessionRefusesTheAdoptTierOnceByName) {
if (!Ready() || IsSkipped()) return;
const char* tier = std::getenv(kAdoptTierRefusalMarker);
if (tier == nullptr || tier[0] == '\0') {
GTEST_SKIP() << "runs only in the `.AdoptTier0.` tcp entries, which set "
"MOBILEGL_IPC_ADOPT_TIER=0 and " << kAdoptTierRefusalMarker << "=T0";
}
if (PipeStatsWindow::LibraryLogPath().empty()) {
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the client's log is "
"where the refusal is written";
}
// A frame of the arena's work first: a refusal is only worth reading from a session that
// then ran - and an armed split lane requires the case to move the record ordinal anyway.
UploadQuad(0.f, 1.f, 0.f);
DrawQuad();
EXPECT_GT(CenterPixel()[1], 200) << "the session that refused the tier did not render";
EXPECT_EQ(FirstGLError(), 0u);
Gl().EndFrame();
const std::string log = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
const std::string line = std::string("Refuse{AdoptTierOnStream, \"") + tier + "\"}";
std::size_t count = 0;
for (auto at = log.find(line); at != std::string::npos; at = log.find(line, at + 1)) ++count;
RecordProperty("adopt_tier_refusals", static_cast<int>(count));
EXPECT_EQ(count, 1u) << "the client names MOBILEGL_IPC_ADOPT_TIER on a stream exactly once, at "
"its handshake (" << line << "), in " << PipeStatsWindow::LibraryLogPath();
EXPECT_EQ(log.find("Fatal{UnimplementedAdoptTier"), std::string::npos) << log;
}
// P11 B2 (MG_Remote/CONTRACT-P11.md B2): A SHARED-SEGMENT SESSION THAT ASKS T0 AND CANNOT RUN IT.
// MOBILEGL_IPC_ADOPT_TIER=0 over shared segments asks T0 in the Hello (A1 made it die at the
// handshake; ruling ID-P11-14 made it this). The lane's server cannot grant it - a host has no
// AHardwareBuffer, or `.AdoptT0Disallowed.` turned its allow switch off - so it refuses by name
// at its first native bind, publishes no kCapAdoptT0, and the client falls back to T2 with ONE
// named line at its first map_persistent. Never a Fatal; the other cases in the lane are green
// and the arena is emulated. Everywhere else it skips.
TEST_F(LargeArenaAdoptionScenario, ASessionThatCannotRunT0FallsBackToT2OnceByName) {
if (!Ready() || IsSkipped()) return;
const char* why = std::getenv(kAdoptT0FallbackMarker);
if (why == nullptr || why[0] == '\0') {
GTEST_SKIP() << "runs only in the shared-segment `.AdoptTier0.` / `.AdoptT0Disallowed.` entries, "
"which set MOBILEGL_IPC_ADOPT_TIER=0 and " << kAdoptT0FallbackMarker;
}
if (PipeStatsWindow::LibraryLogPath().empty()) {
GTEST_SKIP() << "the lane configured no MOBILEGL_LOG_FILE_PATH, and the two logs are where the "
"fallback is written";
}
// A frame of the arena's work first: the fallback is only worth reading from a session that
// then ran on T2 - and the client decides at its first map_persistent, which SetUp's
// definition of the arena has already made.
UploadQuad(0.f, 1.f, 0.f);
DrawQuad();
EXPECT_GT(CenterPixel()[1], 200) << "the session that fell back to T2 did not render";
EXPECT_EQ(FirstGLError(), 0u);
Gl().EndFrame();
const auto countOf = [](const std::string& text, const std::string& needle) {
std::size_t n = 0;
for (auto at = text.find(needle); at != std::string::npos; at = text.find(needle, at + 1)) ++n;
return n;
};
const std::string client = PipeStatsWindow::ReadWholeFile(PipeStatsWindow::LibraryLogPath());
const std::string server = PipeStatsWindow::ReadServerLogSince(PipeStatsWindow::LogMark{});
const std::string clientLine = "Refuse{AdoptT0Unavailable, \"no kCapAdoptT0\"}";
const std::string serverLine = std::string("Refuse{AdoptT0Unavailable, \"") + why + "\"}";
RecordProperty("t0_client_fallbacks", static_cast<int>(countOf(client, clientLine)));
RecordProperty("t0_server_refusals", static_cast<int>(countOf(server, serverLine)));
EXPECT_EQ(countOf(client, clientLine), 1u)
<< "the client names a T0 it asked for and cannot run exactly once, at its first map_persistent ("
<< clientLine << "), in " << PipeStatsWindow::LibraryLogPath();
EXPECT_EQ(countOf(server, serverLine), 1u)
<< "the server names why it did not grant T0 exactly once, at its first native bind (" << serverLine
<< "), in " << PipeStatsWindow::ServerLibraryLogPath();
EXPECT_EQ(client.find("Fatal{"), std::string::npos) << client;
EXPECT_EQ(server.find("Fatal{"), std::string::npos) << server;
}
} // namespace MGITest
@@ -0,0 +1,765 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/OversizedRecordingSplitScenario.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
//
// Scenario - A FRAME WITH MORE DRAWS THAN ONE COMMAND BUFFER MAY HOLD IS SPLIT, AND LOSES NOTHING.
//
// DirectVulkan records a frame into one command buffer, and used to submit it only at Present.
// A driver keeps a command buffer's GPU memory until the buffer is freed, and the Adreno 830
// driver maps that memory into the process in 16 KiB chunks. Minecraft rd12's loading frame
// records 981,654 draws (3.1M commands) into one buffer. It grew to ~40k such mappings and
// aborted inside the driver's own calloc once vm.max_map_count (65530) was spent. The renderer
// now submits the open buffer after MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER draws and goes
// on recording into a fresh one (VulkanRenderer::SplitOversizedRecording).
//
// The first case writes each pixel of a 32x32 target with its own draw and its own uniform
// value, so it pins that no draw is lost and no per-draw state is left behind on either side of
// a split. The rest put GL state that one draw leaves for a later one ACROSS the split: a depth
// occluder, a stencil mark, a blend destination, the samples of a multisample target, an
// occlusion or primitives-generated query that is still open, and a transform-feedback capture
// that is still active. Each draws its "before" part, then kFillers draws, then its "after"
// part. kFillers is more than twice the SplitRecording lane's budget of 64, so at least one
// split lands between the two parts wherever the recording's count stood.
//
// The ambient registrations run the same draws unsplit, because the default budget is far above
// any of these frames; that is the control. The DirectVulkan.SplitRecording. entry pins the
// budget to 64, and there every case also asserts the renderer's latched MGLOG_I - the one
// assertion that is red on a renderer that never splits.
#include <algorithm>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kSide = 32;
constexpr int kDraws = kSide * kSide;
// More than twice the SplitRecording lane's budget (64): wherever the count stands when
// they start, at least one split lands among them.
constexpr int kFillers = 150;
// Draws inside the query and capture spans: also more than twice the budget.
constexpr int kSpanDraws = 200;
// aPos spans the unit square; the uniform moves it onto cell (i % 32, i / 32) of a
// 32x32 grid, which is exactly one pixel of a 32x32 target.
constexpr const char* kVS = R"(#version 330 core
in vec2 aPos;
uniform int uIndex;
void main() {
vec2 cell = vec2(float(uIndex % 32), float(uIndex / 32));
gl_Position = vec4((cell + aPos) / 16.0 - 1.0, 0.0, 1.0);
}
)";
// Multiples of 8/255 survive an RGBA8 round trip exactly, so the readback bytes are
// (8x, 8y, 255, 255) for the pixel at (x, y) and nothing else can produce them.
constexpr const char* kFS = R"(#version 330 core
uniform int uIndex;
out vec4 o_color;
void main() {
o_color = vec4(float(uIndex % 32) * 8.0 / 255.0, float(uIndex / 32) * 8.0 / 255.0, 1.0, 1.0);
}
)";
// The state cases: the unit-square vertices (or a triangle of them) stretched over an
// NDC rectangle at a given depth, in a uniform colour. A zero rectangle makes a
// zero-area primitive - a draw that reaches the renderer and rasterizes nothing.
constexpr const char* kRectVS = R"(#version 330 core
in vec2 aPos;
uniform vec4 uRect;
uniform float uDepth;
void main() { gl_Position = vec4(mix(uRect.xy, uRect.zw, aPos), uDepth, 1.0); }
)";
constexpr const char* kRectFS = R"(#version 330 core
uniform vec4 uColor;
out vec4 o_color;
void main() { o_color = uColor; }
)";
// The capture case: one point per draw, carrying 2 * uIndex + 1.
constexpr const char* kCaptureVS = R"(#version 330 core
uniform int uIndex;
out float vs_value;
void main() {
vs_value = float(uIndex) * 2.0 + 1.0;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
}
)";
constexpr const char* kCaptureFS = R"(#version 330 core
out vec4 o_color;
void main() { o_color = vec4(1.0); }
)";
constexpr float kCapturePoison = -12345.0f;
// The budget the process was launched with, or 0 when it is unset or not a number.
unsigned long PinnedDrawBudget() {
const char* value = std::getenv("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER");
if (value == nullptr || *value == '\0') return 0;
char* end = nullptr;
const unsigned long parsed = std::strtoul(value, &end, 10);
return (end != nullptr && *end == '\0') ? parsed : 0;
}
// The library log, for the arming assertion. MOBILEGL_LOG_FILE_PATH is read at log-init,
// every process in the lane appends to it, and only bytes appended after the snapshot
// count. P11 M2: ON A SPLIT BUILD IT IS A BASE NAME - the library writes one file per role
// (PipeStatsWindow.h) - and the latched line comes from the RENDERER, which under a
// transport is the server role: the apply thread under inproc, the spawned server process
// under spawn (it inherits this entry's environment, budget and log base included). So the
// mark and the read cover both roles, as PrimitivesGeneratedNoXfbScenario's arming does; a
// read of the literal base found no file and reported an unsplit renderer. In the pull
// build the client role is the whole log.
std::string LibraryLogPath() { return PipeStatsWindow::LibraryLogPath(); }
PipeStatsWindow::LogMark LibraryLogSize() { return PipeStatsWindow::MarkLaneLog(); }
std::string LibraryLogSince(const PipeStatsWindow::LogMark& mark) {
return PipeStatsWindow::ReadLaneLogSince(mark);
}
std::string Describe(const Rgba8& c) {
std::ostringstream os;
os << "rgba(" << static_cast<int>(c.r) << ", " << static_cast<int>(c.g) << ", " << static_cast<int>(c.b)
<< ", " << static_cast<int>(c.a) << ")";
return os.str();
}
::testing::AssertionResult EveryPixelHoldsItsOwnDraw(const Image& image) {
int wrong = 0;
int firstX = -1, firstY = -1;
Rgba8 firstSeen{};
for (int y = 0; y < kSide; ++y) {
for (int x = 0; x < kSide; ++x) {
const Rgba8 expected{static_cast<std::uint8_t>(8 * x), static_cast<std::uint8_t>(8 * y), 255, 255};
const Rgba8 seen = image.At(x, y);
if (seen != expected) {
if (wrong == 0) {
firstX = x;
firstY = y;
firstSeen = seen;
}
++wrong;
}
}
}
if (wrong == 0) {
return ::testing::AssertionSuccess();
}
return ::testing::AssertionFailure()
<< wrong << " of " << kDraws << " pixels do not hold their own draw's colour. First: (" << firstX
<< ", " << firstY << ") = " << firstSeen << ", drawn by draw " << (firstY * kSide + firstX)
<< " (a pixel no draw reached still holds the clear colour 0,0,0,0)";
}
// Every pixel of columns [x0, x1] (all rows) is `expected`, each channel within `tolerance`.
::testing::AssertionResult ColumnsAre(const Image& image, int x0, int x1, const Rgba8& expected,
int tolerance, const char* what) {
int wrong = 0;
int firstX = -1, firstY = -1;
Rgba8 firstSeen{};
for (int y = 0; y < kSide; ++y) {
for (int x = x0; x <= x1; ++x) {
const Rgba8 seen = image.At(x, y);
const bool close = std::abs(seen.r - expected.r) <= tolerance &&
std::abs(seen.g - expected.g) <= tolerance &&
std::abs(seen.b - expected.b) <= tolerance &&
std::abs(seen.a - expected.a) <= tolerance;
if (!close) {
if (wrong == 0) {
firstX = x;
firstY = y;
firstSeen = seen;
}
++wrong;
}
}
}
if (wrong == 0) return ::testing::AssertionSuccess();
return ::testing::AssertionFailure()
<< what << ": " << wrong << " pixels of columns " << x0 << ".." << x1 << " are not "
<< Describe(expected) << "; first (" << firstX << ", " << firstY << ") = " << Describe(firstSeen);
}
class OversizedRecordingSplitScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
// Taken before the case draws anything, so the line the arming check looks for
// can only have been written by this case. The renderer latches it at its first
// split, and ctest gives every case a process of its own.
m_logBefore = LibraryLogSize();
// 0..3: the unit square as a strip. 4..6: its lower-left triangle. 7..9: its
// upper-right triangle - the two share the diagonal, so together they cover every
// sample of the square exactly once.
static const float kVertices[] = {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f,
1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(kVertices), kVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
}
void TearDown() override {
if (Ready()) {
glDisable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_BLEND);
glDisable(GL_RASTERIZER_DISCARD);
glDepthMask(GL_TRUE);
glStencilMask(0xFF);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glUseProgram(0);
glBindVertexArray(0);
BindDefaultFramebuffer();
if (m_rectProgram != 0) glDeleteProgram(m_rectProgram);
if (m_vbo != 0) glDeleteBuffers(1, &m_vbo);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
for (GLuint rbo : m_renderbuffers) glDeleteRenderbuffers(1, &rbo);
for (GLuint fbo : m_framebuffers) glDeleteFramebuffers(1, &fbo);
for (ColorFbo& target : m_colorTargets) DestroyColorFbo(target);
}
ScenarioTest::TearDown();
}
// In the SplitRecording lane (budget pinned below what every case draws, DirectVulkan,
// a log to read), a split must have happened during this case. Elsewhere this case is
// the unsplit control and there is nothing to arm.
void ExpectTheRecordingWasSplitWhereTheBudgetIsPinned() {
const unsigned long budget = PinnedDrawBudget();
const bool splitExpected = budget > 0 && budget * 2 < static_cast<unsigned long>(kFillers) &&
Gl().BackendName() == std::string("DirectVulkan") &&
!LibraryLogPath().empty();
RecordProperty("arming_checked", splitExpected ? "yes" : "no");
if (!splitExpected) return;
const std::string appended = LibraryLogSince(m_logBefore);
EXPECT_NE(appended.find("draws into one command buffer; submitting it"), std::string::npos)
<< "MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER=" << budget << " and this case recorded more "
<< "than " << kFillers << " draws in one frame, but DirectVulkan never reported splitting the "
"recording. A renderer that does not split grows a single command buffer without bound "
"(rd12 on Adreno: ~40k driver mappings, then SIGABRT). See "
"VulkanRenderer::SplitOversizedRecording. Log appended by this case:\n"
<< appended;
}
bool BuildRectProgram(std::string* error) {
m_rectProgram = CompileProgram(kRectVS, kRectFS, error);
if (m_rectProgram == 0) return false;
m_rectLoc = glGetUniformLocation(m_rectProgram, "uRect");
m_depthLoc = glGetUniformLocation(m_rectProgram, "uDepth");
m_colorLoc = glGetUniformLocation(m_rectProgram, "uColor");
glUseProgram(m_rectProgram);
glBindVertexArray(m_vao);
return true;
}
// The rectangle [x0, x1] x [y0, y1] in NDC; `first`/`count` pick the strip or a triangle.
void DrawRect(float x0, float y0, float x1, float y1, float depth, float r, float g, float b, float a,
GLenum mode = GL_TRIANGLE_STRIP, GLint first = 0, GLsizei count = 4) {
glUniform4f(m_rectLoc, x0, y0, x1, y1);
glUniform1f(m_depthLoc, depth);
glUniform4f(m_colorLoc, r, g, b, a);
glDrawArrays(mode, first, count);
}
// One pixel of the 32x32 grid, by index.
void DrawPixel(int index, float r, float g, float b, float a) {
const float x = static_cast<float>(index % kSide);
const float y = static_cast<float>(index / kSide);
DrawRect(x / 16.0f - 1.0f, y / 16.0f - 1.0f, (x + 1.0f) / 16.0f - 1.0f, (y + 1.0f) / 16.0f - 1.0f, 0.0f,
r, g, b, a);
}
// Draws that reach the renderer and rasterize nothing: zero-area strips.
void DrawFillers(int count) {
for (int i = 0; i < count; ++i) {
DrawRect(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
}
}
// A kSide x kSide RGBA8 target, optionally with a DEPTH24_STENCIL8 renderbuffer.
// Returns fbo 0 when the driver will not complete it.
ColorFbo MakeTarget(bool withDepthStencil) {
ColorFbo target = MakeColorFbo(kSide, kSide);
if (target.fbo == 0) return target;
m_colorTargets.push_back(target);
if (withDepthStencil) {
GLuint rbo = 0;
glGenRenderbuffers(1, &rbo);
m_renderbuffers.push_back(rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, kSide, kSide);
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
target.fbo = 0;
}
}
return target;
}
Image ReadTarget(const ColorFbo& target) {
glBindFramebuffer(GL_FRAMEBUFFER, target.fbo);
return ReadPixels(kSide, kSide);
}
PipeStatsWindow::LogMark m_logBefore{};
GLuint m_vao = 0;
GLuint m_vbo = 0;
GLuint m_rectProgram = 0;
GLint m_rectLoc = -1;
GLint m_depthLoc = -1;
GLint m_colorLoc = -1;
std::vector<ColorFbo> m_colorTargets;
std::vector<GLuint> m_renderbuffers;
std::vector<GLuint> m_framebuffers;
};
} // namespace
// kDraws single-pixel quads into a fresh target, one uniform value per draw, with no
// readback, flush or rebind in between. Every pixel must hold its own draw's colour.
TEST_F(OversizedRecordingSplitScenario, EveryDrawOfTheFrameLandsAcrossCommandBufferSplits) {
if (!Ready()) return;
std::string error;
const unsigned int program = CompileProgram(kVS, kFS, &error);
ASSERT_NE(program, 0u) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
glUseProgram(program);
glBindVertexArray(m_vao);
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
for (int i = 0; i < kDraws; ++i) {
glUniform1i(indexLocation, i);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
const Image image = ReadPixels(kSide, kSide);
glUseProgram(0);
glDeleteProgram(program);
EXPECT_TRUE(EveryPixelHoldsItsOwnDraw(image));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (a) An occluder drawn before the split must still fail the depth test of a draw after it:
// the depth attachment's contents have to survive the render pass ending and re-beginning.
TEST_F(OversizedRecordingSplitScenario, ADepthOccluderDrawnBeforeTheSplitHidesADrawAfterIt) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(true);
if (target.fbo == 0) GTEST_SKIP() << "no complete RGBA8 + DEPTH24_STENCIL8 target on this driver";
BindFbo(target);
glClearDepth(1.0);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glDepthMask(GL_TRUE);
DrawRect(-1.0f, -1.0f, 0.0f, 1.0f, -0.5f, 1.0f, 0.0f, 0.0f, 1.0f); // left half, near, red
DrawFillers(kFillers);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.5f, 0.0f, 1.0f, 0.0f, 1.0f); // everything, far, green
const Image image = ReadTarget(target);
EXPECT_TRUE(ColumnsAre(image, 0, kSide / 2 - 1, Rgba8{255, 0, 0, 255}, 0,
"the near occluder drawn before the split must hide the far draw after it"));
EXPECT_TRUE(ColumnsAre(image, kSide / 2, kSide - 1, Rgba8{0, 255, 0, 255}, 0,
"where nothing occludes, the far draw after the split must land"));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (b) A stencil mark made before the split must gate a draw after it.
TEST_F(OversizedRecordingSplitScenario, AStencilMarkMadeBeforeTheSplitGatesADrawAfterIt) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(true);
if (target.fbo == 0) GTEST_SKIP() << "no complete RGBA8 + DEPTH24_STENCIL8 target on this driver";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
glStencilMask(0xFF);
glClearStencil(0);
glClear(GL_STENCIL_BUFFER_BIT);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
DrawRect(-1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f); // left half: stencil 1, blue
glStencilFunc(GL_EQUAL, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
DrawFillers(kFillers);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f); // everything, green, where stencil == 1
const Image image = ReadTarget(target);
EXPECT_TRUE(ColumnsAre(image, 0, kSide / 2 - 1, Rgba8{0, 255, 0, 255}, 0,
"where the mark was made before the split, the draw after it must pass"));
EXPECT_TRUE(ColumnsAre(image, kSide / 2, kSide - 1, Rgba8{0, 0, 0, 255}, 0,
"where no mark was made, the draw after the split must be stencilled out"));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (c) Additive blending onto pixels written before the split: every one of kSpanDraws
// full-target draws adds exactly 1/255 of red onto a target whose green was written first,
// so the result counts every draw on both sides of every split.
TEST_F(OversizedRecordingSplitScenario, BlendingAccumulatesOntoPixelsWrittenBeforeTheSplit) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 0.0f);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 55.0f / 255.0f, 0.0f, 0.0f);
for (int i = 0; i < kSpanDraws; ++i) {
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f / 255.0f, 0.0f, 0.0f, 0.0f);
}
const Image image = ReadTarget(target);
EXPECT_TRUE(ColumnsAre(image, 0, kSide - 1, Rgba8{static_cast<std::uint8_t>(kSpanDraws), 55, 0, 0}, 1,
"every additive draw, and the green written before any split, must be in the sum"));
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (d) A multisample target drawn on both sides of the split, then resolved. Before the
// split a triangle covers the lower-left half of the target; after it, the complementary
// triangle covers the upper-right half. On the diagonal each pixel's samples are shared
// between the two, so only a target that kept every SAMPLE across the split resolves them to
// red + green = 255. One that was resolved or re-broadcast at the split gives about 190
// there, and one whose samples were dropped gives black or garbage.
TEST_F(OversizedRecordingSplitScenario, AMultisampleTargetKeepsItsSamplesAcrossTheSplit) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
GLint maxSamples = 0;
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
if (maxSamples < 2) {
GTEST_SKIP() << "GL_MAX_SAMPLES is " << maxSamples << "; this needs a multisample renderbuffer";
}
const GLsizei samples = std::min<GLint>(4, maxSamples);
GLuint msFbo = 0, msRbo = 0;
glGenFramebuffers(1, &msFbo);
m_framebuffers.push_back(msFbo);
glGenRenderbuffers(1, &msRbo);
m_renderbuffers.push_back(msRbo);
glBindRenderbuffer(GL_RENDERBUFFER, msRbo);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, kSide, kSide);
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msRbo);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
GTEST_SKIP() << "no complete " << samples << "x multisample RGBA8 renderbuffer on this driver";
}
ColorFbo resolved = MakeTarget(false);
ASSERT_NE(resolved.fbo, 0u) << "MakeColorFbo failed";
glBindFramebuffer(GL_FRAMEBUFFER, msFbo);
glViewport(0, 0, kSide, kSide);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, GL_TRIANGLES, 4, 3); // lower-left, red
DrawFillers(kFillers);
DrawRect(-1.0f, -1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, GL_TRIANGLES, 7, 3); // upper-right, green
glBindFramebuffer(GL_READ_FRAMEBUFFER, msFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolved.fbo);
glBlitFramebuffer(0, 0, kSide, kSide, 0, 0, kSide, kSide, GL_COLOR_BUFFER_BIT, GL_NEAREST);
const Image image = ReadTarget(resolved);
int wrong = 0;
std::string first;
for (int y = 0; y < kSide && wrong < 1000; ++y) {
for (int x = 0; x < kSide; ++x) {
const Rgba8 seen = image.At(x, y);
bool ok = false;
// The diagonal x + y = kSide in pixel units passes through the pixels whose
// x + y == kSide - 1; every other pixel lies wholly on one side of it.
if (x + y < kSide - 1) {
ok = seen == Rgba8{255, 0, 0, 255};
} else if (x + y > kSide - 1) {
ok = seen == Rgba8{0, 255, 0, 255};
} else {
ok = seen.b == 0 && seen.a == 255 && seen.r > 0 && seen.g > 0 &&
std::abs(static_cast<int>(seen.r) + static_cast<int>(seen.g) - 255) <= 3;
}
if (!ok) {
if (wrong == 0) {
std::ostringstream os;
os << "(" << x << ", " << y << ") = " << Describe(seen);
first = os.str();
}
++wrong;
}
}
}
EXPECT_EQ(wrong, 0) << wrong << " resolved pixels are wrong; first " << first
<< ". Lower-left of the diagonal must be red, upper-right green, and a pixel on it "
"red + green = 255 with both present (its samples were split between a draw before "
"the command-buffer split and one after it)";
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (e) Occlusion queries open across the split. GL_SAMPLES_PASSED counts kSpanDraws
// single-pixel draws on both sides of it; GL_ANY_SAMPLES_PASSED is answered by the one draw
// made before the split, the fillers after it pass nothing - with a fillers-only control
// that must answer false.
TEST_F(OversizedRecordingSplitScenario, OcclusionQueriesOpenAcrossTheSplitCountBothSides) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
GLuint queries[3] = {};
glGenQueries(3, queries);
FirstGLError();
// Any samples: control first (fillers only), then one real pixel before the split.
glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[0]);
const unsigned int anyBeginError = FirstGLError();
if (anyBeginError != 0u) {
glDeleteQueries(3, queries);
GTEST_SKIP() << Gl().BackendName() << " does not accept GL_ANY_SAMPLES_PASSED ("
<< GLErrorName(anyBeginError) << ")";
}
DrawFillers(kFillers);
glEndQuery(GL_ANY_SAMPLES_PASSED);
glBeginQuery(GL_ANY_SAMPLES_PASSED, queries[1]);
DrawPixel(0, 1.0f, 1.0f, 1.0f, 1.0f);
DrawFillers(kFillers);
glEndQuery(GL_ANY_SAMPLES_PASSED);
GLuint anyControl = 7, anyAcross = 7;
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &anyControl);
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &anyAcross);
EXPECT_EQ(anyControl, static_cast<GLuint>(GL_FALSE)) << "fillers alone rasterize nothing";
EXPECT_EQ(anyAcross, static_cast<GLuint>(GL_TRUE))
<< "the only passing draw was made before the split; the query that spans it lost that answer";
// Samples passed: kSpanDraws distinct single-sample pixels, spanning at least one split.
glBeginQuery(GL_SAMPLES_PASSED, queries[2]);
const unsigned int samplesBeginError = FirstGLError();
if (samplesBeginError == 0u) {
for (int i = 0; i < kSpanDraws; ++i) {
DrawPixel(i, 1.0f, 1.0f, 1.0f, 1.0f);
}
glEndQuery(GL_SAMPLES_PASSED);
GLuint samplesPassed = 0;
glGetQueryObjectuiv(queries[2], GL_QUERY_RESULT, &samplesPassed);
if (Gl().BackendName() == std::string("DirectGLES")) {
// OpenGL ES has no sample-counting occlusion query, only ANY_SAMPLES_PASSED*, so
// DirectGLES answers GL_SAMPLES_PASSED with a 0/1 approximation (DirectGLES.cpp,
// the occlusion-query result path). The exact count is not this backend's to give.
RecordProperty("samples_passed", "0/1 approximation on OpenGL ES; exact count not checked");
EXPECT_EQ(samplesPassed, 1u) << "samples passed, so the approximation must read 1";
} else {
EXPECT_EQ(samplesPassed, static_cast<GLuint>(kSpanDraws))
<< "each of the " << kSpanDraws << " draws covers exactly one single-sample pixel";
}
} else {
RecordProperty("samples_passed", std::string("not accepted: ") + GLErrorName(samplesBeginError));
}
glDeleteQueries(3, queries);
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (e) GL_PRIMITIVES_GENERATED open across the split: kSpanDraws strips of two triangles.
// A one-draw query first establishes that this stack counts at all.
TEST_F(OversizedRecordingSplitScenario, APrimitivesGeneratedQueryOpenAcrossTheSplitCountsBothSides) {
if (!Ready()) return;
std::string error;
ASSERT_TRUE(BuildRectProgram(&error)) << error;
ColorFbo target = MakeTarget(false);
ASSERT_NE(target.fbo, 0u) << "MakeColorFbo failed";
BindFbo(target);
glDisable(GL_DEPTH_TEST);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
GLuint queries[2] = {};
glGenQueries(2, queries);
FirstGLError();
glBeginQuery(GL_PRIMITIVES_GENERATED, queries[0]);
const unsigned int beginError = FirstGLError();
if (beginError != 0u) {
glDeleteQueries(2, queries);
GTEST_SKIP() << Gl().BackendName() << " does not accept GL_PRIMITIVES_GENERATED ("
<< GLErrorName(beginError) << ")";
}
DrawPixel(0, 1.0f, 1.0f, 1.0f, 1.0f);
glEndQuery(GL_PRIMITIVES_GENERATED);
GLuint control = 0;
glGetQueryObjectuiv(queries[0], GL_QUERY_RESULT, &control);
if (control == 0u) {
glDeleteQueries(2, queries);
GTEST_SKIP() << "GL_PRIMITIVES_GENERATED reads 0 for a single unsplit strip on " << Gl().BackendName()
<< " (" << Gl().RendererString() << "): nothing this stack counts, so nothing to carry";
}
ASSERT_EQ(control, 2u) << "one strip of four vertices is two triangles";
glBeginQuery(GL_PRIMITIVES_GENERATED, queries[1]);
for (int i = 0; i < kSpanDraws; ++i) {
DrawPixel(i, 1.0f, 1.0f, 1.0f, 1.0f);
}
glEndQuery(GL_PRIMITIVES_GENERATED);
GLuint generated = 0;
glGetQueryObjectuiv(queries[1], GL_QUERY_RESULT, &generated);
EXPECT_EQ(generated, static_cast<GLuint>(2 * kSpanDraws))
<< kSpanDraws << " strips of two triangles each, drawn across at least one split";
glDeleteQueries(2, queries);
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
// (f) Transform feedback active across the split: kSpanDraws one-point draws, draw i
// capturing 2i + 1, must append in order into one buffer with nothing past the end, and
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN must count every one.
TEST_F(OversizedRecordingSplitScenario, ATransformFeedbackCaptureActiveAcrossTheSplitAppendsEveryDraw) {
if (!Ready()) return;
auto compile = [](GLenum stage, const char* source, std::string* log) -> GLuint {
const GLuint shader = glCreateShader(stage);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char buffer[2048] = {};
glGetShaderInfoLog(shader, sizeof(buffer) - 1, nullptr, buffer);
*log = buffer;
glDeleteShader(shader);
return 0;
}
return shader;
};
std::string log;
const GLuint vs = compile(GL_VERTEX_SHADER, kCaptureVS, &log);
ASSERT_NE(vs, 0u) << log;
const GLuint fs = compile(GL_FRAGMENT_SHADER, kCaptureFS, &log);
ASSERT_NE(fs, 0u) << log;
const GLuint program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
const char* varyings[] = {"vs_value"};
glTransformFeedbackVaryings(program, 1, varyings, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
GLint linked = 0;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked) {
char buffer[2048] = {};
glGetProgramInfoLog(program, sizeof(buffer) - 1, nullptr, buffer);
glDeleteProgram(program);
FAIL() << "capture program failed to link: " << buffer;
}
constexpr int kSlack = 4;
std::vector<float> poison(kSpanDraws + kSlack, kCapturePoison);
GLuint buffer = 0;
glGenBuffers(1, &buffer);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
glBufferData(GL_TRANSFORM_FEEDBACK_BUFFER, static_cast<GLsizeiptr>(poison.size() * sizeof(float)),
poison.data(), GL_STATIC_COPY);
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer);
GLuint written = 0;
glGenQueries(1, &written);
glUseProgram(program);
glBindVertexArray(m_vao);
const GLint indexLocation = glGetUniformLocation(program, "uIndex");
glEnable(GL_RASTERIZER_DISCARD);
glBeginQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN, written);
glBeginTransformFeedback(GL_POINTS);
for (int i = 0; i < kSpanDraws; ++i) {
glUniform1i(indexLocation, i);
glDrawArrays(GL_POINTS, 0, 1);
}
glEndTransformFeedback();
glEndQuery(GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN);
glDisable(GL_RASTERIZER_DISCARD);
glUseProgram(0);
GLuint primitivesWritten = 0;
glGetQueryObjectuiv(written, GL_QUERY_RESULT, &primitivesWritten);
std::vector<float> captured(poison.size(), 0.0f);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, buffer);
glGetBufferSubData(GL_TRANSFORM_FEEDBACK_BUFFER, 0, static_cast<GLsizeiptr>(captured.size() * sizeof(float)),
captured.data());
glBindBufferBase(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
glBindBuffer(GL_TRANSFORM_FEEDBACK_BUFFER, 0);
glDeleteQueries(1, &written);
glDeleteBuffers(1, &buffer);
glDeleteProgram(program);
EXPECT_EQ(primitivesWritten, static_cast<GLuint>(kSpanDraws));
int wrong = 0;
std::string first;
for (int i = 0; i < static_cast<int>(captured.size()); ++i) {
const float expected = i < kSpanDraws ? static_cast<float>(2 * i + 1) : kCapturePoison;
if (captured[i] != expected) {
if (wrong == 0) {
std::ostringstream os;
os << "record " << i << " = " << captured[i] << ", expected " << expected;
first = os.str();
}
++wrong;
}
}
EXPECT_EQ(wrong, 0) << wrong << " of " << captured.size() << " records are wrong; first " << first
<< " (records past " << kSpanDraws << " must keep the poison)";
EXPECT_EQ(FirstGLError(), 0u);
ExpectTheRecordingWasSplitWhereTheBudgetIsPinned();
}
} // namespace MGITest
@@ -33,7 +33,7 @@
// must see it too.
// M-A Nothing produced kMGPipeBindSampler / kMGPipeBindShaderImage, so ImageBindableHint was
// dead: the applier never saw a texture become image-bound, the metadata respecify
// (ID-18 M4) had no live trigger, and the remint pull the hint exists to prevent was
// (ID-18 M4) had no live trigger, and the re-mint the hint exists to prevent was
// neither prevented nor counted. The case here reads the applier's record around a
// glBindImageTexture: the hint arrives as a metadata update that keeps the pending upload
// standing beside it, and the picture after the transition is the texels that upload
@@ -537,9 +537,12 @@ void main() { oColor = texture(uTex, vUv); }
return;
}
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR: a texture Espryt allocated BEFORE the
// hint reached it is re-minted image-bindable at the bind and its levels replayed
// from the client's shadow - one remint pull, counted. Arming the counter here is
// THE NUMBER ROADMAP OPEN QUESTION 2 ASKS FOR, on a texture Espryt allocated BEFORE the
// hint reached it. An immutable RGBA8 allocation is already what an image unit needs,
// so neither arm re-mints it and the counter does not move: the server has kept it
// since P9 W2, and the monolith re-mint has had the same keep path since dev's
// 568090f0 (merged in P11 M), where it used to re-mint from the shadow and count one.
// A counted re-mint is TextureRemintPullScenario's A and H. Arming the counter here is
// what makes it readable without a stats-enabled lane.
unsigned long long pullsBefore = 0;
const bool pullsReadable = PeekPipeStatsTextureRemintPulls(&pullsBefore);
@@ -580,11 +583,10 @@ void main() { oColor = texture(uTex, vUv); }
// THE TRANSITION. An immutable texture has no storage-defining respecify left, so the
// hint can only arrive as a metadata update (ID-18 M4). Espryt syncs the texture
// eagerly inside glBindImageTexture and the widening re-mints its storage, replaying
// every defined level from the shadow (the remint pull the counter below counts), so
// the standing upload is consumed by that regeneration here and the picture that
// follows is blue whatever the metadata respecify did to the record - the KEPT
// property is proved further down, on a texture no remint stands in front of.
// eagerly inside glBindImageTexture and KEEPS the allocation (above), so the standing
// upload is consumed by the kept storage's own sync and the picture that follows is
// blue - the KEPT property of the record is proved further down, on a texture that is
// image-bindable already.
(void)uploadsBeforeBind;
(void)uploadsReadable;
glBindImageTexture(0, texture, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
@@ -608,14 +610,10 @@ void main() { oColor = texture(uTex, vUv); }
const auto runtime = PeekSplitRuntime();
if (runtime.transportResolved && runtime.transportName == "inproc") {
ASSERT_TRUE(runtime.sessionActive);
EXPECT_EQ(pullsAfter, pullsBefore)
<< "the server must preserve this already immutable RGBA8 allocation; "
"image binding needs no remint pull";
} else {
EXPECT_EQ(pullsAfter, pullsBefore + 1)
<< "the monolith re-mint of a texture allocated before its hint was not counted "
"as a remint pull (trp= on the stats line is ROADMAP open question 2's number)";
}
EXPECT_EQ(pullsAfter, pullsBefore)
<< "an already immutable RGBA8 allocation must be kept on every arm; image binding "
"needs no re-mint (trp= on the stats line is ROADMAP open question 2's number)";
}
// THE PREVENTION HALF, measured the other way round: a texture whose hint arrives at
@@ -0,0 +1,454 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PackBufferReadbackScenario.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
//
// P9 (W1, MG_Remote/CONTRACT-P9.md §1): READS INTO A PIXEL PACK BUFFER ARE ANSWERED BY NOTHING.
//
// Under a transport a glReadPixels / glGetTexImage whose destination is a bound
// GL_PIXEL_PACK_BUFFER no longer waits for its pixels: it crosses as read_pixels_to_buffer /
// get_texture_image_to_buffer, the server lands the rows in the buffer at the client's PACK
// layout, and the client marks the buffer GPU-written so that the next CPU read of it - a
// glGetBufferSubData, a glMapBufferRange, or a texture upload that uses it as the UNPACK source -
// fetches the bytes through resource_readback.
//
// THE CASES ARE ORDINARY GL AND RUN ON THE MONOLITH ARMS TOO. Each one compares the pack buffer
// against the same read into client memory under the same pack state, both destinations
// pre-filled with the same byte, so the padding the pack state skips has to survive as well: the
// monolith and every split arm must agree with themselves, and therefore with each other. The
// one exception is the last case, which is about the WIRE (the read posted no reply and marked
// the buffer) and skips where there is no wire to ask.
//
// Every image is POSITION-DEPENDENT, so a row landed at the wrong stride, a band at the wrong
// offset or a byte swapped the wrong way cannot look right.
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr GLubyte kFill = 0xAB;
// A texel that names its own position (and its layer), so nothing misplaced can match.
std::array<GLubyte, 4> PatternRgba8(int x, int y, int layer) {
return {static_cast<GLubyte>(x * 7 + layer * 31), static_cast<GLubyte>(y * 11 + layer),
static_cast<GLubyte>((x ^ y) + layer * 5), static_cast<GLubyte>(0x80 | (x + y))};
}
// Integers below 2^24 in every component, so float storage is exact and a byte swap of
// any of them is visibly different from the value.
std::array<GLfloat, 4> PatternRgba32f(int x, int y) {
return {static_cast<GLfloat>(x + 1), static_cast<GLfloat>(y + 3),
static_cast<GLfloat>(x * 64 + y), static_cast<GLfloat>(1000 + x * y)};
}
struct PackState {
const char* name;
GLint alignment;
GLint rowLength;
GLint skipPixels;
GLint skipRows;
GLboolean swapBytes;
};
// Neutral, then each PACK parameter that changes the layout, then all of them at once.
constexpr PackState kPackStates[] = {
{"neutral", 4, 0, 0, 0, GL_FALSE},
{"alignment 1", 1, 0, 0, 0, GL_FALSE},
{"alignment 8", 8, 0, 0, 0, GL_FALSE},
{"row length 53", 4, 53, 0, 0, GL_FALSE},
{"skip pixels 3, skip rows 2", 4, 0, 3, 2, GL_FALSE},
{"everything", 8, 47, 5, 3, GL_FALSE},
};
class PackBufferReadbackScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
DrainErrors();
glGenFramebuffers(1, &m_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glDisable(GL_SCISSOR_TEST);
ResetPack();
}
void TearDown() override {
if (!Ready()) return;
ResetPack();
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
for (GLuint buffer : m_buffers) glDeleteBuffers(1, &buffer);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
for (GLuint texture : m_textures) glDeleteTextures(1, &texture);
m_buffers.clear();
m_textures.clear();
m_fbo = 0;
DrainErrors();
ScenarioTest::TearDown();
}
static void ResetPack() {
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glPixelStorei(GL_PACK_SKIP_PIXELS, 0);
glPixelStorei(GL_PACK_SKIP_ROWS, 0);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, 0);
glPixelStorei(GL_PACK_SKIP_IMAGES, 0);
glPixelStorei(GL_PACK_SWAP_BYTES, GL_FALSE);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
}
static void ApplyPack(const PackState& pack) {
glPixelStorei(GL_PACK_ALIGNMENT, pack.alignment);
glPixelStorei(GL_PACK_ROW_LENGTH, pack.rowLength);
glPixelStorei(GL_PACK_SKIP_PIXELS, pack.skipPixels);
glPixelStorei(GL_PACK_SKIP_ROWS, pack.skipRows);
glPixelStorei(GL_PACK_SWAP_BYTES, pack.swapBytes);
}
static void DrainErrors() {
for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) {
}
}
// The bytes a pack state makes a width x height read occupy, from the first byte the
// read may touch (GL 4.6 §8.4.4.1 / §18.2): the skips, then the padded rows, the last
// row tight.
static std::size_t PackedExtent(const PackState& pack, int width, int height, std::size_t bpp) {
const std::size_t rowPixels = pack.rowLength > 0 ? static_cast<std::size_t>(pack.rowLength)
: static_cast<std::size_t>(width);
const std::size_t align = static_cast<std::size_t>(pack.alignment);
const std::size_t stride = (rowPixels * bpp + align - 1) / align * align;
return static_cast<std::size_t>(pack.skipRows) * stride + static_cast<std::size_t>(pack.skipPixels) * bpp +
static_cast<std::size_t>(height - 1) * stride + static_cast<std::size_t>(width) * bpp;
}
GLuint NewTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
GLuint NewBuffer(GLenum target, std::size_t size, GLubyte fill) {
GLuint buffer = 0;
glGenBuffers(1, &buffer);
m_buffers.push_back(buffer);
glBindBuffer(target, buffer);
const std::vector<GLubyte> initial(size, fill);
glBufferData(target, static_cast<GLsizeiptr>(size), initial.data(), GL_DYNAMIC_READ);
return buffer;
}
// An RGBA8 texture holding the pattern, attached as COLOR_ATTACHMENT0 for reading.
GLuint AttachPatternRgba8(int width, int height) {
std::vector<GLubyte> texels(static_cast<std::size_t>(width) * height * 4);
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x) {
const auto t = PatternRgba8(x, y, 0);
std::memcpy(texels.data() + (static_cast<std::size_t>(y) * width + x) * 4, t.data(), 4);
}
const GLuint texture = NewTexture();
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glReadBuffer(GL_COLOR_ATTACHMENT0);
EXPECT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
EXPECT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << "pattern upload";
return texture;
}
// glReadPixels of (x, y, width, height) under `pack` into client memory and into a
// pack buffer at `offset`, both pre-filled with kFill; the buffer's bytes are then
// read back by `readBack` and the two must be identical, byte for byte, padding
// included - and the bytes in front of `offset` must still be kFill.
template <typename ReadBack>
void ExpectPackBufferMatchesClientMemory(const PackState& pack, int x, int y, int width, int height,
GLenum type, std::size_t bpp, ReadBack&& readBack) {
ApplyPack(pack);
const std::size_t extent = PackedExtent(pack, width, height, bpp);
std::vector<GLubyte> host(extent, kFill);
glReadPixels(x, y, width, height, GL_RGBA, type, host.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << pack.name << ": client-memory read";
constexpr std::size_t kOffset = 64;
NewBuffer(GL_PIXEL_PACK_BUFFER, kOffset + extent, kFill);
glReadPixels(x, y, width, height, GL_RGBA, type, reinterpret_cast<void*>(kOffset));
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << pack.name << ": pack-buffer read";
std::vector<GLubyte> actual(kOffset + extent, 0);
readBack(actual);
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << pack.name << ": buffer read-back";
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
ResetPack();
std::size_t lead = 0;
for (std::size_t i = 0; i < kOffset; ++i) lead += actual[i] != kFill ? 1 : 0;
EXPECT_EQ(lead, 0u) << pack.name << ": " << lead << " bytes in front of the read's offset moved";
std::size_t bad = 0, first = 0;
for (std::size_t i = 0; i < extent; ++i) {
if (actual[kOffset + i] != host[i] && bad++ == 0) first = i;
}
EXPECT_EQ(bad, 0u) << pack.name << ": " << bad << " of " << extent
<< " bytes differ from the client-memory read, first at byte " << first
<< " (buffer " << static_cast<int>(actual[kOffset + first]) << ", host "
<< static_cast<int>(host[first]) << ")";
}
GLuint m_fbo = 0;
std::vector<GLuint> m_buffers;
std::vector<GLuint> m_textures;
};
TEST_F(PackBufferReadbackScenario, ReadPixelsIntoAPackBufferMatchesClientMemoryUnderEveryPackState) {
if (!Ready()) return;
AttachPatternRgba8(37, 23);
if (HasFatalFailure()) return;
for (const PackState& pack : kPackStates) {
ExpectPackBufferMatchesClientMemory(pack, 2, 1, 31, 19, GL_UNSIGNED_BYTE, 4, [](std::vector<GLubyte>& out) {
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(out.size()), out.data());
});
if (HasFatalFailure()) return;
}
}
// The map is the OTHER CPU reader, and the one an application that streams readbacks
// through a PBO actually uses: glMapBufferRange(GL_MAP_READ_BIT) must see the pixels the
// server landed, not the bytes the client shadow held before the read.
TEST_F(PackBufferReadbackScenario, MapBufferRangeAfterAPackBufferReadSeesThePixels) {
if (!Ready()) return;
AttachPatternRgba8(37, 23);
if (HasFatalFailure()) return;
for (const PackState& pack : {kPackStates[0], kPackStates[5]}) {
ExpectPackBufferMatchesClientMemory(pack, 0, 0, 37, 23, GL_UNSIGNED_BYTE, 4, [](std::vector<GLubyte>& out) {
void* mapped = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(out.size()),
GL_MAP_READ_BIT);
ASSERT_NE(mapped, nullptr);
std::memcpy(out.data(), mapped, out.size());
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
});
if (HasFatalFailure()) return;
}
}
// GL_PACK_SWAP_BYTES reverses every component's bytes; the client carries the group
// width and the server swaps, so a wrong width or a skipped swap is a different float.
TEST_F(PackBufferReadbackScenario, SwapBytesIntoAPackBufferMatchesClientMemory) {
if (!Ready()) return;
constexpr int kWidth = 13, kHeight = 9;
std::vector<GLfloat> texels(static_cast<std::size_t>(kWidth) * kHeight * 4);
for (int y = 0; y < kHeight; ++y)
for (int x = 0; x < kWidth; ++x) {
const auto t = PatternRgba32f(x, y);
std::memcpy(texels.data() + (static_cast<std::size_t>(y) * kWidth + x) * 4, t.data(), sizeof(t));
}
const GLuint texture = NewTexture();
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA32F, kWidth, kHeight);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_RGBA, GL_FLOAT, texels.data());
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glReadBuffer(GL_COLOR_ATTACHMENT0);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast<GLenum>(GL_FRAMEBUFFER_COMPLETE));
const PackState swapped{"swap bytes, row length 17", 4, 17, 1, 1, GL_TRUE};
ExpectPackBufferMatchesClientMemory(swapped, 0, 0, kWidth, kHeight, GL_FLOAT, 16, [](std::vector<GLubyte>& out) {
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(out.size()), out.data());
});
}
// glGetTexImage into a pack buffer, 2D and layered: the layered case is the one whose
// layout has an image stride (GL_PACK_IMAGE_HEIGHT) and a skip in the third dimension.
TEST_F(PackBufferReadbackScenario, GetTexImageIntoAPackBufferMatchesClientMemory) {
if (!Ready()) return;
constexpr int kWidth = 11, kHeight = 7, kLayers = 3;
std::vector<GLubyte> texels(static_cast<std::size_t>(kWidth) * kHeight * kLayers * 4);
for (int layer = 0; layer < kLayers; ++layer)
for (int y = 0; y < kHeight; ++y)
for (int x = 0; x < kWidth; ++x) {
const auto t = PatternRgba8(x, y, layer);
std::memcpy(texels.data() + ((static_cast<std::size_t>(layer) * kHeight + y) * kWidth + x) * 4,
t.data(), 4);
}
const GLuint flat = NewTexture();
glBindTexture(GL_TEXTURE_2D, flat);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, texels.data());
const GLuint layered = NewTexture();
glBindTexture(GL_TEXTURE_2D_ARRAY, layered);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, kWidth, kHeight, kLayers);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, kWidth, kHeight, kLayers, GL_RGBA, GL_UNSIGNED_BYTE,
texels.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << "texture upload";
struct Shape {
const char* name;
GLenum target;
GLuint texture;
int depth;
GLint rowLength, alignment, imageHeight, skipImages;
};
const Shape shapes[] = {
{"2D, row length 13, alignment 8", GL_TEXTURE_2D, flat, 1, 13, 8, 0, 0},
{"2D array, image height 9, skip images 1", GL_TEXTURE_2D_ARRAY, layered, kLayers, 0, 4, 9, 1},
};
for (const Shape& shape : shapes) {
glBindTexture(shape.target, shape.texture);
glPixelStorei(GL_PACK_ROW_LENGTH, shape.rowLength);
glPixelStorei(GL_PACK_ALIGNMENT, shape.alignment);
glPixelStorei(GL_PACK_IMAGE_HEIGHT, shape.imageHeight);
glPixelStorei(GL_PACK_SKIP_IMAGES, shape.skipImages);
const std::size_t rowPixels = shape.rowLength > 0 ? static_cast<std::size_t>(shape.rowLength) : kWidth;
const std::size_t stride = (rowPixels * 4 + shape.alignment - 1) / shape.alignment * shape.alignment;
const std::size_t imageRows = shape.imageHeight > 0 ? static_cast<std::size_t>(shape.imageHeight) : kHeight;
const std::size_t imageStride = stride * imageRows;
const std::size_t extent = static_cast<std::size_t>(shape.skipImages) * imageStride +
static_cast<std::size_t>(shape.depth - 1) * imageStride +
static_cast<std::size_t>(kHeight - 1) * stride + kWidth * 4u;
std::vector<GLubyte> host(extent, kFill);
glGetTexImage(shape.target, 0, GL_RGBA, GL_UNSIGNED_BYTE, host.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << shape.name << ": client-memory read";
constexpr std::size_t kOffset = 32;
NewBuffer(GL_PIXEL_PACK_BUFFER, kOffset + extent, kFill);
glGetTexImage(shape.target, 0, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast<void*>(kOffset));
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << shape.name << ": pack-buffer read";
std::vector<GLubyte> actual(kOffset + extent, 0);
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(actual.size()), actual.data());
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
ResetPack();
std::size_t bad = 0, first = 0;
for (std::size_t i = 0; i < kOffset; ++i) bad += actual[i] != kFill ? 1 : 0;
EXPECT_EQ(bad, 0u) << shape.name << ": bytes in front of the offset moved";
bad = 0;
for (std::size_t i = 0; i < extent; ++i)
if (actual[kOffset + i] != host[i] && bad++ == 0) first = i;
EXPECT_EQ(bad, 0u) << shape.name << ": " << bad << " of " << extent
<< " bytes differ from the client-memory read, first at byte " << first;
}
}
// THE UNPACK HALF. A read into a buffer followed by an upload FROM that buffer is a
// GPU-to-GPU copy an application expects to work; under a transport the upload reads the
// client's shadow of the buffer, which the pack read did not touch, so this is exactly
// the case that goes wrong if the upload path forgets that the buffer was GPU-written.
TEST_F(PackBufferReadbackScenario, APackBufferReadFeedsAnUploadFromTheSameBuffer) {
if (!Ready()) return;
constexpr int kWidth = 29, kHeight = 17;
AttachPatternRgba8(kWidth, kHeight);
if (HasFatalFailure()) return;
const GLuint buffer = NewBuffer(GL_PIXEL_PACK_BUFFER, static_cast<std::size_t>(kWidth) * kHeight * 4, kFill);
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
const GLuint copy = NewTexture();
glBindTexture(GL_TEXTURE_2D, copy);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kWidth, kHeight);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, buffer);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR)) << "read into the buffer, then upload from it";
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, copy, 0);
std::vector<GLubyte> actual(static_cast<std::size_t>(kWidth) * kHeight * 4, 0);
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, actual.data());
std::size_t bad = 0;
std::string first;
for (int y = 0; y < kHeight; ++y)
for (int x = 0; x < kWidth; ++x) {
const auto expected = PatternRgba8(x, y, 0);
if (std::memcmp(actual.data() + (static_cast<std::size_t>(y) * kWidth + x) * 4, expected.data(), 4) != 0 &&
bad++ == 0)
first = "(" + std::to_string(x) + "," + std::to_string(y) + ")";
}
EXPECT_EQ(bad, 0u) << bad << " texels of the uploaded copy differ from the pattern, first at " << first;
}
// ORDER AGAINST LATER WRITES. A glBufferSubData into part of the buffer AFTER the read
// must win over the read's bytes there and leave the rest of the read alone - on the
// server the pixels and the later write are two queued ranges of one store, drained in
// record order.
TEST_F(PackBufferReadbackScenario, ALaterBufferSubDataWinsOverThePackReadWhereTheyOverlap) {
if (!Ready()) return;
constexpr int kWidth = 16, kHeight = 8;
AttachPatternRgba8(kWidth, kHeight);
if (HasFatalFailure()) return;
const std::size_t size = static_cast<std::size_t>(kWidth) * kHeight * 4;
std::vector<GLubyte> host(size, kFill);
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, host.data());
NewBuffer(GL_PIXEL_PACK_BUFFER, size, kFill);
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const std::vector<GLubyte> overwrite(40, 0x5A);
glBufferSubData(GL_PIXEL_PACK_BUFFER, 100, static_cast<GLsizeiptr>(overwrite.size()), overwrite.data());
std::vector<GLubyte> actual(size, 0);
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(size), actual.data());
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
std::memcpy(host.data() + 100, overwrite.data(), overwrite.size());
std::size_t bad = 0, first = 0;
for (std::size_t i = 0; i < size; ++i)
if (actual[i] != host[i] && bad++ == 0) first = i;
EXPECT_EQ(bad, 0u) << bad << " bytes differ, first at " << first;
}
// THE WIRE HALF, and the one case that is about the transport rather than the pixels:
// under a transport a pack-buffer read posts NO reply-slot record and marks the buffer
// GPU-written, once per read. MOBILEGL_IPC_PBO_READBACK_SYNC=1 puts the reply form back,
// and this case must then fail on both counters - that is its red-once, recorded in
// docs/Disaggregated/notes/p9/.
TEST_F(PackBufferReadbackScenario, APackBufferReadPostsNoReplyAndMarksTheBuffer) {
if (!Ready()) return;
const std::string split = SplitRuntimeSkipReason();
if (!split.empty()) GTEST_SKIP() << split;
constexpr int kWidth = 24, kHeight = 12;
AttachPatternRgba8(kWidth, kHeight);
if (HasFatalFailure()) return;
NewBuffer(GL_PIXEL_PACK_BUFFER, static_cast<std::size_t>(kWidth) * kHeight * 4, kFill);
// A warm-up read, so every state record the read's fill has to publish once is out
// of the way before the counted window opens.
glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
constexpr int kReads = 4;
const SplitRuntimeState before = PeekSplitRuntime();
for (int i = 0; i < kReads; ++i) glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const SplitRuntimeState after = PeekSplitRuntime();
ASSERT_EQ(FirstGLError(), static_cast<GLenum>(GL_NO_ERROR));
EXPECT_EQ(after.replyPostings - before.replyPostings, 0u)
<< "a read into a pack buffer published a reply-slot record: it took the reply form "
"(MOBILEGL_IPC_PBO_READBACK_SYNC=1 does this on purpose)";
EXPECT_EQ(after.packBufferReadbackMarks - before.packBufferReadbackMarks,
static_cast<unsigned long long>(kReads + 1))
<< "every pack-buffer read marks its buffer GPU-written exactly once";
// And the bytes are still right after all of that.
std::vector<GLubyte> actual(static_cast<std::size_t>(kWidth) * kHeight * 4, 0);
glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast<GLsizeiptr>(actual.size()), actual.data());
const auto corner = PatternRgba8(kWidth - 1, kHeight - 1, 0);
EXPECT_EQ(std::memcmp(actual.data() + actual.size() - 4, corner.data(), 4), 0);
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,600 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/PartialUploadOverGpuContentScenario.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
//
// Scenario - A SMALL glTexSubImage INTO A LEVEL THE GPU HAS WRITTEN.
//
// Espryt keeps a CPU shadow of every level and syncs a pending client write by uploading the
// region it covers (SyncMipmapsToBackend's sub-rect path). The shadow never sees a GPU write, so
// only the region may go up: anything more puts the shadow's stale texels over what the GPU drew.
// The region upload used to be skipped whenever the shadow's bytes had to be CONVERTED on the way
// to the driver - a three-channel format the driver cannot render is stored four-channel and every
// upload is repacked - and whenever the ES upload is shaped differently from the shadow (a 1D
// array, stored as a 2D array one texel high): those levels went up WHOLE, and a 2x2 write erased
// the rest of the level.
//
// Every case uploads a level, lets the GPU clear it, writes one 2x2 box, and reads the level back:
// the clear outside the box, the write inside. The formats are picked by what they cost the ES
// upload, which is per driver - on Mesa llvmpipe GL_SRGB8, GL_RGB8_SNORM and GL_RGB8UI are stored
// four-channel (see ThreeChannelAttachmentScenario) while GL_RGB16F is native; ES has no renderable
// three-channel format at all, so on a phone all four are converted. GL_RGBA8 is the control that
// never converts. A format a backend cannot attach is skipped, not failed.
//
// The second half is about ORDER, with GL_RGBA8. Magma queues a clear until the texture's next
// use and records texel uploads into a batch submitted ahead of the frame's recorded commands, so
// a write issued after a clear of (or a draw into) the level reached the image FIRST and the
// earlier clear or draw landed over it: the box read back green. Each order is covered - the
// clear already recorded, still queued, synced by a draw or by the readback, the reverse order
// in which the clear must win, and a draw instead of a clear.
//
// The last part is about SCATTER: several boxes written between two syncs, with the GPU's clear in
// the gaps between them. Everything a backend has to go on is the union box and the storage's
// rect list, and both describe more than the writes - the box spans the gaps, the list merges
// touching rects into their bounding box, folds once it holds 96, and is withheld once its rects
// fill 3/4 of the box; a box spanning the level went up as the whole level. Espryt also uploaded
// the union box whenever its unpack ring was in use. Each case below reaches one of those: two
// distant boxes, a hundred of them, boxes filling more than 3/4 of their union, and boxes whose
// union spans the level.
#include <cmath>
#include <cstdint>
#include <cstring>
#include <sstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kEdge = 8;
constexpr int kBoxAt = 2;
constexpr int kBoxEdge = 2;
constexpr const char* kVS = R"(#version 430 core
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
)";
// How a value in [0, 1] is spelled in the client data, and how the level is cleared and
// read back. Integer levels spell 1 as kIntegerOne.
enum class Encoding { Unorm8, Snorm8, Float32, Uint8 };
constexpr unsigned kIntegerOne = 9;
struct Format {
GLenum internalFormat;
GLenum clientFormat;
GLenum clientType;
int components;
Encoding encoding;
};
std::vector<std::uint8_t> Fill(const Format& f, int texels, float r, float g, float b) {
const float rgba[4] = {r, g, b, 1.0f};
const std::size_t componentBytes = f.encoding == Encoding::Float32 ? 4u : 1u;
std::vector<std::uint8_t> out(static_cast<std::size_t>(texels) * f.components * componentBytes);
for (int t = 0; t < texels; ++t) {
for (int c = 0; c < f.components; ++c) {
std::uint8_t* dst = out.data() + (static_cast<std::size_t>(t) * f.components + c) * componentBytes;
const float v = rgba[c];
switch (f.encoding) {
case Encoding::Unorm8: *dst = static_cast<std::uint8_t>(std::lround(v * 255.0f)); break;
case Encoding::Snorm8:
*dst = static_cast<std::uint8_t>(static_cast<std::int8_t>(std::lround(v * 127.0f)));
break;
case Encoding::Float32: std::memcpy(dst, &v, sizeof(v)); break;
case Encoding::Uint8: *dst = static_cast<std::uint8_t>(std::lround(v * kIntegerOne)); break;
}
}
}
return out;
}
class PartialUploadOverGpuContentScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_scratch = MakeColorFbo(4, 4);
glGenVertexArrays(1, &m_vao);
glGenFramebuffers(1, &m_fbo);
FirstGLError();
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
for (GLuint p : m_programs) glDeleteProgram(p);
if (!m_textures.empty()) glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
if (m_vao) glDeleteVertexArrays(1, &m_vao);
if (m_fbo) glDeleteFramebuffers(1, &m_fbo);
BindDefaultFramebuffer();
DestroyColorFbo(m_scratch);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
GLuint NewTexture(GLenum target) {
GLuint t = 0;
glGenTextures(1, &t);
m_textures.push_back(t);
glBindTexture(target, t);
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(target, GL_TEXTURE_MAX_LEVEL, 0);
glBindTexture(target, 0);
return t;
}
// A draw that samples the texture, which is what syncs its pending writes to the
// driver (an FBO attach alone may find the framebuffer already synced).
void SyncBySampling(GLenum target, GLuint tex, const char* samplerType, const char* coord) {
std::string fs = std::string("#version 430 core\nuniform ") + samplerType +
" u_tex;\nout vec4 o_color;\nvoid main() { o_color = vec4(texelFetch(u_tex, " + coord +
", 0)); }\n";
std::string error;
const GLuint program = CompileProgram(kVS, fs.c_str(), &error);
ASSERT_NE(program, 0u) << error;
m_programs.push_back(program);
BindFbo(m_scratch);
glUseProgram(program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(target, tex);
glUniform1i(glGetUniformLocation(program, "u_tex"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glBindTexture(target, 0);
glUseProgram(0);
BindDefaultFramebuffer();
glFinish();
EXPECT_EQ(FirstGLError(), 0u) << "sampling sync";
}
// Attaches (tex, layer) to the scratch framebuffer; false when this backend cannot.
bool Attach(GLuint tex, int layer) {
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
if (layer >= 0) {
glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, tex, 0, layer);
} else {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
}
return glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
}
void Detach() {
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
}
// The GPU write the shadow never sees: the whole level (or layer) to green.
void GpuClearGreen(const Format& f) {
if (f.encoding == Encoding::Uint8) {
const GLuint green[4] = {0u, kIntegerOne, 0u, 1u};
glClearBufferuiv(GL_COLOR, 0, green);
} else {
const GLfloat green[4] = {0.0f, 1.0f, 0.0f, 1.0f};
glClearBufferfv(GL_COLOR, 0, green);
}
}
// The attached image's rgb, in units of "one", width x height texels.
std::vector<float> ReadRgb(const Format& f, int width, int height) {
std::vector<float> rgb(static_cast<std::size_t>(width * height) * 3u);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
if (f.encoding == Encoding::Uint8) {
std::vector<GLuint> texels(static_cast<std::size_t>(width * height) * 4u, 0xA5A5A5A5u);
glReadPixels(0, 0, width, height, GL_RGBA_INTEGER, GL_UNSIGNED_INT, texels.data());
for (int i = 0; i < width * height; ++i)
for (int c = 0; c < 3; ++c)
rgb[i * 3 + c] = static_cast<float>(texels[i * 4 + c]) / kIntegerOne;
} else {
std::vector<GLfloat> texels(static_cast<std::size_t>(width * height) * 4u, -7.0f);
glReadPixels(0, 0, width, height, GL_RGBA, GL_FLOAT, texels.data());
for (int i = 0; i < width * height; ++i)
for (int c = 0; c < 3; ++c) rgb[i * 3 + c] = texels[i * 4 + c];
}
EXPECT_EQ(FirstGLError(), 0u) << "readback";
return rgb;
}
// Green everywhere but blue inside [boxX, boxX + boxW) x [boxY, boxY + boxH).
static void ExpectClearAroundBox(const std::vector<float>& rgb, int width, int height, int boxX, int boxY,
int boxW, int boxH) {
int bad = 0;
std::ostringstream first;
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x) {
const bool inBox = x >= boxX && x < boxX + boxW && y >= boxY && y < boxY + boxH;
const float want[3] = {0.0f, inBox ? 0.0f : 1.0f, inBox ? 1.0f : 0.0f};
const float* got = &rgb[static_cast<std::size_t>(y * width + x) * 3u];
bool texelBad = false;
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
if (texelBad && bad++ == 0) {
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
<< " want " << want[0] << "," << want[1] << "," << want[2];
}
}
EXPECT_EQ(bad, 0) << bad << " of " << width * height << " texels wrong; first " << first.str()
<< " (red outside the box: the stale shadow went back up; green inside it: the "
"write landed before the clear or draw)";
}
// Upload red, sync, clear green on the GPU, sync, write a blue 2x2 box, sync, read.
void Run2D(const Format& f) {
const GLuint t = NewTexture(GL_TEXTURE_2D);
const bool integer = f.encoding == Encoding::Uint8;
const char* sampler = integer ? "usampler2D" : "sampler2D";
const auto red = Fill(f, kEdge * kEdge, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
f.clientType, red.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
if (!Attach(t, -1)) {
Detach();
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
}
GpuClearGreen(f);
Detach();
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(f, kEdge, kEdge);
Detach();
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge);
}
// P11 M: TWO SCATTERED WRITES INTO ONE LEVEL, so the level carries a rect list (their
// area is well under 3/4 of their union box) and not just a box. Under a transport
// (and on the push builds' monolith arm) the list crosses in the client's record,
// whose regions state their source pitch and offset in the CLIENT's layout; a level
// whose upload is converted is cut from the conversion's wider buffer, so those
// numbers must not address it (Managers.cpp, carriedLayoutApplies). The whole level
// is uploaded green first, so every texel the union box spans outside the two boxes
// is green in the shadow too: this case is about WHERE the boxes land, not about GPU
// content between them. Green everywhere but the two blue boxes.
void Run2DScatter(const Format& f) {
constexpr int kFirstAt = 1, kSecondAt = 5;
const GLuint t = NewTexture(GL_TEXTURE_2D);
const bool integer = f.encoding == Encoding::Uint8;
const char* sampler = integer ? "usampler2D" : "sampler2D";
const auto green = Fill(f, kEdge * kEdge, 0.0f, 1.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, static_cast<GLint>(f.internalFormat), kEdge, kEdge, 0, f.clientFormat,
f.clientType, green.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
if (!Attach(t, -1)) {
Detach();
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
GTEST_SKIP() << "the format does not attach on backend " << Gl().BackendName();
}
Detach();
const auto blue = Fill(f, kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, kFirstAt, kFirstAt, kBoxEdge, kBoxEdge, f.clientFormat, f.clientType,
blue.data());
glTexSubImage2D(GL_TEXTURE_2D, 0, kSecondAt, kSecondAt, kBoxEdge, kBoxEdge, f.clientFormat,
f.clientType, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
SyncBySampling(GL_TEXTURE_2D, t, sampler, "ivec2(0)");
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(f, kEdge, kEdge);
Detach();
int bad = 0;
std::ostringstream first;
for (int y = 0; y < kEdge; ++y)
for (int x = 0; x < kEdge; ++x) {
const auto inBox = [&](int at) {
return x >= at && x < at + kBoxEdge && y >= at && y < at + kBoxEdge;
};
const bool blueHere = inBox(kFirstAt) || inBox(kSecondAt);
const float want[3] = {0.0f, blueHere ? 0.0f : 1.0f, blueHere ? 1.0f : 0.0f};
const float* got = &rgb[static_cast<std::size_t>(y * kEdge + x) * 3u];
bool texelBad = false;
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
if (texelBad && bad++ == 0) {
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
<< " want " << want[0] << "," << want[1] << "," << want[2];
}
}
EXPECT_EQ(bad, 0) << bad << " of " << kEdge * kEdge << " texels wrong; first " << first.str()
<< " (a box read at the wrong pitch or offset lands the wrong bytes)";
}
// Building blocks of the RGBA8 ordering cases: the same texels, issued in the orders
// a backend that defers work may get wrong.
static Format Rgba8() { return {GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8}; }
GLuint NewRedRgba8() {
const GLuint t = NewTexture(GL_TEXTURE_2D);
const auto red = Fill(Rgba8(), kEdge * kEdge, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glBindTexture(GL_TEXTURE_2D, 0);
EXPECT_EQ(FirstGLError(), 0u) << "definition";
return t;
}
void SampleRgba8(GLuint t) { SyncBySampling(GL_TEXTURE_2D, t, "sampler2D", "ivec2(0)"); }
void ClearRgba8Green(GLuint t) {
ASSERT_TRUE(Attach(t, -1));
GpuClearGreen(Rgba8());
Detach();
}
// A draw that writes green over the whole level.
void DrawRgba8Green(GLuint t) {
std::string error;
const GLuint program = CompileProgram(
kVS, "#version 430 core\nout vec4 o_color;\nvoid main() { o_color = vec4(0.0, 1.0, 0.0, 1.0); }\n",
&error);
ASSERT_NE(program, 0u) << error;
m_programs.push_back(program);
ASSERT_TRUE(Attach(t, -1));
glViewport(0, 0, kEdge, kEdge);
glUseProgram(program);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glUseProgram(0);
Detach();
glViewport(0, 0, Gl().Width(), Gl().Height());
}
void WriteRgba8BlueBox(GLuint t) {
const auto blue = Fill(Rgba8(), kBoxEdge * kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_2D, t);
glTexSubImage2D(GL_TEXTURE_2D, 0, kBoxAt, kBoxAt, kBoxEdge, kBoxEdge, GL_RGBA, GL_UNSIGNED_BYTE,
blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
}
// Green everywhere, and blue in the box when `boxWins`.
void ExpectRgba8(GLuint t, bool boxWins) {
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(Rgba8(), kEdge, kEdge);
Detach();
ExpectClearAroundBox(rgb, kEdge, kEdge, kBoxAt, kBoxAt, boxWins ? kBoxEdge : 0, boxWins ? kBoxEdge : 0);
}
struct Box {
int x, y, w, h;
};
// An edge x edge RGBA8 level uploaded red, cleared green on the GPU, then written blue in
// every box by one glTexSubImage each, with no sync in between; read back after a sync.
// Blue inside the boxes, green everywhere else.
void RunScatteredWrites(int edge, const std::vector<Box>& boxes) {
const GLuint t = NewTexture(GL_TEXTURE_2D);
const auto red = Fill(Rgba8(), edge * edge, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_2D, t);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, edge, edge, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glBindTexture(GL_TEXTURE_2D, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SampleRgba8(t);
ClearRgba8Green(t);
SampleRgba8(t);
glBindTexture(GL_TEXTURE_2D, t);
for (const Box& box : boxes) {
const auto blue = Fill(Rgba8(), box.w * box.h, 0.0f, 0.0f, 1.0f);
glTexSubImage2D(GL_TEXTURE_2D, 0, box.x, box.y, box.w, box.h, GL_RGBA, GL_UNSIGNED_BYTE,
blue.data());
}
glBindTexture(GL_TEXTURE_2D, 0);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
ASSERT_EQ(FirstGLError(), 0u) << "writes";
SampleRgba8(t);
ASSERT_TRUE(Attach(t, -1));
const auto rgb = ReadRgb(Rgba8(), edge, edge);
Detach();
int bad = 0, stale = 0;
std::ostringstream first;
for (int y = 0; y < edge; ++y)
for (int x = 0; x < edge; ++x) {
bool inBox = false;
for (const Box& box : boxes)
inBox |= x >= box.x && x < box.x + box.w && y >= box.y && y < box.y + box.h;
const float want[3] = {0.0f, inBox ? 0.0f : 1.0f, inBox ? 1.0f : 0.0f};
const float* got = &rgb[static_cast<std::size_t>(y * edge + x) * 3u];
bool texelBad = false;
for (int c = 0; c < 3; ++c) texelBad |= std::fabs(got[c] - want[c]) > 0.02f;
if (!texelBad) continue;
stale += got[0] > 0.5f;
if (bad++ == 0) {
first << "(" << x << "," << y << ") got " << got[0] << "," << got[1] << "," << got[2]
<< " want " << want[0] << "," << want[1] << "," << want[2];
}
}
EXPECT_EQ(bad, 0) << bad << " of " << edge * edge << " texels wrong, " << stale
<< " of them red - the stale shadow went up outside the writes; first "
<< first.str();
}
GLuint m_vao = 0, m_fbo = 0;
ColorFbo m_scratch{};
std::vector<GLuint> m_programs;
std::vector<GLuint> m_textures;
};
// Control: the shadow's bytes go to the driver as they are.
TEST_F(PartialUploadOverGpuContentScenario, Rgba8SubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Srgb8SubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb8SnormSubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGB8_SNORM, GL_RGB, GL_BYTE, 3, Encoding::Snorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb16fSubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGB16F, GL_RGB, GL_FLOAT, 3, Encoding::Float32});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiSubImageKeepsTheGpuClear) {
if (!Ready()) return;
Run2D({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
}
// P11 M: the rect-list shape of the same upload (Run2DScatter). RGBA8 is the control whose
// record pitch does address the uploaded bytes; the other two are stored four-channel on
// llvmpipe and on every ES driver, so their boxes are cut from the conversion's buffer.
TEST_F(PartialUploadOverGpuContentScenario, Rgba8ScatteredSubImagesLandWhereWritten) {
if (!Ready()) return;
Run2DScatter({GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Srgb8ScatteredSubImagesLandWhereWritten) {
if (!Ready()) return;
Run2DScatter({GL_SRGB8, GL_RGB, GL_UNSIGNED_BYTE, 3, Encoding::Unorm8});
}
TEST_F(PartialUploadOverGpuContentScenario, Rgb8uiScatteredSubImagesLandWhereWritten) {
if (!Ready()) return;
Run2DScatter({GL_RGB8UI, GL_RGB_INTEGER, GL_UNSIGNED_BYTE, 3, Encoding::Uint8});
}
// A 1D array's ES image is a 2D array one texel high with the layers in depth, so the
// upload is shaped differently from the shadow (layers as rows) even with no conversion.
TEST_F(PartialUploadOverGpuContentScenario, OneDimensionalArraySubImageKeepsTheGpuClearedLayer) {
if (!Ready()) return;
constexpr int kLayers = 2;
const Format f{GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, 4, Encoding::Unorm8};
const GLuint t = NewTexture(GL_TEXTURE_1D_ARRAY);
const auto red = Fill(f, kEdge * kLayers, 1.0f, 0.0f, 0.0f);
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_RGBA8, kEdge, kLayers, 0, GL_RGBA, GL_UNSIGNED_BYTE, red.data());
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
ASSERT_EQ(FirstGLError(), 0u) << "definition";
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
if (!Attach(t, 1)) {
Detach();
GTEST_SKIP() << "a 1D array layer does not attach on backend " << Gl().BackendName();
}
GpuClearGreen(f);
Detach();
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
const auto blue = Fill(f, kBoxEdge, 0.0f, 0.0f, 1.0f);
glBindTexture(GL_TEXTURE_1D_ARRAY, t);
glTexSubImage2D(GL_TEXTURE_1D_ARRAY, 0, kBoxAt, 1, kBoxEdge, 1, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_1D_ARRAY, 0);
SyncBySampling(GL_TEXTURE_1D_ARRAY, t, "sampler1DArray", "ivec2(0)");
ASSERT_TRUE(Attach(t, 1));
const auto layer1 = ReadRgb(f, kEdge, 1);
Detach();
ExpectClearAroundBox(layer1, kEdge, 1, kBoxAt, 0, kBoxEdge, 1);
}
// The orders below matter to a backend that defers GPU work (Magma queues clears and runs
// texel uploads in a batch ahead of the frame's recorded commands); every one is plain GL.
// The clear is still queued when the write arrives: it has to reach the level first.
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClear) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
ClearRgba8Green(t);
WriteRgba8BlueBox(t);
SampleRgba8(t);
ExpectRgba8(t, true);
}
// Same, with the readback as the first thing to sync the write.
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterAQueuedClearLandsOnTheClearForAReadback) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
ClearRgba8Green(t);
WriteRgba8BlueBox(t);
ExpectRgba8(t, true);
}
// The other order: the write is still pending when the clear arrives, and the clear wins.
TEST_F(PartialUploadOverGpuContentScenario, ClearAfterAPendingSubImageWins) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
WriteRgba8BlueBox(t);
ClearRgba8Green(t);
SampleRgba8(t);
ExpectRgba8(t, false);
}
// A draw into the level instead of a clear.
TEST_F(PartialUploadOverGpuContentScenario, SubImageAfterADrawIntoTheLevelLandsOnTheDraw) {
if (!Ready()) return;
const GLuint t = NewRedRgba8();
SampleRgba8(t);
DrawRgba8Green(t);
WriteRgba8BlueBox(t);
SampleRgba8(t);
ExpectRgba8(t, true);
}
// Two boxes in opposite corners: their union box is 7x7, of which the writes are 8 texels.
TEST_F(PartialUploadOverGpuContentScenario, TwoDistantSubImagesKeepTheGpuClearBetweenThem) {
if (!Ready()) return;
RunScatteredWrites(kEdge, {{0, 0, 2, 2}, {5, 5, 2, 2}});
}
// A hundred 2x2 boxes two texels apart: more than the 96 rects the storage keeps, so its
// list folds neighbours together, gaps and all.
TEST_F(PartialUploadOverGpuContentScenario, AHundredSubImagesKeepTheGpuClearBetweenThem) {
if (!Ready()) return;
std::vector<Box> boxes;
for (int j = 0; j < 10; ++j)
for (int i = 0; i < 10; ++i) boxes.push_back({4 * i + 1, 4 * j + 1, 2, 2});
RunScatteredWrites(40, boxes);
}
// Four 4x4 boxes around a one-texel cross: 64 of their union's 81 texels, past the 3/4 at
// which the storage stops handing its rect list out.
TEST_F(PartialUploadOverGpuContentScenario, SubImagesFillingThreeQuartersOfTheirUnionKeepTheGpuClearBetweenThem) {
if (!Ready()) return;
RunScatteredWrites(16, {{2, 2, 4, 4}, {7, 2, 4, 4}, {2, 7, 4, 4}, {7, 7, 4, 4}});
}
// Three quadrants: the union box is the whole level, and the list merges the touching
// boxes into it; the fourth quadrant is still the GPU's.
TEST_F(PartialUploadOverGpuContentScenario, SubImagesWhoseUnionSpansTheLevelKeepTheGpuClearInTheGap) {
if (!Ready()) return;
RunScatteredWrites(kEdge, {{0, 0, 4, 4}, {4, 0, 4, 4}, {0, 4, 4, 4}});
}
} // namespace
} // namespace MGITest
@@ -30,7 +30,9 @@ TEST_F(SyncWireScenario, FenceWaitStatusAndDeletionCrossAndPreserveRenderedPixel
glWaitSync(sync, 0, GL_TIMEOUT_IGNORED);
glDeleteSync(sync);
EXPECT_FALSE(glIsSync(sync));
EXPECT_GE(PeekSplitRuntime().emitSeq, before + 5);
// Create, the server-side wait and the delete always cross; since P10 the two polls may be
// answered from the server's fence report instead (FencePollScenario), so they are not counted.
EXPECT_GE(PeekSplitRuntime().emitSeq, before + 3);
GLubyte pixel[4]{};
glReadPixels(1, 1, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);
const int expected[4] = {64, 128, 191, 255};
@@ -0,0 +1,610 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/TextureRemintPullScenario.cpp
// Copyright (c) 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
//
// Scenario - A TEXTURE RE-MINT NEEDS NO BYTE FROM THE CLIENT (P9 W2, notes/p9/W2-REMINT.md).
//
// The name is the ROADMAP's: P4a named `texture-remint-pull` the one new stall class of the
// split - a texture first bound to an image unit AFTER the backend allocated it has to be
// re-minted in image-bindable (possibly channel-widened) storage, and design/04 §8.4 assumed the
// server held no texels to fill it with, so the levels would be PULLED back from the client. P9
// W2 measured the opposite on both backends: Espryt's server reads each level back from the old
// GPU texture and replays its own staged store for what is still pending
// (RequireImageBindableStorageByHandle), Magma copies the old image into the new one GPU to GPU
// (PreserveTextureContentsOnRecreate). No pull protocol exists, and this scenario is what says
// the re-mint is right without one.
//
// EVERY CASE HAS ONE SHAPE. Give a texture its content by one means, sync it to the backend with
// a sampling draw (so the backend really holds storage when the bind arrives - a texture whose
// first sync comes after the bind is allocated image-bindable up front and re-mints nothing),
// optionally write it on the GPU, then glBindImageTexture it and copy it with imageLoad into a
// destination that was image-bound BEFORE its own first sync (so the destination never re-mints),
// and read the destination back through a framebuffer. The expectation is the GL answer, never
// "what monolith does": the cases exist exactly where the arms can differ.
//
// A mutable, uploaded, synced the store and the GPU agree
// B mutable, uploaded, then cleared on the GPU the store is STALE, the GPU holds it
// C immutable RGBA8, uploaded, then cleared Espryt's split keeps the allocation
// D null-defined, only ever cleared on the GPU the store has NO bytes
// E cleared, then an unsynced partial upload GPU texels merged with the pending box
// F glGenerateMipmap's level 1 GPU-generated, no bytes ever crossed
// F2 an uploaded level 1, then cleared on the GPU as B, one level down
// G level 1 null-defined AFTER the last sync the driver does not hold the level
// G2 level 0 redefined larger, null, after the sync the driver holds the OLD extent
// G3 G, plus a partial upload into that level the store holds the whole level
// H widened rg8 immutable, uploaded, then cleared the carrier re-mint (Espryt promotes)
// H2 widened rg8, uploaded only
// I RGBA8_SNORM mutable, uploaded only
// J RGBA16_SNORM immutable (widened), uploaded only
//
// G and G3 were Fatal{ResourceUnavailable, "image-promotion-readback"} on Espryt's split arms
// until W2-a: the promotion read a level the driver never allocated.
//
// ARMS. Both backends on the monolith arm and on Split / Spawn / Tcp, every case. The monolith
// DirectGLES arm used to be skipped for B, C, D, E, F, F2 and H: its re-mint replayed the client's
// shadow over texels the GPU wrote (W2-REMINT.md §6, the half of ID-P9-8 left to dev). Dev's
// 568090f0 made it read the GPU level back first (AdoptDriverLevelIntoShadow), P11 M merged it,
// and the skip is retired; E is also what holds the push builds' monolith arm to merging the
// applier's pending boxes, which the frontend's dirty model no longer names there.
//
// W2-d's COUNTER. `trp=` (TextureRemintPulls) counts a re-mint of storage the backend already
// held on BOTH Espryt arms now. The peek is process-wide, so it sees the server's count only
// where the server is this process: monolith and inproc. A/H assert +1 there, C asserts +0 on
// inproc (the kept allocation is not a re-mint).
//
// W2-b's CONTROL. MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE=1 (read by the SERVER) makes every
// promotion readback fail, which a host driver cannot be made to do for real. The
// `DirectGLES.{Split,Spawn}.RemintFallback.` entries run the uploaded-only cases under it: the
// store covers those levels, so the pixels stay right and the server log names the fallback.
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/P4aFinalFixPeek.h"
#include "../Harness/PipeStatsWindow.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kEdge = 8;
constexpr const char* kForceReadbackFailure = "MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE";
constexpr const char* kFallbackMarker = "remint-readback-fallback";
constexpr const char* kVertexSource = R"(#version 430 core
void main() {
vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
}
)";
// The sync: any draw that samples the texture makes the backend allocate and upload it.
constexpr const char* kSampleSource = R"(#version 430 core
uniform sampler2D u_texture;
out vec4 o_color;
void main() { o_color = texelFetch(u_texture, ivec2(gl_FragCoord.xy) % 4, 0); }
)";
std::string CopyComputeSource(const char* sourceFormat, bool signedNormalized) {
std::string source = "#version 430 core\nlayout(local_size_x = 8, local_size_y = 8) in;\n";
source += std::string("layout(") + sourceFormat + ", binding = 0) readonly uniform image2D u_source;\n";
source += "layout(rgba8, binding = 1) writeonly uniform image2D u_destination;\n";
source += "void main() {\n ivec2 p = ivec2(gl_GlobalInvocationID.xy);\n";
source += " vec4 v = imageLoad(u_source, p);\n";
if (signedNormalized) source += " v = v * 0.5 + 0.5;\n";
source += " imageStore(u_destination, p, v);\n}\n";
return source;
}
std::vector<GLubyte> Pattern(int edge, GLubyte magic) {
std::vector<GLubyte> texels(static_cast<std::size_t>(edge * edge) * 4u);
for (int y = 0; y < edge; ++y) {
for (int x = 0; x < edge; ++x) {
const std::size_t at = static_cast<std::size_t>((y * edge + x) * 4);
texels[at + 0] = magic;
texels[at + 1] = static_cast<GLubyte>(16 * (x + 1));
texels[at + 2] = static_cast<GLubyte>(16 * (y + 1));
texels[at + 3] = 0xFF;
}
}
return texels;
}
std::vector<GLubyte> Solid(int edge, GLubyte r, GLubyte g, GLubyte b, GLubyte a) {
std::vector<GLubyte> texels(static_cast<std::size_t>(edge * edge) * 4u);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = r;
texels[i + 1] = g;
texels[i + 2] = b;
texels[i + 3] = a;
}
return texels;
}
const GLfloat kGreen[4] = {0.0f, 1.0f, 0.0f, 1.0f};
class TextureRemintPullScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
GLint computeImages = 0;
glGetIntegerv(GL_MAX_COMPUTE_IMAGE_UNIFORMS, &computeImages);
FirstGLError();
if (computeImages < 2) {
GTEST_SKIP() << "the compute stage has fewer than two image uniforms";
}
m_wire = SplitRuntimeSkipReason().empty();
m_inproc = m_wire && PeekSplitRuntime().transportName == "inproc";
m_espryt = Gl().BackendName() == "DirectGLES";
m_forced = std::getenv(kForceReadbackFailure) != nullptr;
std::string error;
m_sampleProgram = CompileProgram(kVertexSource, kSampleSource, &error);
ASSERT_NE(m_sampleProgram, 0u) << error;
m_scratch = MakeColorFbo(4, 4);
ASSERT_NE(m_scratch.fbo, 0u) << "could not create the sampling target";
glGenVertexArrays(1, &m_vao);
glGenFramebuffers(1, &m_fbo);
ASSERT_EQ(FirstGLError(), 0u);
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindTexture(GL_TEXTURE_2D, 0);
if (m_sampleProgram != 0) glDeleteProgram(m_sampleProgram);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo);
if (!m_textures.empty()) {
glDeleteTextures(static_cast<GLsizei>(m_textures.size()), m_textures.data());
}
BindDefaultFramebuffer();
if (m_scratch.fbo != 0) DestroyColorFbo(m_scratch);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
GLuint NewTexture() {
GLuint texture = 0;
glGenTextures(1, &texture);
m_textures.push_back(texture);
return texture;
}
static void NearestWithMaxLevel(GLuint texture, int maxLevel) {
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, maxLevel);
glBindTexture(GL_TEXTURE_2D, 0);
}
// A draw that samples the texture: from here on the backend holds storage for it.
void SyncBySampling(GLuint texture) {
BindFbo(m_scratch);
glUseProgram(m_sampleProgram);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(m_sampleProgram, "u_texture"), 0);
glBindVertexArray(m_vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
BindDefaultFramebuffer();
glFinish();
}
// A GPU write the client's shadow never sees.
void ClearOnTheGpu(GLuint texture, int level, const GLfloat* color) {
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, level);
ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE));
glClearBufferfv(GL_COLOR, 0, color);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
}
// THE TRANSITION AND THE READ. The destination is image-bound before its first sync,
// so only the source can re-mint; the copy goes through imageLoad, which reads the
// re-minted storage and nothing else.
std::vector<GLubyte> ImageCopy(GLuint source, int level, GLenum unitFormat, const char* glslFormat,
bool signedNormalized = false) {
const GLuint destination = NewTexture();
glBindTexture(GL_TEXTURE_2D, destination);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glBindTexture(GL_TEXTURE_2D, 0);
glBindImageTexture(1, destination, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8);
glBindImageTexture(0, source, level, GL_FALSE, 0, GL_READ_ONLY, unitFormat);
EXPECT_EQ(FirstGLError(), 0u) << "glBindImageTexture raised a GL error";
const std::string computeSource = CopyComputeSource(glslFormat, signedNormalized);
const char* text = computeSource.c_str();
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &text, nullptr);
glCompileShader(shader);
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
EXPECT_EQ(linked, GL_TRUE) << "the imageLoad copy did not link";
glUseProgram(program);
// A BARRIER THE SPEC DOES NOT ASK FOR, AND IT IS HERE ON PURPOSE. GL orders a
// framebuffer write (the GPU clears above) before a later shader image load without
// one, yet on the Espryt split arm the kept-allocation case (C, no re-mint to
// serialize anything) read the pre-clear texels in 3 of 30 runs without it and 0 of
// 30 with it, while every server-side decision was identical in passing and failing
// runs (W2-REMINT.md §7). That ordering gap is not the re-mint's question; this case
// asks what the re-minted storage HOLDS, so the ordering is pinned here.
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_ALL_BARRIER_BITS);
glUseProgram(0);
glDeleteProgram(program);
glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindImageTexture(1, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8);
glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, destination, 0);
std::vector<GLubyte> read(static_cast<std::size_t>(kEdge * kEdge) * 4u, 0xA5);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, read.data());
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, 0, 0);
BindDefaultFramebuffer();
EXPECT_EQ(FirstGLError(), 0u) << "the copy or its readback raised a GL error";
return read;
}
// The `edge` x `edge` corner of the read against `want`, `channels` channels, within 2.
void ExpectTexels(const char* what, const std::vector<GLubyte>& read, int edge,
const std::vector<GLubyte>& want, int channels = 4) {
int wrong = 0;
int firstX = -1, firstY = -1;
for (int y = 0; y < edge; ++y) {
for (int x = 0; x < edge; ++x) {
for (int c = 0; c < channels; ++c) {
const int got = read[static_cast<std::size_t>((y * kEdge + x) * 4 + c)];
const int expected = want[static_cast<std::size_t>((y * edge + x) * 4 + c)];
if (std::abs(got - expected) > 2 && wrong++ == 0) {
firstX = x;
firstY = y;
}
}
}
}
const std::size_t readAt = static_cast<std::size_t>((std::max(firstY, 0) * kEdge + std::max(firstX, 0)) * 4);
const std::size_t wantAt = static_cast<std::size_t>((std::max(firstY, 0) * edge + std::max(firstX, 0)) * 4);
EXPECT_EQ(wrong, 0) << what << ": " << wrong << " channel(s) differ; first at (" << firstX << ", "
<< firstY << ") read " << int(read[readAt]) << "," << int(read[readAt + 1]) << ","
<< int(read[readAt + 2]) << "," << int(read[readAt + 3]) << " want "
<< int(want[wantAt]) << "," << int(want[wantAt + 1]) << ","
<< int(want[wantAt + 2]) << "," << int(want[wantAt + 3]) << " (backend "
<< Gl().BackendName() << ", " << (m_wire ? "split" : "monolith") << ")";
}
// W2-d: where the peek can see the server's count (monolith, inproc) - and only on a
// push build, which is where PipeStats exists; the pull build's peek answers false (P11
// M: the skip retired above now runs H there too, and A always did).
bool RemintCountVisible() const {
unsigned long long ignored = 0;
return m_espryt && (!m_wire || m_inproc) && PeekPipeStatsTextureRemintPulls(&ignored);
}
unsigned long long RemintCount() {
unsigned long long count = 0;
EXPECT_TRUE(PeekPipeStatsTextureRemintPulls(&count));
return count;
}
// W2-b: under the forced-failure knob the server must have named its fallback.
void ExpectFallbackNamedSince(const PipeStatsWindow::LogMark& mark) {
if (!m_forced) return;
const std::string log = PipeStatsWindow::ReadServerLogSince(mark);
EXPECT_NE(log.find(kFallbackMarker), std::string::npos)
<< kForceReadbackFailure << " is set but the server log after the image bind carries no '"
<< kFallbackMarker << "' line: either the knob did not reach the server (the entry "
<< "proves nothing) or the promotion took no readback at all";
}
bool m_wire = false;
bool m_inproc = false;
bool m_espryt = false;
bool m_forced = false;
GLuint m_sampleProgram = 0;
GLuint m_vao = 0;
GLuint m_fbo = 0;
ColorFbo m_scratch{};
std::vector<GLuint> m_textures;
};
TEST_F(TextureRemintPullScenario, AnUploadedMutableTextureKeepsItsTexelsAcrossTheImageBind) {
if (!Ready()) return;
const bool counted = RemintCountVisible() && !m_forced;
const unsigned long long before = counted ? RemintCount() : 0;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("an uploaded mutable texture", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge, pattern);
ExpectFallbackNamedSince(mark);
if (counted) {
EXPECT_EQ(RemintCount(), before + 1)
<< "the re-mint of a mutable texture the backend already held was not counted (trp=)";
}
}
TEST_F(TextureRemintPullScenario, AGpuClearAfterTheUploadSurvivesTheImageBind) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
ExpectTexels("a GPU clear after the upload", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge,
Solid(kEdge, 0, 255, 0, 255));
}
TEST_F(TextureRemintPullScenario, AnImmutableTexturesGpuClearSurvivesTheImageBind) {
if (!Ready()) return;
const bool counted = RemintCountVisible() && m_inproc;
const unsigned long long before = counted ? RemintCount() : 0;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
ExpectTexels("an immutable texture's GPU clear", ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge,
Solid(kEdge, 0, 255, 0, 255));
if (counted) {
EXPECT_EQ(RemintCount(), before)
<< "an immutable core-format allocation is KEPT by the server, not re-minted, and must "
"not count (trp=)";
}
}
TEST_F(TextureRemintPullScenario, ANullDefinedTextureWrittenOnlyByTheGpuSurvivesTheImageBind) {
if (!Ready()) return;
const GLuint texture = NewTexture();
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
ExpectTexels("a null-defined texture only the GPU wrote", ImageCopy(texture, 0, GL_RGBA8, "rgba8"),
kEdge, Solid(kEdge, 0, 255, 0, 255));
}
TEST_F(TextureRemintPullScenario, APendingPartialUploadMergesOverGpuWrittenTexels) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
ClearOnTheGpu(texture, 0, kGreen);
SyncBySampling(texture);
const auto blue = Solid(2, 0, 0, 255, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 2, 2, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
glBindTexture(GL_TEXTURE_2D, 0);
auto want = Solid(kEdge, 0, 255, 0, 255);
for (int y = 2; y < 4; ++y) {
for (int x = 2; x < 4; ++x) {
const std::size_t at = static_cast<std::size_t>((y * kEdge + x) * 4);
want[at] = 0;
want[at + 1] = 0;
want[at + 2] = 255;
}
}
ExpectTexels("a pending partial upload over GPU texels", ImageCopy(texture, 0, GL_RGBA8, "rgba8"),
kEdge, want);
}
TEST_F(TextureRemintPullScenario, AGeneratedMipLevelSurvivesTheImageBind) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto solid = Solid(kEdge, 200, 100, 50, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, solid.data());
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
SyncBySampling(texture);
ExpectTexels("glGenerateMipmap's level 1", ImageCopy(texture, 1, GL_RGBA8, "rgba8"), kEdge / 2,
Solid(kEdge / 2, 200, 100, 50, 255));
}
TEST_F(TextureRemintPullScenario, AGpuClearedMipLevelSurvivesTheImageBind) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto level0 = Solid(kEdge, 200, 100, 50, 255);
const auto level1 = Solid(kEdge / 2, 10, 20, 30, 255);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, level1.data());
NearestWithMaxLevel(texture, 1);
SyncBySampling(texture);
ClearOnTheGpu(texture, 1, kGreen);
SyncBySampling(texture);
ExpectTexels("a GPU-cleared level 1", ImageCopy(texture, 1, GL_RGBA8, "rgba8"), kEdge / 2,
Solid(kEdge / 2, 0, 255, 0, 255));
}
// W2-a. Level 1 exists in the store (a null-data respecify) and on no driver: the texture's
// last sync predates it. The promotion must not read it back.
TEST_F(TextureRemintPullScenario, ALevelDefinedWithoutDataAfterTheLastSyncDoesNotEndTheSession) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectTexels("level 0 beside a level defined after the last sync",
ImageCopy(texture, 0, GL_RGBA8, "rgba8"), kEdge, pattern);
}
// W2-a's extent half: the driver holds level 0 at its OLD extent. The new contents are
// undefined (GL 4.6 core 8.5), so only "no Fatal, no GL error" is asserted.
TEST_F(TextureRemintPullScenario, ALevelRedefinedLargerWithoutDataAfterTheLastSyncDoesNotEndTheSession) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge / 2, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
(void)ImageCopy(texture, 0, GL_RGBA8, "rgba8");
EXPECT_EQ(FirstGLError(), 0u);
}
// W2-a's store half: the level is on no driver, but its whole staged run is in the store
// (the client's level shadow, the partial upload inside it), and that is what replays.
TEST_F(TextureRemintPullScenario, APartialUploadIntoALevelDefinedAfterTheLastSyncReachesTheImage) {
if (!Ready()) return;
const GLuint texture = NewTexture();
const auto pattern = Pattern(kEdge, 0x40);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kEdge, kEdge, 0, GL_RGBA, GL_UNSIGNED_BYTE, pattern.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, kEdge / 2, kEdge / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
const auto blue = Solid(2, 0, 0, 255, 255);
glTexSubImage2D(GL_TEXTURE_2D, 1, 0, 0, 2, 2, GL_RGBA, GL_UNSIGNED_BYTE, blue.data());
// Level 1 inside [BASE_LEVEL, MAX_LEVEL], or the image access is invalid (8.26).
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
glBindTexture(GL_TEXTURE_2D, 0);
ExpectTexels("a partial upload into a level defined after the last sync",
ImageCopy(texture, 1, GL_RGBA8, "rgba8"), 2, blue);
}
TEST_F(TextureRemintPullScenario, AWidenedFormatsGpuClearSurvivesTheCarrierRemint) {
if (!Ready()) return;
const bool counted = RemintCountVisible();
const unsigned long long before = counted ? RemintCount() : 0;
const GLuint texture = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u, 0x33);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const GLfloat halfQuarter[4] = {0.5f, 0.25f, 0.0f, 1.0f};
ClearOnTheGpu(texture, 0, halfQuarter);
SyncBySampling(texture);
ExpectTexels("a widened rg8 texture's GPU clear", ImageCopy(texture, 0, GL_RG8, "rg8"), kEdge,
Solid(kEdge, 128, 64, 0, 255), 2);
if (counted) {
EXPECT_EQ(RemintCount(), before + 1)
<< "the carrier re-mint of a widened texture the backend already held was not counted "
"(trp=)";
}
}
TEST_F(TextureRemintPullScenario, AWidenedFormatsUploadSurvivesTheCarrierRemint) {
if (!Ready()) return;
const GLuint texture = NewTexture();
std::vector<GLubyte> rg(static_cast<std::size_t>(kEdge * kEdge) * 2u);
for (std::size_t i = 0; i < rg.size(); i += 2) {
rg[i] = 0x33;
rg[i + 1] = 0x99;
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RG8, kEdge, kEdge);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RG, GL_UNSIGNED_BYTE, rg.data());
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("a widened rg8 upload", ImageCopy(texture, 0, GL_RG8, "rg8"), kEdge,
Solid(kEdge, 0x33, 0x99, 0, 255), 2);
ExpectFallbackNamedSince(mark);
}
TEST_F(TextureRemintPullScenario, AnRgba8SnormUploadSurvivesTheImageBind) {
if (!Ready()) return;
const GLuint texture = NewTexture();
std::vector<GLbyte> texels(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = 127;
texels[i + 1] = 0;
texels[i + 2] = -127;
texels[i + 3] = 127;
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8_SNORM, kEdge, kEdge, 0, GL_RGBA, GL_BYTE, texels.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("an RGBA8_SNORM upload", ImageCopy(texture, 0, GL_RGBA8_SNORM, "rgba8_snorm", true), kEdge,
Solid(kEdge, 255, 128, 0, 255), 3);
ExpectFallbackNamedSince(mark);
}
TEST_F(TextureRemintPullScenario, AnRgba16SnormImmutableUploadSurvivesTheCarrierRemint) {
if (!Ready()) return;
const GLuint texture = NewTexture();
std::vector<GLshort> texels(static_cast<std::size_t>(kEdge * kEdge) * 4u);
for (std::size_t i = 0; i < texels.size(); i += 4) {
texels[i] = 32767;
texels[i + 1] = 0;
texels[i + 2] = -32767;
texels[i + 3] = 32767;
}
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA16_SNORM, kEdge, kEdge);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kEdge, kEdge, GL_RGBA, GL_SHORT, texels.data());
NearestWithMaxLevel(texture, 0);
SyncBySampling(texture);
const auto mark = PipeStatsWindow::MarkLaneLog();
ExpectTexels("an RGBA16_SNORM immutable upload",
ImageCopy(texture, 0, GL_RGBA16_SNORM, "rgba16_snorm", true), kEdge,
Solid(kEdge, 255, 128, 0, 255), 3);
ExpectFallbackNamedSince(mark);
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,314 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/UnboundUniformBlockScenario.cpp
// Copyright (c) 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
//
// Scenario - A PROGRAM DECLARES A UNIFORM BLOCK AND NOTHING IS BOUND AT ITS BINDING POINT.
//
// The uniform-block sibling of UnboundImageDescriptorScenario. A draw or dispatch whose program
// declares a uniform block at an indexed point with no buffer on it raises no GL error - whether
// the point was never bound or its buffer was deleted while bound (glDeleteBuffers unbinds it
// from every indexed point of the current context). Only what the shader READS from the block is
// undefined.
//
// DirectVulkan's ResolveUniformBufferPayload looked up the buffer at the block's point and
// dereferenced it with nothing there: a SIGSEGV inside the draw or the dispatch, taking the whole
// process with it. It now answers with a zeroed block of the reflected size.
//
// The block is STATICALLY USED in every case, because an unreferenced block is optimised out
// before it ever reaches a descriptor and would prove nothing. Its one read sits behind a
// uniform-controlled branch that is false whenever the point is empty, so no undefined value
// reaches an assertion. Each case also binds a buffer at the same point and reads it back through
// the same program - proof that the block is live and wired to that point - and then empties the
// point again, so the transition back to "nothing bound" is covered as well as the first use.
//
// DirectGLES forwards the empty point to the GLES driver, which does what GL says, so it is the
// control - every test here must stay green on both backends.
#include <array>
#include <string>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#undef GL_GLEXT_PROTOTYPES
namespace MGITest {
namespace {
constexpr int kFboSize = 32;
// The point under test. Not 0, so a stray glBindBufferBase(GL_UNIFORM_BUFFER, 0, ...) from
// an earlier scenario in this process is not what decides the case - and SetUp empties
// this one explicitly anyway.
constexpr GLuint kUniformPoint = 5;
constexpr GLuint kResultPoint = 0;
constexpr unsigned int kMarker = 0xC0FFEEu;
constexpr unsigned int kFallback = 7u;
constexpr unsigned int kBoundWord = 0x1234u;
// No vertex attributes: the quad's corners come from gl_VertexID, so nothing about the
// vertex fetch can be confused with the descriptor question under test.
constexpr const char* kQuadVertexSource = R"(#version 430 core
void main() {
vec2 corner = vec2((gl_VertexID & 1) == 0 ? -1.0 : 1.0,
(gl_VertexID & 2) == 0 ? -1.0 : 1.0);
gl_Position = vec4(corner, 0.0, 1.0);
}
)";
// Green unless u_read is set, in which case the block's tint.
constexpr const char* kFragmentSource = R"(#version 430 core
layout(std140, binding = 5) uniform Target { vec4 u_tint; };
uniform int u_read;
out vec4 o_color;
void main() {
vec4 color = vec4(0.0, 1.0, 0.0, 1.0);
if (u_read != 0) {
color = u_tint;
}
o_color = color;
}
)";
// The marker is unconditional, so a zero marker means the dispatch never ran; the value is
// the fallback unless u_read is set, in which case it is the block's word.
constexpr const char* kComputeSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Result { uint g_marker; uint g_value; };
layout(std140, binding = 5) uniform Target { uint u_word; };
uniform int u_read;
void main() {
uint value = 7u;
if (u_read != 0) {
value = u_word;
}
g_marker = 0xC0FFEEu;
g_value = value;
}
)";
class UnboundUniformBlockScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_target = MakeColorFbo(kFboSize, kFboSize);
ASSERT_NE(m_target.fbo, 0u) << "could not create the render target";
glGenVertexArrays(1, &m_vao);
const unsigned int zero[2] = {0u, 0u};
glGenBuffers(1, &m_result);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(zero), zero, GL_DYNAMIC_COPY);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kResultPoint, m_result);
// The harness shares one context across every scenario in the process, so an
// earlier one may have left a buffer at this point. The whole subject here is that
// nothing is bound, so say so rather than assume it.
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
ASSERT_EQ(FirstGLError(), 0u) << "setting up the scenario raised a GL error";
}
void TearDown() override {
if (!Ready()) return;
glUseProgram(0);
glBindVertexArray(0);
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, kResultPoint, 0);
if (m_block != 0) glDeleteBuffers(1, &m_block);
if (m_result != 0) glDeleteBuffers(1, &m_result);
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_draw != 0) glDeleteProgram(m_draw);
if (m_compute != 0) glDeleteProgram(m_compute);
BindDefaultFramebuffer();
DestroyColorFbo(m_target);
glViewport(0, 0, Gl().Width(), Gl().Height());
FirstGLError();
}
unsigned int MakeComputeProgram(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint compiled = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);
if (compiled == GL_FALSE) {
char log[4096] = {};
glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute shader did not compile: " << log;
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
GLint linked = GL_FALSE;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (linked == GL_FALSE) {
char log[4096] = {};
glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
ADD_FAILURE() << "the compute program did not link: " << log;
glDeleteProgram(program);
return 0;
}
return program;
}
// The block must be ACTIVE and at kUniformPoint, and the guard uniform must exist:
// if either were optimised away the program would never ask for the empty point and
// every case below would pass without testing anything.
static void ExpectTheBlockIsLiveAtThePoint(GLuint program) {
const GLuint block = glGetUniformBlockIndex(program, "Target");
ASSERT_NE(block, GL_INVALID_INDEX) << "the uniform block was optimised out - the case proves nothing";
GLint binding = -1;
glGetActiveUniformBlockiv(program, block, GL_UNIFORM_BLOCK_BINDING, &binding);
ASSERT_EQ(binding, static_cast<GLint>(kUniformPoint)) << "the block is not at the point under test";
ASSERT_NE(glGetUniformLocation(program, "u_read"), -1) << "the guard uniform was optimised out";
ASSERT_EQ(FirstGLError(), 0u) << "querying the block raised a GL error";
}
static void ExpectThePointIsEmpty(const char* when) {
GLint bound = -1;
glGetIntegeri_v(GL_UNIFORM_BUFFER_BINDING, kUniformPoint, &bound);
ASSERT_EQ(bound, 0) << "uniform point " << kUniformPoint << " is not empty " << when;
}
// A 16-byte std140 block at kUniformPoint holding `words`.
void BindBlock(const std::array<unsigned int, 4>& words) {
if (m_block == 0) glGenBuffers(1, &m_block);
glBindBuffer(GL_UNIFORM_BUFFER, m_block);
glBufferData(GL_UNIFORM_BUFFER, sizeof(words), words.data(), GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, m_block);
}
// Runs the compute program once; returns {marker, value}.
std::array<unsigned int, 2> Dispatch(int read) {
const unsigned int zero[2] = {0u, 0u};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(zero), zero);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glUseProgram(m_compute);
glUniform1i(glGetUniformLocation(m_compute, "u_read"), read);
glDispatchCompute(1, 1, 1);
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
std::array<unsigned int, 2> out = {0xDEADBEEFu, 0xDEADBEEFu};
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_result);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, sizeof(out), out.data());
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
glUseProgram(0);
return out;
}
void ExpectTheDispatchRunsOnAnEmptyPoint(const char* when) {
ASSERT_NO_FATAL_FAILURE(ExpectThePointIsEmpty(when));
const auto result = Dispatch(0);
EXPECT_EQ(FirstGLError(), 0u) << "the dispatch raised a GL error " << when;
EXPECT_EQ(result[0], kMarker) << "the dispatch did not run " << when;
EXPECT_EQ(result[1], kFallback) << "the dispatch took the block's branch " << when;
}
// Draws the full-target quad and expects every pixel to be `color`.
void ExpectTheDraw(int read, const char* color, const std::string& when) {
BindFbo(m_target);
ClearTo(0.0f, 0.0f, 0.0f, 1.0f);
glBindVertexArray(m_vao);
glUseProgram(m_draw);
glUniform1i(glGetUniformLocation(m_draw, "u_read"), read);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glUseProgram(0);
EXPECT_EQ(FirstGLError(), 0u) << "the draw raised a GL error " << when;
const Image image = ReadPixels(kFboSize, kFboSize);
ASSERT_FALSE(image.Empty()) << "the readback came back empty " << when;
EXPECT_TRUE(RegionIsMostly(image, 0, kFboSize - 1, 0, kFboSize - 1, color, 0.0, when))
<< "an all-black target means the draw was dropped";
}
void ExpectTheDrawRunsOnAnEmptyPoint(const std::string& when) {
ASSERT_NO_FATAL_FAILURE(ExpectThePointIsEmpty(when.c_str()));
ExpectTheDraw(0, "green", when);
}
ColorFbo m_target{};
GLuint m_vao = 0;
GLuint m_result = 0;
GLuint m_block = 0;
unsigned int m_draw = 0;
unsigned int m_compute = 0;
};
} // namespace
TEST_F(UnboundUniformBlockScenario, ADispatchDeclaringAnEmptyUniformPointRuns) {
if (!Ready() || IsSkipped()) return;
m_compute = MakeComputeProgram(kComputeSource);
ASSERT_NE(m_compute, 0u);
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_compute));
ExpectTheDispatchRunsOnAnEmptyPoint("with nothing ever bound at the point");
BindBlock({kBoundWord, 0u, 0u, 0u});
const auto bound = Dispatch(1);
EXPECT_EQ(FirstGLError(), 0u);
EXPECT_EQ(bound[0], kMarker) << "the dispatch did not run with a buffer bound";
EXPECT_EQ(bound[1], kBoundWord) << "the program did not read the buffer bound at the block's point";
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
ExpectTheDispatchRunsOnAnEmptyPoint("after the point was emptied again");
}
TEST_F(UnboundUniformBlockScenario, ADrawDeclaringAnEmptyUniformPointPaintsTheFrame) {
if (!Ready() || IsSkipped()) return;
std::string error;
m_draw = CompileProgram(kQuadVertexSource, kFragmentSource, &error);
ASSERT_NE(m_draw, 0u) << error;
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_draw));
ExpectTheDrawRunsOnAnEmptyPoint("with nothing ever bound at the point");
// Opaque red: 1.0f and 0.0f as their IEEE-754 words, so the block is one vec4.
BindBlock({0x3F800000u, 0u, 0u, 0x3F800000u});
ExpectTheDraw(1, "red", "reading the buffer bound at the block's point");
glBindBufferBase(GL_UNIFORM_BUFFER, kUniformPoint, 0);
ExpectTheDrawRunsOnAnEmptyPoint("after the point was emptied again");
}
// The shape real applications reach it by: the buffer is bound and used, then deleted while
// still bound, which empties the point without the application ever unbinding it.
TEST_F(UnboundUniformBlockScenario, AUniformBufferDeletedWhileBoundLeavesAnEmptyPointBothPathsSurvive) {
if (!Ready() || IsSkipped()) return;
m_compute = MakeComputeProgram(kComputeSource);
ASSERT_NE(m_compute, 0u);
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_compute));
std::string error;
m_draw = CompileProgram(kQuadVertexSource, kFragmentSource, &error);
ASSERT_NE(m_draw, 0u) << error;
ASSERT_NO_FATAL_FAILURE(ExpectTheBlockIsLiveAtThePoint(m_draw));
BindBlock({kBoundWord, 0u, 0u, 0u});
const auto bound = Dispatch(1);
EXPECT_EQ(bound[0], kMarker) << "the dispatch did not run with a buffer bound";
EXPECT_EQ(bound[1], kBoundWord) << "the program did not read the buffer bound at the block's point";
ASSERT_EQ(FirstGLError(), 0u);
glDeleteBuffers(1, &m_block);
m_block = 0;
ExpectTheDispatchRunsOnAnEmptyPoint("after the bound buffer was deleted");
ExpectTheDrawRunsOnAnEmptyPoint("after the bound buffer was deleted");
}
} // namespace MGITest
@@ -0,0 +1,292 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDispatchScenario.cpp
// Copyright (c) 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
//
// P8-SV: THE MAGMA WIRE ARM ISSUES glDispatchComputeIndirect FROM THE WIRE STORE, AS THE MONOLITH
// ARM DOES (docs/Disaggregated/notes/p8/SV.md).
//
// Before P8-SV the wire arm read the three group counts through ReadWireBuffer and dispatched them
// from the CPU, so a dispatch buffer a shader had written cost a whole-GPU wait per call - the
// dispatch twin of what P8-D removed for indirect draws (WireIndirectDrawScenario.cpp). The cases
// pin both halves:
// * THE GRID, on every arm and both backends - each dispatch marks exactly the work groups its
// counts name, and GPU-written counts are the ones the dispatch sees (the buffer starts with
// OTHER counts, so a stale read shows as the wrong grid);
// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on the
// inproc arm: every call issued as vkCmdDispatchIndirect (`wdsp`), no host wait (`whw`), and the
// INDIRECT_COMMAND_READ barrier exactly once per shader write (`wibar`).
// The counters are read BEFORE the grids are read back: that readback is a host read of a store a
// shader wrote, so it takes a host wait of its own, which is the application's and not the
// dispatch's.
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#include "../Harness/WireIndirectPeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
namespace MGITest {
namespace {
// Grids up to 4 x 4 x 2: one cell per work group.
constexpr std::uint32_t kGridX = 4, kGridY = 4, kGridZ = 2;
constexpr std::uint32_t kCells = kGridX * kGridY * kGridZ;
// One invocation per work group, and each group marks its own cell.
constexpr const char* kMarkSource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 2) buffer Marks { uint marks[]; };
void main() {
uvec3 g = gl_WorkGroupID;
atomicAdd(marks[(g.z * 4u + g.y) * 4u + g.x], 1u);
}
)";
// Copies `source` into `words`, one word per invocation: the compute write under test.
constexpr const char* kCopySource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Words { uint words[]; };
layout(std430, binding = 1) readonly buffer Source { uint source[]; };
void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; }
)";
using Grid = std::array<std::uint32_t, 3>;
class WireIndirectDispatchScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_magma = Gl().BackendName() == "DirectVulkan";
m_transport = PeekSplitRuntime().transportName;
m_mark = CompileCompute(kMarkSource);
ASSERT_NE(m_mark, 0u);
m_copy = CompileCompute(kCopySource);
ASSERT_NE(m_copy, 0u);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
// The counters are armed by the first peek, so it has to come before any dispatch.
if (CountersReadable()) {
ASSERT_TRUE(PeekWireIndirectCounters(&m_before))
<< "Magma on the inproc arm must be able to read the wire arm's own counters";
}
}
void TearDown() override {
if (!Ready()) return;
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, 0);
for (GLuint& buffer : m_marks) {
if (buffer != 0) glDeleteBuffers(1, &buffer);
}
for (GLuint* buffer : {&m_groupCounts, &m_source}) {
if (*buffer != 0) glDeleteBuffers(1, buffer);
*buffer = 0;
}
if (m_mark != 0) glDeleteProgram(m_mark);
if (m_copy != 0) glDeleteProgram(m_copy);
}
static GLuint CompileCompute(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (ok != GL_TRUE) {
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
glGetProgramiv(program, GL_LINK_STATUS, &ok);
if (ok != GL_TRUE) {
glDeleteProgram(program);
return 0;
}
return program;
}
static void Fill(GLuint& name, GLenum target, const std::vector<std::uint32_t>& words) {
if (name == 0) glGenBuffers(1, &name);
glBindBuffer(target, name);
glBufferData(target, static_cast<GLsizeiptr>(words.size() * sizeof(std::uint32_t)), words.data(),
GL_DYNAMIC_COPY);
}
// The dispatch-indirect store, holding CPU-written counts.
void Parameters(const std::vector<std::uint32_t>& words) {
Fill(m_groupCounts, GL_DISPATCH_INDIRECT_BUFFER, words);
}
// The compute write: the parameter store (already holding its poison) receives `words`.
void ComputeWriteParameters(const std::vector<std::uint32_t>& words) {
Fill(m_source, GL_SHADER_STORAGE_BUFFER, words);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_groupCounts);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source);
glUseProgram(m_copy);
glDispatchCompute(static_cast<GLuint>(words.size()), 1, 1);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
}
// One indirect dispatch of the mark program into a fresh, zeroed cell buffer. Returns
// the index of that buffer for ExpectGrid.
size_t DispatchIndirect(GLintptr offset) {
GLuint marks = 0;
Fill(marks, GL_SHADER_STORAGE_BUFFER, std::vector<std::uint32_t>(kCells, 0u));
m_marks.push_back(marks);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, marks);
glUseProgram(m_mark);
glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, m_groupCounts);
glDispatchComputeIndirect(offset);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, 0);
return m_marks.size() - 1;
}
// A round trip that reads no buffer store, so the server has applied every dispatch
// above before the counters are read - and no host wait of its own is added.
static void Settle() {
BindDefaultFramebuffer();
(void)ReadPixels(1, 1);
}
void ExpectGrid(size_t which, const Grid& grid, const std::string& what) {
glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT);
std::vector<std::uint32_t> cells(kCells, 0xDEADu);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_marks[which]);
glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, static_cast<GLsizeiptr>(kCells * sizeof(std::uint32_t)),
cells.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << what << ": readback";
size_t wrong = 0;
std::string first;
for (std::uint32_t z = 0; z < kGridZ; ++z) {
for (std::uint32_t y = 0; y < kGridY; ++y) {
for (std::uint32_t x = 0; x < kGridX; ++x) {
const bool inside = x < grid[0] && y < grid[1] && z < grid[2];
const std::uint32_t got = cells[(z * kGridY + y) * kGridX + x];
if (got != (inside ? 1u : 0u) && wrong++ == 0) {
first = "group (" + std::to_string(x) + "," + std::to_string(y) + "," +
std::to_string(z) + ") = " + std::to_string(got);
}
}
}
}
EXPECT_EQ(wrong, 0u) << what << ": expected exactly the " << grid[0] << "x" << grid[1] << "x"
<< grid[2] << " grid marked once; first wrong " << first;
}
// Magma's server in THIS process: the inproc arm. Spawn and tcp servers are other
// processes, and the monolith arm never takes the wire path at all.
bool CountersReadable() const { return m_magma && m_transport == "inproc"; }
WireIndirectCounters Delta() const {
WireIndirectCounters now{};
EXPECT_TRUE(PeekWireIndirectCounters(&now));
WireIndirectCounters delta{};
delta.nativeDraws = now.nativeDraws - m_before.nativeDraws;
delta.cpuExpansions = now.cpuExpansions - m_before.cpuExpansions;
delta.barriers = now.barriers - m_before.barriers;
delta.hostWaits = now.hostWaits - m_before.hostWaits;
delta.indirectWaits = now.indirectWaits - m_before.indirectWaits;
delta.nativeDispatches = now.nativeDispatches - m_before.nativeDispatches;
return delta;
}
bool m_magma = false;
std::string m_transport;
WireIndirectCounters m_before{};
GLuint m_mark = 0, m_copy = 0, m_groupCounts = 0, m_source = 0;
std::vector<GLuint> m_marks;
};
// CPU-written counts at two non-zero offsets of one store. On Magma/inproc both calls must
// be issued natively, with no host wait and no barrier (no shader wrote the store).
TEST_F(WireIndirectDispatchScenario, CpuWrittenGroupCountsAreReadByTheGpuAtTheirOffsets) {
if (!Ready() || IsSkipped()) return;
Parameters({1, 1, 1, 3, 2, 1, 4, 1, 2});
const size_t first = DispatchIndirect(12);
const size_t second = DispatchIndirect(24);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Settle();
if (CountersReadable()) {
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDispatches, 2u) << "every indirect dispatch must be vkCmdDispatchIndirect";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write to the counts";
EXPECT_EQ(delta.barriers, 0u) << "no shader wrote this store, so no barrier is due";
}
ExpectGrid(first, {3, 2, 1}, "glDispatchComputeIndirect at byte 12");
ExpectGrid(second, {4, 1, 2}, "glDispatchComputeIndirect at byte 24");
}
// Counts a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT. The
// store starts with OTHER counts, so a stale read dispatches the wrong grid. On
// Magma/inproc: issued natively and - the point of the change - NO host wait.
TEST_F(WireIndirectDispatchScenario, ComputeWrittenGroupCountsDispatchWithoutAHostWait) {
if (!Ready() || IsSkipped()) return;
Parameters({1, 1, 1});
ComputeWriteParameters({3, 2, 1});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
const size_t first = DispatchIndirect(0);
Parameters({1, 1, 1, 1, 1, 1});
ComputeWriteParameters({1, 1, 1, 2, 2, 2});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
const size_t second = DispatchIndirect(12);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Settle();
if (CountersReadable()) {
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDispatches, 2u) << "both dispatches must be issued natively";
EXPECT_EQ(delta.hostWaits, 0u)
<< "an indirect dispatch of shader-written counts waited for the GPU on the host";
EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store";
}
ExpectGrid(first, {3, 2, 1}, "compute-written counts at byte 0");
ExpectGrid(second, {2, 2, 2}, "compute-written counts at byte 12");
}
// Flywheel's shape, one call over: the counts are written and dispatched with no
// glMemoryBarrier between (GL leaves it undefined). The wire arm orders it anyway, once per
// write, as P8-D does for draws. Magma's split arms only: the monolith arm records no
// dependency here (P8-D recorded that half for dev), and Espryt's is its driver's.
TEST_F(WireIndirectDispatchScenario, ShaderWrittenGroupCountsWithoutAnApplicationBarrierAreOrdered) {
if (!Ready() || IsSkipped()) return;
if (!m_magma || m_transport == "monolith") {
GTEST_SKIP() << "the implicit INDIRECT_COMMAND_READ barrier is the Magma wire arm's (P8-D, P8-SV)";
}
Parameters({1, 1, 1});
ComputeWriteParameters({3, 2, 1});
const size_t first = DispatchIndirect(0);
const size_t again = DispatchIndirect(0);
ComputeWriteParameters({1, 2, 2});
const size_t third = DispatchIndirect(0);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
Settle();
if (CountersReadable()) {
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDispatches, 3u);
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second dispatch reads no new write";
}
ExpectGrid(first, {3, 2, 1}, "the first dispatch after the write");
ExpectGrid(again, {3, 2, 1}, "a second dispatch, no write between");
ExpectGrid(third, {1, 2, 2}, "the dispatch after a second write");
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,368 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireIndirectDrawScenario.cpp
// Copyright (c) 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
//
// P8-D: THE MAGMA WIRE ARM ISSUES INDIRECT DRAWS FROM THE WIRE STORE, AS THE MONOLITH ARM DOES.
//
// Before P8-D every wire indirect draw read its commands (and its count word) through
// ReadWireBuffer and expanded them on the CPU, and a store a shader had written turned that read
// into a whole-GPU wait per draw (create-indirect: 321 expansions, 327 waits, ~4-8 s of waiting
// per replay on lavapipe). The cases pin both halves of the change:
// * PIXELS, on every arm and both backends where the backend's own semantics allow it - each
// indirect form draws exactly the strips its commands name, and GPU-written commands are the
// ones the draw sees (the poison the buffers start with names the OTHER strips);
// * THE SERVER'S OWN COUNTERS (Harness/WireIndirectPeek.h), where they can be read - Magma on
// the inproc arm, whose server is this process: every call issued natively, no CPU
// expansion, no host wait, and the INDIRECT_COMMAND_READ barrier exactly once per write.
//
// Four strips across the viewport, one per quarter; a command draws one strip. Green = drawn,
// red = the clear. The VBO opens with three padding vertices so a command that loses its `first`
// or `firstIndex` draws a degenerate triangle rather than the right strip.
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitRuntimePeek.h"
#include "../Harness/WireIndirectPeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
namespace MGITest {
namespace {
constexpr int kStrips = 4;
constexpr std::uint32_t kPad = 3;
constexpr std::uint32_t kStripVertices = 6;
constexpr const char* kVertexSource = R"(#version 430 core
layout(location = 0) in vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
)";
constexpr const char* kFragmentSource = R"(#version 430 core
out vec4 oColor;
void main() { oColor = vec4(0.0, 1.0, 0.0, 1.0); }
)";
// Copies `source` into `words`, one word per invocation: the compute write under test.
constexpr const char* kCopySource = R"(#version 430 core
layout(local_size_x = 1) in;
layout(std430, binding = 0) buffer Words { uint words[]; };
layout(std430, binding = 1) readonly buffer Source { uint source[]; };
void main() { words[gl_GlobalInvocationID.x] = source[gl_GlobalInvocationID.x]; }
)";
struct ArraysCommand {
std::uint32_t count, instanceCount, first, baseInstance;
};
struct ElementsCommand {
std::uint32_t count, instanceCount, firstIndex;
std::int32_t baseVertex;
std::uint32_t baseInstance;
};
// A padded elements command, for the stride case: 32 bytes, the tail unread.
struct PaddedElementsCommand {
ElementsCommand command;
std::uint32_t padding[3];
};
ArraysCommand Arrays(int strip) { return {kStripVertices, 1, kPad + kStripVertices * strip, 0}; }
ElementsCommand Elements(int strip) { return {kStripVertices, 1, kStripVertices * strip, 0, 0}; }
using Strips = std::array<bool, kStrips>;
class WireIndirectDrawScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_magma = Gl().BackendName() == "DirectVulkan";
m_transport = PeekSplitRuntime().transportName;
std::string error;
m_program = CompileProgram(kVertexSource, kFragmentSource, &error);
ASSERT_NE(m_program, 0u) << error;
m_copy = CompileCompute(kCopySource);
ASSERT_NE(m_copy, 0u);
std::vector<float> vertices(2 * kPad, 0.0f);
for (int strip = 0; strip < kStrips; ++strip) {
const float x0 = -1.0f + 0.5f * static_cast<float>(strip);
const float x1 = x0 + 0.5f;
const float quad[12] = {x0, -1.0f, x1, -1.0f, x1, 1.0f, x0, -1.0f, x1, 1.0f, x0, 1.0f};
vertices.insert(vertices.end(), quad, quad + 12);
}
std::vector<std::uint32_t> indices;
for (std::uint32_t i = 0; i < kStripVertices * kStrips; ++i) indices.push_back(kPad + i);
glGenVertexArrays(1, &m_vao);
glBindVertexArray(m_vao);
glGenBuffers(1, &m_vbo);
glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
glBufferData(GL_ARRAY_BUFFER, static_cast<GLsizeiptr>(vertices.size() * sizeof(float)),
vertices.data(), GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), nullptr);
glGenBuffers(1, &m_ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, static_cast<GLsizeiptr>(indices.size() * sizeof(std::uint32_t)),
indices.data(), GL_STATIC_DRAW);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "scene setup left a GL error behind";
// The counters are armed by the first peek, so it has to come before any draw.
if (CountersReadable()) {
ASSERT_TRUE(PeekWireIndirectCounters(&m_before))
<< "Magma on the inproc arm must be able to read the wire arm's own counters";
}
}
void TearDown() override {
if (!Ready()) return;
glBindVertexArray(0);
for (GLuint* buffer : {&m_vbo, &m_ebo, &m_indirect, &m_parameter, &m_source}) {
if (*buffer != 0) glDeleteBuffers(1, buffer);
*buffer = 0;
}
if (m_vao != 0) glDeleteVertexArrays(1, &m_vao);
if (m_program != 0) glDeleteProgram(m_program);
if (m_copy != 0) glDeleteProgram(m_copy);
}
static GLuint CompileCompute(const char* source) {
const GLuint shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, &source, nullptr);
glCompileShader(shader);
GLint ok = GL_FALSE;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (ok != GL_TRUE) {
glDeleteShader(shader);
return 0;
}
const GLuint program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);
glDeleteShader(shader);
glGetProgramiv(program, GL_LINK_STATUS, &ok);
if (ok != GL_TRUE) {
glDeleteProgram(program);
return 0;
}
return program;
}
template <typename T>
static void Fill(GLuint& name, GLenum target, const std::vector<T>& data) {
if (name == 0) glGenBuffers(1, &name);
glBindBuffer(target, name);
glBufferData(target, static_cast<GLsizeiptr>(data.size() * sizeof(T)), data.data(), GL_DYNAMIC_COPY);
}
// The compute write: `words` (already holding its poison) receives `data` from the GPU.
template <typename T>
void ComputeWrite(GLuint words, const std::vector<T>& data) {
Fill(m_source, GL_SHADER_STORAGE_BUFFER, data);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, words);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, m_source);
glUseProgram(m_copy);
glDispatchCompute(static_cast<GLuint>(data.size() * sizeof(T) / sizeof(std::uint32_t)), 1, 1);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, 0);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
}
template <typename DrawFn>
Image Render(DrawFn&& draw) {
BindDefaultFramebuffer();
glViewport(0, 0, Gl().Width(), Gl().Height());
glDisable(GL_DEPTH_TEST);
ClearTo(1.0f, 0.0f, 0.0f, 1.0f);
glUseProgram(m_program);
glBindVertexArray(m_vao);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, m_indirect);
draw();
return ReadPixels(Gl().Width(), Gl().Height());
}
static void ExpectStrips(const Image& image, const Strips& drawn, const std::string& what) {
for (int strip = 0; strip < kStrips; ++strip) {
const int x = image.Width() * (2 * strip + 1) / (2 * kStrips);
EXPECT_STREQ(image.ColorName(x, image.Height() / 2), drawn[strip] ? "green" : "red")
<< what << ": strip " << strip;
}
}
// Magma's server in THIS process: the inproc arm. Spawn and tcp servers are other
// processes, and the monolith arm never takes the wire path at all.
bool CountersReadable() const { return m_magma && m_transport == "inproc"; }
WireIndirectCounters Delta() const {
WireIndirectCounters now{};
EXPECT_TRUE(PeekWireIndirectCounters(&now));
return {now.nativeDraws - m_before.nativeDraws, now.cpuExpansions - m_before.cpuExpansions,
now.barriers - m_before.barriers, now.hostWaits - m_before.hostWaits,
now.indirectWaits - m_before.indirectWaits};
}
bool m_magma = false;
std::string m_transport;
WireIndirectCounters m_before{};
GLuint m_program = 0, m_copy = 0, m_vao = 0, m_vbo = 0, m_ebo = 0;
GLuint m_indirect = 0, m_parameter = 0, m_source = 0;
};
// Every indirect form from CPU-written commands. On Magma/inproc the server must have
// issued all six natively, expanded none, waited for nothing and recorded no barrier (no
// shader ever wrote these stores).
TEST_F(WireIndirectDrawScenario, EveryIndirectFormIsIssuedNativelyFromItsStore) {
if (!Ready() || IsSkipped()) return;
// Arrays commands at 0.., elements commands after them: the offsets are byte offsets
// into one store, as an application packs them.
const std::vector<ArraysCommand> arrays = {Arrays(0), Arrays(1), Arrays(3), Arrays(2)};
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, arrays);
Image image = Render([&] { glDrawArraysIndirect(GL_TRIANGLES, reinterpret_cast<const void*>(16)); });
ExpectStrips(image, {false, true, false, false}, "glDrawArraysIndirect at byte 16");
image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 3, 0); });
ExpectStrips(image, {true, true, false, true}, "glMultiDrawArraysIndirect, 3 commands");
const std::vector<PaddedElementsCommand> padded = {{Elements(3), {}}, {Elements(0), {}}};
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, padded);
image = Render([&] {
glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 2, sizeof(PaddedElementsCommand));
});
ExpectStrips(image, {true, false, false, true}, "glMultiDrawElementsIndirect, stride 32");
image = Render([&] {
glDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT,
reinterpret_cast<const void*>(sizeof(PaddedElementsCommand)));
});
ExpectStrips(image, {true, false, false, false}, "glDrawElementsIndirect at byte 32");
// The count forms: three commands each, a count word that allows fewer.
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{7, 2, 1});
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(0), Arrays(1), Arrays(2)});
image = Render([&] {
glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 4, 3, 0);
});
ExpectStrips(image, {true, true, false, false}, "glMultiDrawArraysIndirectCount, count word 2 of 3");
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(3), Elements(2), Elements(1)});
image = Render([&] {
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 8, 3, 0);
});
ExpectStrips(image, {false, false, false, true}, "glMultiDrawElementsIndirectCount, count word 1 of 3");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 6u) << "every indirect call must be issued as vkCmdDraw*Indirect[Count]";
EXPECT_EQ(delta.cpuExpansions, 0u) << "no indirect call may read its words on the CPU";
EXPECT_EQ(delta.indirectWaits, 0u) << "an indirect call took a host wait";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken with no GPU write anywhere";
EXPECT_EQ(delta.barriers, 0u) << "no shader wrote these stores, so no barrier is due";
}
// Commands a compute shader wrote, behind the application's own GL_COMMAND_BARRIER_BIT.
// The buffers start with commands for the OTHER strips, so a read of stale words shows.
// On Magma/inproc: issued natively, and - the point of the package - NO host wait.
TEST_F(WireIndirectDrawScenario, ComputeWrittenCommandsDrawWithoutAHostWait) {
if (!Ready() || IsSkipped()) return;
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(2), Arrays(3)});
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(0), Arrays(1)});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
Image image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
ExpectStrips(image, {true, true, false, false}, "glMultiDrawArraysIndirect of compute-written commands");
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(0), Elements(1)});
ComputeWrite(m_indirect, std::vector<ElementsCommand>{Elements(3), Elements(2)});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
image = Render([&] { glMultiDrawElementsIndirect(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 2, 0); });
ExpectStrips(image, {false, false, true, true}, "glMultiDrawElementsIndirect of compute-written commands");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 2u) << "both draws must be issued natively";
EXPECT_EQ(delta.cpuExpansions, 0u) << "no indirect call may read its words on the CPU";
EXPECT_EQ(delta.indirectWaits, 0u)
<< "an indirect draw of shader-written commands waited for the GPU on the host";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "one INDIRECT_COMMAND_READ barrier per freshly written store";
}
// The COUNT word a compute shader wrote. Espryt reads that word on the CPU from the
// server's staged copy of the parameter buffer; P8-C refreshes that copy after a shader
// write (SplitHostBytesForCpuRead), so its split arms answer here too (P8-SE). Espryt's
// MONOLITH arm still reads the frontend shadow without a sync (OPEN-QUESTIONS 15, C.md's
// monolith defects), left to P13 (ID-P8-13).
TEST_F(WireIndirectDrawScenario, ComputeWrittenCountWordIsTheOneTheGpuReads) {
if (!Ready() || IsSkipped()) return;
if (!m_magma && m_transport == "monolith") {
GTEST_SKIP() << "Espryt's monolith arm reads the count word from the frontend shadow without "
"a sync (OQ15; recorded for P13, ID-P8-13)";
}
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ElementsCommand>{Elements(0), Elements(3), Elements(2)});
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{3});
ComputeWrite(m_parameter, std::vector<std::uint32_t>{1});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
Image image = Render([&] {
glMultiDrawElementsIndirectCount(GL_TRIANGLES, GL_UNSIGNED_INT, nullptr, 0, 3, 0);
});
ExpectStrips(image, {true, false, false, false}, "glMultiDrawElementsIndirectCount, shader-written count 1");
if (m_transport == "monolith") {
// DirectVulkan.cpp's monolith MultiDrawArraysIndirectCount reads this word from the
// frontend shadow without SyncGpuWrites: recorded for dev (ID-P8-3), not this lane's.
return;
}
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(1), Arrays(2), Arrays(3)});
Fill(m_parameter, GL_PARAMETER_BUFFER, std::vector<std::uint32_t>{0});
ComputeWrite(m_parameter, std::vector<std::uint32_t>{2});
glMemoryBarrier(GL_COMMAND_BARRIER_BIT);
image = Render([&] { glMultiDrawArraysIndirectCount(GL_TRIANGLES, nullptr, 0, 3, 0); });
ExpectStrips(image, {false, true, true, false}, "glMultiDrawArraysIndirectCount, shader-written count 2");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 2u);
EXPECT_EQ(delta.cpuExpansions, 0u) << "a count word was read on the CPU";
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "one barrier per draw whose count store a shader wrote";
}
// Flywheel's shape (create-indirect's trace: 322 cull-then-draw rounds, no glMemoryBarrier
// between them). GL leaves it undefined; the wire arm orders it anyway, once per write,
// because the CPU path it replaced did (by waiting). Magma's split arms only: the monolith
// arm records no dependency here (recorded for dev), and Espryt's is its driver's.
TEST_F(WireIndirectDrawScenario, ShaderWrittenCommandsWithoutAnApplicationBarrierAreOrdered) {
if (!Ready() || IsSkipped()) return;
if (!m_magma || m_transport == "monolith") {
GTEST_SKIP() << "the implicit INDIRECT_COMMAND_READ barrier is the Magma wire arm's (P8-D)";
}
Fill(m_indirect, GL_DRAW_INDIRECT_BUFFER, std::vector<ArraysCommand>{Arrays(3), Arrays(2)});
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(0), Arrays(1)});
Image image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
ExpectStrips(image, {true, true, false, false}, "the first draw after the write");
image = Render([&] { glDrawArraysIndirect(GL_TRIANGLES, reinterpret_cast<const void*>(16)); });
ExpectStrips(image, {false, true, false, false}, "a second draw, no write between");
ComputeWrite(m_indirect, std::vector<ArraysCommand>{Arrays(2), Arrays(3)});
image = Render([&] { glMultiDrawArraysIndirect(GL_TRIANGLES, nullptr, 2, 0); });
ExpectStrips(image, {false, false, true, true}, "the draw after a second write");
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
if (!CountersReadable()) return;
const WireIndirectCounters delta = Delta();
EXPECT_EQ(delta.nativeDraws, 3u);
EXPECT_EQ(delta.cpuExpansions, 0u);
EXPECT_EQ(delta.hostWaits, 0u) << "a host wait was taken";
EXPECT_EQ(delta.barriers, 2u) << "exactly one barrier per write: the second draw reads no new write";
}
} // namespace
} // namespace MGITest
@@ -0,0 +1,220 @@
// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/WireMipShapeScenario.cpp
// Copyright (c) 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
//
// P8-SV: TWO glGenerateMipmap SHAPES THE MAGMA WIRE ARM GOT WRONG (docs/Disaggregated/notes/p8/SV.md).
//
// * A 3D chain through the NATIVE blit. The wire arm passed the level's depth as the blit's
// subresource layerCount on an image with one array layer (VUID-vkCmdBlitImage-srcImage-00240,
// -srcSubresource-01707, -dstSubresource-01708). Lavapipe blits a 3D image by its z offsets and
// ignores layerCount, so the PIXELS below are right with or without the fix on this host; what
// goes red on the host is the validation layer, which the `.Mip3DValidation.` entry runs this
// case under (inproc, so the server's Vulkan instance is this process and its stdout is ours).
// The pixel half runs on both backends and every arm, and is what a driver that honours
// layerCount would fail.
// * A 1D DEPTH chain with no native blit. The depth shader arm renders 2D views, which a 1D image
// does not admit; the wire arm handed it the chain anyway. It is now the named decline
// MipmapShaderFormatOrShape, decided before the chain grows, and the session carries on.
// Magma only, and reachable on this host only with MGITEST_MAGMA_FORCE_SHADER_MIPMAP (lavapipe
// blits depth natively), which the SERVER reads: split and spawn entries only.
#include <array>
#include <cstdint>
#include <fstream>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#include "../Harness/SplitLane.h"
#include "../Harness/SplitRuntimePeek.h"
#include "../Harness/WireDeclinePeek.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glext.h>
namespace MGITest {
namespace {
using Texel = std::array<int, 4>;
// Is the Khronos validation layer loaded into THIS process? On the inproc arm the server's
// Vulkan instance lives here, so this is the proof that a run "under the layer" really was.
bool ValidationLayerMapped() {
#if defined(__linux__)
std::ifstream maps("/proc/self/maps");
std::string line;
while (std::getline(maps, line)) {
if (line.find("VkLayer_khronos_validation") != std::string::npos) return true;
}
#endif
return false;
}
class WireMipShapeScenario : public ScenarioTest {
protected:
void SetUp() override {
ScenarioTest::SetUp();
if (!Ready()) return;
m_magma = Gl().BackendName() == "DirectVulkan";
m_transport = PeekSplitRuntime().transportName;
}
// The Magma server's decline tally is readable where the server is this process.
bool DeclinesReadable() const { return m_magma && m_transport == "inproc"; }
bool m_magma = false;
std::string m_transport;
};
// A 3D RGBA8 chain, 8x8x8 with its full storage, whose level-0 slices come in PAIRS of one
// colour: level 1's slice k is then pair k's colour exactly, and every level below is the
// average of two slices of the level above. A blit that loses the z axis (one slice only,
// or slices read as array layers of a 2D image) cannot produce those numbers.
TEST_F(WireMipShapeScenario, VolumeChainIsGeneratedAlongItsDepth) {
if (!Ready() || IsSkipped()) return;
constexpr int kSide = 8, kLevels = 4;
const std::array<Texel, 4> pairs = {Texel{200, 40, 80, 255}, Texel{40, 200, 120, 255},
Texel{100, 100, 240, 255}, Texel{240, 160, 0, 255}};
GLuint volume = 0;
glGenTextures(1, &volume);
glBindTexture(GL_TEXTURE_3D, volume);
glTexStorage3D(GL_TEXTURE_3D, kLevels, GL_RGBA8, kSide, kSide, kSide);
std::vector<GLubyte> level0(static_cast<size_t>(kSide * kSide * kSide * 4));
for (int z = 0; z < kSide; ++z) {
const Texel& texel = pairs[static_cast<size_t>(z / 2)];
for (int at = 0; at < kSide * kSide; ++at) {
for (int c = 0; c < 4; ++c)
level0[static_cast<size_t>((z * kSide * kSide + at) * 4 + c)] = static_cast<GLubyte>(texel[c]);
}
}
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexSubImage3D(GL_TEXTURE_3D, 0, 0, 0, 0, kSide, kSide, kSide, GL_RGBA, GL_UNSIGNED_BYTE, level0.data());
glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "3D texture setup";
glGenerateMipmap(GL_TEXTURE_3D);
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "glGenerateMipmap(GL_TEXTURE_3D)";
const auto average = [](const Texel& a, const Texel& b) {
return Texel{(a[0] + b[0]) / 2, (a[1] + b[1]) / 2, (a[2] + b[2]) / 2, (a[3] + b[3]) / 2};
};
// expected[level][slice]
std::vector<std::vector<Texel>> expected(kLevels);
expected[1].assign(pairs.begin(), pairs.end());
expected[2] = {average(pairs[0], pairs[1]), average(pairs[2], pairs[3])};
expected[3] = {average(expected[2][0], expected[2][1])};
glPixelStorei(GL_PACK_ALIGNMENT, 4);
for (int level = 1; level < kLevels; ++level) {
const int side = kSide >> level;
GLint width = 0, height = 0, depth = 0;
glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_WIDTH, &width);
glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_HEIGHT, &height);
glGetTexLevelParameteriv(GL_TEXTURE_3D, level, GL_TEXTURE_DEPTH, &depth);
EXPECT_EQ(width, side) << "level " << level;
EXPECT_EQ(height, side) << "level " << level;
EXPECT_EQ(depth, side) << "level " << level;
std::vector<GLubyte> pixels(static_cast<size_t>(side * side * side * 4), 0x5a);
glGetTexImage(GL_TEXTURE_3D, level, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "readback of level " << level;
for (int z = 0; z < side; ++z) {
const Texel& want = expected[static_cast<size_t>(level)][static_cast<size_t>(z)];
size_t wrong = 0;
Texel first{};
for (int at = 0; at < side * side; ++at) {
const size_t base = static_cast<size_t>((z * side * side + at) * 4);
bool ok = true;
for (int c = 0; c < 4; ++c) {
const int got = pixels[base + static_cast<size_t>(c)];
if (got < want[c] - 2 || got > want[c] + 2) ok = false;
}
if (!ok && wrong++ == 0) {
for (int c = 0; c < 4; ++c) first[c] = pixels[base + static_cast<size_t>(c)];
}
}
EXPECT_EQ(wrong, 0u) << "level " << level << " slice " << z << ": expected (" << want[0] << ","
<< want[1] << "," << want[2] << "," << want[3] << "), first wrong texel ("
<< first[0] << "," << first[1] << "," << first[2] << "," << first[3] << ")";
}
}
glBindTexture(GL_TEXTURE_3D, 0);
glDeleteTextures(1, &volume);
// The `.Mip3DValidation.` entry: the blit above ran under the Khronos validation layer,
// whose VUID lines the entry's FAIL_REGULAR_EXPRESSION reads. Without the layer actually
// loaded, a clean output would prove nothing, so the entry requires it.
if (SplitLane::MarkerIsOne("MGITEST_REQUIRE_VK_VALIDATION_LAYER")) {
ASSERT_TRUE(m_magma && m_transport == "inproc")
<< "the validation entry reads the server's layer output in-process: Magma, inproc";
EXPECT_TRUE(ValidationLayerMapped())
<< "VK_LAYER_KHRONOS_validation is not loaded in this process, so the absence of "
"VUID lines in this entry's output says nothing";
}
}
// A 1D DEPTH chain with the native blit forced off: no mip arm renders a 1D image, so the
// wire arm declines by name (MipmapShaderFormatOrShape) before growing the chain. Pinned:
// no GL error, the session answering (two readback round trips), level 0 as uploaded, level
// 1 still the application's own sentinel - and, where the server is this process, exactly
// one MipmapShaderFormatOrShape decline counted.
TEST_F(WireMipShapeScenario, Depth1DChainWithoutANativeBlitDeclinesByName) {
if (!Ready() || IsSkipped()) return;
if (!m_magma)
GTEST_SKIP() << "Magma wire-arm decline (P8-SV): the knob and the depth shader arm it forces are "
"Magma's; Espryt (" << Gl().BackendName() << ") generates this chain itself";
const std::string tier = SplitLane::MarkerValue("MGITEST_MAGMA_FORCE_SHADER_MIPMAP");
if (tier.empty())
GTEST_SKIP() << "MGITEST_MAGMA_FORCE_SHADER_MIPMAP is unset: this driver blits depth natively, "
"so the depth shader arm this case refuses is not entered";
constexpr int kWidth = 16;
GLuint line = 0;
glGenTextures(1, &line);
glBindTexture(GL_TEXTURE_1D, line);
glTexStorage1D(GL_TEXTURE_1D, 5, GL_DEPTH_COMPONENT32F, kWidth);
const std::vector<GLfloat> level0(kWidth, 0.25f), level1(kWidth / 2, 0.75f);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexSubImage1D(GL_TEXTURE_1D, 0, 0, kWidth, GL_DEPTH_COMPONENT, GL_FLOAT, level0.data());
glTexSubImage1D(GL_TEXTURE_1D, 1, 0, kWidth / 2, GL_DEPTH_COMPONENT, GL_FLOAT, level1.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "1D depth texture setup";
unsigned long long declinesBefore = 0;
const bool counted =
DeclinesReadable() && PeekWireDeclineCount("MipmapShaderFormatOrShape", &declinesBefore);
glGenerateMipmap(GL_TEXTURE_1D);
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "glGenerateMipmap(GL_TEXTURE_1D) on a depth chain";
glPixelStorei(GL_PACK_ALIGNMENT, 4);
for (const int level : {0, 1}) {
const int width = kWidth >> level;
std::vector<GLfloat> depths(static_cast<size_t>(width), -1.0f);
glGetTexImage(GL_TEXTURE_1D, level, GL_DEPTH_COMPONENT, GL_FLOAT, depths.data());
ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "readback of level " << level;
const GLfloat want = level == 0 ? 0.25f : 0.75f;
size_t wrong = 0;
GLfloat first = 0.0f;
for (const GLfloat depth : depths) {
if (depth < want - 0.0001f || depth > want + 0.0001f) {
if (wrong++ == 0) first = depth;
}
}
EXPECT_EQ(wrong, 0u) << "level " << level << " tier=" << tier << ": a declined mip must leave "
<< "every level as the application wrote it (expected " << want
<< ", first wrong " << first << ")";
}
glBindTexture(GL_TEXTURE_1D, 0);
glDeleteTextures(1, &line);
if (counted) {
unsigned long long declinesAfter = 0;
ASSERT_TRUE(PeekWireDeclineCount("MipmapShaderFormatOrShape", &declinesAfter));
EXPECT_EQ(declinesAfter - declinesBefore, 1u)
<< "the 1D depth chain must be refused once, by name, before the chain grows";
}
}
} // namespace
} // namespace MGITest

Some files were not shown because too many files have changed in this diff Show More