[Feature] (SharedImage): zero-copy Wayland windows via linux-dmabuf, EGL dma-buf import, GBM backend, and Espryt/Magma AHB present and texture binding

This commit is contained in:
2026-10-02 08:39:59 -04:00
parent 38842985f8
commit 3d17e8c10b
39 changed files with 2868 additions and 54 deletions
+9
View File
@@ -919,6 +919,15 @@ if (UNIX AND NOT APPLE)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
endif()
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
# docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
# it through dlsym. Self-contained: nothing outside this block refers to either.
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE MobileGL/MG_Impl/SharedImageApi.cpp)
add_subdirectory(MobileGL/MG_Gbm)
endif()
if (ANDROID)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
android
@@ -1199,6 +1199,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image) {
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image);
#else
(void)image;
return false;
#endif
}
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
@@ -41,6 +41,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
// Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is
// the image's EGLImage, then a wait on its fence (DirectGLES.h, SharedImageImpl).
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
@@ -32,6 +32,11 @@
#if MOBILEGL_BUILD_DISAGGREGATED
// P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads.
#include <MG_Remote/Server/StagedTextureStore.h>
// Shared images: the registry the EGLImages are built from (SharedImageImpl below).
#include <MG_Remote/Server/SharedImageRegistry.h>
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#endif
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
@@ -16223,6 +16228,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
static EGLDisplay g_Display = EGL_NO_DISPLAY;
// eglInitialize/eglTerminate pairing: the display is terminated only when the count drops to 0.
static Uint32 g_displayUsers = 0;
// How many times the display has been terminated. eglTerminate destroys every EGLImage made on
// it, and a re-initialized display hands back the same EGLDisplay handle, so a holder destroys
// its EGLImage itself only while this has not moved since the image was made.
static std::atomic<Uint64> g_displayTerminations{0};
#if MOBILEGL_BUILD_DISAGGREGATED
namespace SharedImageImpl {
// The display (and every EGLImage and renderbuffer made on it) is gone: forget the present
// targets without calling into the driver, releasing the images they held.
void ForgetPresentTargetsAfterTerminate();
} // namespace SharedImageImpl
#endif
static void NoteDisplayTerminated() {
g_displayTerminations.fetch_add(1, std::memory_order_acq_rel);
#if MOBILEGL_BUILD_DISAGGREGATED
SharedImageImpl::ForgetPresentTargetsAfterTerminate();
#endif
}
struct NativeContextTuple {
Uint64 SessionKey = 0;
@@ -17029,6 +17051,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (g_displayUsers > 0) --g_displayUsers;
if (g_displayUsers == 0 && g_Display != EGL_NO_DISPLAY && g_EGLFuncs.eglTerminate) {
g_EGLFuncs.eglTerminate(g_Display);
NoteDisplayTerminated();
g_Display = EGL_NO_DISPLAY;
}
}
@@ -18082,8 +18105,403 @@ namespace MobileGL::MG_Backend::DirectGLES {
// closes while another is rendering must not take eglTerminate with it.
if (g_displayUsers == 0 && g_EGLFuncs.eglTerminate) {
g_EGLFuncs.eglTerminate(g_Display);
NoteDisplayTerminated();
g_Display = EGL_NO_DISPLAY;
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (DirectGLES.h; docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) ------
namespace SharedImageImpl {
#if defined(__ANDROID__)
namespace {
namespace SI = MG_Remote::Server::SharedImages;
// EGL_ANDROID_image_native_buffer's target and EGL_KHR_image_base's attribute, spelled
// here so no extension header is needed.
constexpr EGLenum kEglNativeBufferAndroid = 0x3140;
constexpr EGLint kEglImagePreservedKhr = 0x30D2;
using GetNativeClientBufferFn = EGLClientBuffer (*)(const AHardwareBuffer*);
using CreateImageKhrFn = void* (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
using DestroyImageKhrFn = EGLBoolean (*)(EGLDisplay, void*);
using ImageTargetFn = void (*)(GLenum, void*);
struct EntryPoints {
Bool Resolved = false;
Bool Usable = false;
String Why;
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
CreateImageKhrFn CreateImage = nullptr;
DestroyImageKhrFn DestroyImage = nullptr;
ImageTargetFn TargetTexture2D = nullptr;
ImageTargetFn TargetRenderbuffer = nullptr;
};
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] == '\0' || at[n] == ' ';
if (startOk && endOk) return true;
}
return false;
}
// None of these is in the loader's tables, and eglGetProcAddress hands back live-looking
// stubs for entry points a driver lacks, so the extension strings decide first. Resolved
// once, from the first call that has a context current to read the strings from; the
// answer is never modified afterwards, so it is read without the lock.
const EntryPoints& ResolvedEntryPoints() {
static auto* mutex = new std::mutex();
static auto* entry = new EntryPoints();
const std::lock_guard<std::mutex> lock(*mutex);
if (entry->Resolved) return *entry;
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_GLESFuncs.glGetString) {
entry->Why = "the loader has no eglGetProcAddress/eglQueryString/glGetString";
return *entry;
}
const char* egl = g_Display != EGL_NO_DISPLAY ? g_EGLFuncs.eglQueryString(g_Display, EGL_EXTENSIONS)
: nullptr;
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
if (egl == nullptr || gl == nullptr) {
entry->Why = "no current display and context to read the extension strings from";
return *entry; // asked again on the next call
}
entry->Resolved = true;
if (!HasExtensionWord(egl, "EGL_KHR_image_base") ||
!HasExtensionWord(egl, "EGL_ANDROID_image_native_buffer") ||
!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
entry->Why = "EGL_KHR_image_base / EGL_ANDROID_image_native_buffer / "
"EGL_ANDROID_get_native_client_buffer is not advertised";
return *entry;
}
if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
entry->Why = "GL_OES_EGL_image is not advertised";
return *entry;
}
entry->GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
entry->CreateImage =
reinterpret_cast<CreateImageKhrFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
entry->DestroyImage =
reinterpret_cast<DestroyImageKhrFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
entry->TargetTexture2D =
reinterpret_cast<ImageTargetFn>(g_EGLFuncs.eglGetProcAddress("glEGLImageTargetTexture2DOES"));
entry->TargetRenderbuffer = reinterpret_cast<ImageTargetFn>(
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
if (!entry->GetNativeClientBuffer || !entry->CreateImage || !entry->DestroyImage ||
!entry->TargetTexture2D || !entry->TargetRenderbuffer) {
entry->Why = "eglGetProcAddress gave no address for an advertised EGLImage entry point";
return *entry;
}
entry->Usable = true;
return *entry;
}
void DrainErrors() {
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
}
}
} // namespace
#endif
struct EglImage {
#if defined(__ANDROID__)
SI::ImageRef Image;
void* Handle = nullptr;
EGLDisplay Display = EGL_NO_DISPLAY;
Uint64 DisplayTerminations = 0;
DestroyImageKhrFn Destroy = nullptr;
~EglImage() {
// eglDestroyImageKHR needs the display, not a context, so a session's twin teardown
// (no context current) still releases the image; only exit() and a terminated
// display - which destroyed the image itself - skip it.
if (Handle == nullptr || Destroy == nullptr || InProcessExit()) return;
if (g_displayTerminations.load(std::memory_order_acquire) != DisplayTerminations) return;
Destroy(Display, Handle);
}
#endif
};
#if defined(__ANDROID__)
EglImageRef CreateEglImage(Uint64 id, String& why) {
const EntryPoints& entry = ResolvedEntryPoints();
if (!entry.Usable) {
why = entry.Why;
return nullptr;
}
SI::ImageRef image = SI::Find(id);
if (image == nullptr) {
why = "no live shared image has this id";
return nullptr;
}
if (image->Native == nullptr) {
why = "the image has no AHardwareBuffer";
return nullptr;
}
const EGLClientBuffer buffer =
entry.GetNativeClientBuffer(static_cast<const AHardwareBuffer*>(image->Native));
if (buffer == nullptr) {
why = "eglGetNativeClientBufferANDROID returned null";
return nullptr;
}
const EGLint attribs[] = {kEglImagePreservedKhr, EGL_TRUE, EGL_NONE};
void* handle = entry.CreateImage(g_Display, EGL_NO_CONTEXT, kEglNativeBufferAndroid, buffer, attribs);
if (handle == nullptr) {
char text[96];
std::snprintf(text, sizeof(text), "eglCreateImageKHR failed (EGL error 0x%04x)",
g_EGLFuncs.eglGetError ? static_cast<unsigned>(g_EGLFuncs.eglGetError()) : 0u);
why = text;
return nullptr;
}
auto result = MakeShared<EglImage>();
result->Image = std::move(image);
result->Handle = handle;
result->Display = g_Display;
result->DisplayTerminations = g_displayTerminations.load(std::memory_order_acquire);
result->Destroy = entry.DestroyImage;
return result;
}
Bool TargetBoundTexture2D(const EglImageRef& image) {
const EntryPoints& entry = ResolvedEntryPoints();
if (!entry.Usable || image == nullptr || image->Handle == nullptr) return false;
DrainErrors();
entry.TargetTexture2D(GL_TEXTURE_2D, image->Handle);
return g_GLESFuncs.glGetError() == GL_NO_ERROR;
}
namespace {
// ONE PRESENT TARGET PER IMAGE, PROCESS-WIDE: the EGLImage and a renderbuffer whose
// storage it is. Every native context is in one share group, so the renderbuffer is
// one object for every session's context; only the framebuffer it is attached to is
// per context. Kept until no session holds the image any more (pruned on every present
// - the cache's own reference is then the only one), with a small cap so a client that
// cycles through many images cannot pile them up between presents.
struct PresentTarget {
EglImageRef Image;
GLuint Renderbuffer = 0;
Uint64 LastUse = 0;
};
struct PresentTargets {
std::mutex Mutex;
std::unordered_map<Uint64, PresentTarget> ById;
Uint64 UseClock = 0;
};
constexpr SizeT kMaxPresentTargets = 8;
PresentTargets& Targets() {
static auto* targets = new PresentTargets();
return *targets;
}
// The framebuffer a present draws through, one per native context (container objects
// are never shared). It carries an attachment only for the duration of a present, so
// no context's framebuffer keeps a dropped target's renderbuffer alive.
PerNativeContext<GLuint> g_presentFramebuffers;
Bool TargetIsCurrent(const PresentTarget& target) {
return target.Image != nullptr &&
target.Image->DisplayTerminations == g_displayTerminations.load(std::memory_order_acquire);
}
void DeleteTarget(PresentTarget& target) {
if (target.Renderbuffer != 0 && TargetIsCurrent(target)) {
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
}
target.Renderbuffer = 0;
target.Image = nullptr;
}
// Caller holds the mutex and has a context of the share group current.
void PruneLocked(PresentTargets& targets) {
for (auto it = targets.ById.begin(); it != targets.ById.end();) {
if (!TargetIsCurrent(it->second) || it->second.Image->Image.use_count() <= 1) {
DeleteTarget(it->second);
it = targets.ById.erase(it);
} else {
++it;
}
}
}
GLuint MakeRenderbuffer(const EglImage& image, String& why) {
GLuint renderbuffer = 0;
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
if (renderbuffer == 0) {
why = "glGenRenderbuffers gave no name";
return 0;
}
// Espryt keeps no renderbuffer-binding shadow; put the driver's binding back as found.
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(GL_RENDERBUFFER_BINDING, &previous);
DrainErrors();
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
ResolvedEntryPoints().TargetRenderbuffer(GL_RENDERBUFFER, image.Handle);
const GLenum error = g_GLESFuncs.glGetError();
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, static_cast<GLuint>(previous));
if (error != GL_NO_ERROR) {
char text[96];
std::snprintf(text, sizeof(text), "glEGLImageTargetRenderbufferStorageOES -> 0x%04x",
static_cast<unsigned>(error));
why = text;
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
return 0;
}
return renderbuffer;
}
// The present returns once the copy completed (the client attaches the buffer to its
// wl_surface right after and nothing else orders the compositor's read).
Bool WaitForCompletion() {
if (g_GLESFuncs.glFenceSync && g_GLESFuncs.glClientWaitSync && g_GLESFuncs.glDeleteSync) {
if (GLsync fence = g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)) {
constexpr GLuint64 kSliceNs = 1000ull * 1000 * 1000;
GLenum status = GL_TIMEOUT_EXPIRED;
GLbitfield flags = GL_SYNC_FLUSH_COMMANDS_BIT;
for (Uint slice = 0; slice < 10 && status == GL_TIMEOUT_EXPIRED; ++slice) {
status = g_GLESFuncs.glClientWaitSync(fence, flags, kSliceNs);
flags = 0;
}
g_GLESFuncs.glDeleteSync(fence);
if (status == GL_ALREADY_SIGNALED || status == GL_CONDITION_SATISFIED) return true;
if (status == GL_TIMEOUT_EXPIRED) {
MGLOG_E("Shared image present: the copy did not complete within 10 s");
return false;
}
// GL_WAIT_FAILED: glFinish is the same promise without the object.
}
}
g_GLESFuncs.glFinish();
return true;
}
} // namespace
Bool BlitDefaultFramebufferTo(const SharedImageView& view) {
if (view.Id == 0 || view.Width == 0 || view.Height == 0 || view.NativeBuffer == nullptr) return false;
if (!IsBackendContextCurrentOnThisThread()) {
MGLOG_E_ONCE("Shared image present: no backend context is current on the calling thread");
return false;
}
const EGLSurface surface = CurrentContextDrawSurface();
EGLint sourceWidth = 0;
EGLint sourceHeight = 0;
if (surface == EGL_NO_SURFACE || !g_EGLFuncs.eglQuerySurface ||
!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_WIDTH, &sourceWidth) ||
!g_EGLFuncs.eglQuerySurface(g_Display, surface, EGL_HEIGHT, &sourceHeight) || sourceWidth <= 0 ||
sourceHeight <= 0) {
MGLOG_E_ONCE("Shared image present: the current context has no default framebuffer to copy");
return false;
}
Bool copied = false;
{
auto& targets = Targets();
const std::lock_guard<std::mutex> lock(targets.Mutex);
PruneLocked(targets);
auto it = targets.ById.find(view.Id);
if (it != targets.ById.end() && it->second.Image->Image->Native != view.NativeBuffer) {
DeleteTarget(it->second);
targets.ById.erase(it);
it = targets.ById.end();
}
if (it == targets.ById.end()) {
String why;
PresentTarget target;
target.Image = CreateEglImage(view.Id, why);
if (target.Image != nullptr) target.Renderbuffer = MakeRenderbuffer(*target.Image, why);
if (target.Renderbuffer == 0) {
MGLOG_E_ONCE("Shared image present: image %llu cannot be rendered to: %s",
static_cast<unsigned long long>(view.Id), why.c_str());
return false;
}
while (targets.ById.size() >= kMaxPresentTargets) {
auto oldest = targets.ById.begin();
for (auto candidate = targets.ById.begin(); candidate != targets.ById.end(); ++candidate) {
if (candidate->second.LastUse < oldest->second.LastUse) oldest = candidate;
}
DeleteTarget(oldest->second);
targets.ById.erase(oldest);
}
it = targets.ById.emplace(view.Id, std::move(target)).first;
}
it->second.LastUse = ++targets.UseClock;
GLuint& framebuffer = g_presentFramebuffers.Current();
if (framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
if (framebuffer == 0) {
MGLOG_E_ONCE("Shared image present: glGenFramebuffers gave no name");
return false;
}
// Every binding goes through Espryt's shadows (BindFramebufferId, the scissor test's
// render-state shadow) and is put back by the guards, so the application's state and
// the caches that describe it are what they were. A blit is scissored like a draw.
ScopedScissorDisable scissorOff;
ScopedFramebufferBinding binding(/*saveRead=*/true, /*saveDraw=*/true);
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, 0);
FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, framebuffer);
g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER,
it->second.Renderbuffer);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER);
if (status == GL_FRAMEBUFFER_COMPLETE) {
// The default framebuffer's read buffer is context state the application may
// have set to GL_NONE; the copy reads the back buffer either way.
GLint readBuffer = GL_BACK;
g_GLESFuncs.glGetIntegerv(GL_READ_BUFFER, &readBuffer);
if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(GL_BACK);
DrainErrors();
const Bool sameExtent = static_cast<Uint32>(sourceWidth) == view.Width &&
static_cast<Uint32>(sourceHeight) == view.Height;
// TOP ROW FIRST: GL's top row (y = height - 1) lands in the image's first memory
// row, which an AHardwareBuffer-backed renderbuffer addresses as y = 0.
g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0,
static_cast<GLint>(view.Height), static_cast<GLint>(view.Width), 0,
GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR);
const GLenum error = g_GLESFuncs.glGetError();
copied = error == GL_NO_ERROR;
if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(static_cast<GLenum>(readBuffer));
if (!copied) {
MGLOG_E_ONCE("Shared image present: glBlitFramebuffer %dx%d -> %ux%u failed (0x%04x)",
sourceWidth, sourceHeight, view.Width, view.Height,
static_cast<unsigned>(error));
}
} else {
MGLOG_E_ONCE("Shared image present: image %llu's framebuffer is incomplete (0x%04x)",
static_cast<unsigned long long>(view.Id), static_cast<unsigned>(status));
}
g_GLESFuncs.glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, 0);
}
return copied && WaitForCompletion();
}
void ForgetPresentTargetsAfterTerminate() {
auto& targets = Targets();
const std::lock_guard<std::mutex> lock(targets.Mutex);
// TargetIsCurrent is already false for every entry, so nothing reaches the driver.
for (auto& entry : targets.ById) DeleteTarget(entry.second);
targets.ById.clear();
}
#else
EglImageRef CreateEglImage(Uint64 id, String& why) {
(void)id;
why = "shared images are imported through AHardwareBuffer, which only Android has";
return nullptr;
}
Bool TargetBoundTexture2D(const EglImageRef& image) {
(void)image;
return false;
}
Bool BlitDefaultFramebufferTo(const SharedImageView& view) {
(void)view;
return false;
}
void ForgetPresentTargetsAfterTerminate() {}
#endif
} // namespace SharedImageImpl
#endif
} // namespace MobileGL::MG_Backend::DirectGLES
@@ -462,6 +462,32 @@ namespace MobileGL::MG_Backend::DirectGLES {
void InvalidateSyncedRenderState();
} // namespace RenderStateImpl
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) ----
//
// A shared image reaches the driver only through the Android import path: its AHardwareBuffer
// becomes an EGLImage (EGL_ANDROID_get_native_client_buffer + EGL_ANDROID_image_native_buffer)
// and that EGLImage the storage of a texture or renderbuffer (GL_OES_EGL_image). On a host
// build there is no such path and every entry below declines.
namespace SharedImageImpl {
// One EGLImage over one shared image, holding the image's registry reference for as long
// as the EGLImage lives (SharedImageRegistry.h's lifetime rule). The last reference
// destroys the EGLImage, unless the display that made it has been terminated since (which
// destroyed it already). Opaque outside DirectGLES.cpp.
struct EglImage;
using EglImageRef = SharedPtr<EglImage>;
// An EGLImage for the live shared image `id`, or null with `why`. Needs a current context
// (the first call resolves the extension entry points).
EglImageRef CreateEglImage(Uint64 id, String& why);
// glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image) on what the active unit has bound to
// GL_TEXTURE_2D. False when the driver refused it (its GL error is consumed).
Bool TargetBoundTexture2D(const EglImageRef& image);
// BackendObject::BlitDefaultFramebufferToSharedImage, for the calling thread's context.
Bool BlitDefaultFramebufferTo(const SharedImageView& image);
} // namespace SharedImageImpl
#endif
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#else
Bool InProcessTeardown() { return g_processTeardown; }
#endif
Bool InProcessExit() { return g_processTeardown; }
void EnsureProcessTeardownSentinel() {
std::call_once(g_teardownSentinelOnce,
[] { std::atexit(+[] { g_processTeardown = true; }); });
@@ -7587,6 +7588,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
#if MOBILEGL_BUILD_DISAGGREGATED
// A shared image's EGLImage lives exactly as long as the name it was bound to.
m_sharedImage = nullptr;
#endif
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
m_contextGeneration = g_backendContextGeneration;
if (m_backendTextureId == 0) {
@@ -8837,6 +8842,92 @@ namespace MobileGL::MG_Backend::DirectGLES {
#define MGB_STORAGE_KIND(obj) ((obj)->GetStorageType())
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). The client defined
// level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor
// still says so, but the texels are the image's, so nothing below may allocate or upload.
Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) {
const Uint64 imageId = record.SharedImageId;
if (imageId == 0) {
// Respecified away from the image. The ES name is an EGLImage sibling, which must not
// be redefined in place: the immutable flag sends the normal path through
// RecreateBackendTexture, and the EGLImage goes with the name it was bound to.
m_sharedImageId = 0;
m_isInitialized = false;
m_backendStorageImmutable = true;
return false;
}
// Sub-data aimed at the image is not replayed into it - the image is written by the
// producer's present - but it must still leave the pending set, or the per-draw clean
// gate (IsDrawSyncCleanByRecord) would never close.
const auto dropPendingUploads = [&record] {
if (record.PendingUploads.empty()) return;
MGLOG_W_ONCE("Shared image texture %u: sub-data on an attached shared image is dropped",
record.Desc.GlNameForDiag);
Vector<std::pair<Uint16, Uint16>> keys;
keys.reserve(record.PendingUploads.size());
for (const auto& pending : record.PendingUploads) {
keys.emplace_back(MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget), pending.Level);
}
for (const auto& key : keys) ConsumePipeTextureUpload(record.Desc.Resource, key.first, key.second);
};
// Steady state: bound to this image already (the serial moves with sub-data and with a
// metadata respecify, neither of which changes what the name samples). A failed bind is
// retried only once the record moves.
if (m_sharedImageId == imageId && m_isInitialized &&
(m_sharedImage != nullptr || m_syncedResourceSerial == record.Serial)) {
dropPendingUploads();
m_syncedResourceSerial = record.Serial;
return true;
}
// A fresh mutable name: the current one may hold immutable storage (which no EGLImage
// can replace) or another image's sibling. RecreateBackendTexture also moves the FBO
// twins' attachment generation and forces the parameter resync the new name needs.
RecreateBackendTexture();
m_sharedImage = nullptr;
m_sharedImageId = imageId;
m_isInitialized = true;
// Whatever comes next redefines storage through a re-mint (see the imageId == 0 arm).
m_backendStorageImmutable = true;
m_syncedResourceSerial = record.Serial;
dropPendingUploads();
const TextureTarget target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
if (ConvertTextureTargetToBackendGLEnum(target) != GL_TEXTURE_2D) {
MGLOG_E_ONCE("Shared image texture %u: target %s cannot take an EGLImage; left empty",
record.Desc.GlNameForDiag, MG_Util::ConvertTextureTargetToString(target).c_str());
return true;
}
String why;
SharedImageImpl::EglImageRef image = SharedImageImpl::CreateEglImage(imageId, why);
if (image == nullptr) {
MGLOG_E_ONCE("Shared image texture %u: image %llu cannot be bound (%s); left empty",
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId), why.c_str());
return true;
}
Bind(GL_TEXTURE_2D);
if (!SharedImageImpl::TargetBoundTexture2D(image)) {
MGLOG_E_ONCE("Shared image texture %u: glEGLImageTargetTexture2DOES refused image %llu; left empty",
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId));
return true;
}
m_sharedImage = std::move(image);
// The shape the name now has: the descriptor's level 0, and that one level only.
m_prevTextureInfo = {static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
record.Desc.Width,
record.Desc.Height,
record.Desc.Depth,
1,
0,
record.Desc.Samples,
record.Desc.FixedSampleLocations != 0};
return true;
}
#endif
void BackendTextureObject::SyncMipmapsToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
@@ -8879,6 +8970,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES: ahead of the view test and of the staged-store and serial gates below,
// none of which knows the texels belong to an image - the staged store holds none of
// them, and the storage path would allocate fresh, empty storage over the image.
if (pushedStorage != nullptr && (pushedStorage->SharedImageId != 0 || m_sharedImageId != 0) &&
SyncSharedImageStorage(*pushedStorage)) {
return;
}
// P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE
// RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it
// had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage
+20
View File
@@ -74,6 +74,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// flag while the process is very much alive.
Bool InProcessTeardown();
void EnsureProcessTeardownSentinel();
// The exit() half of InProcessTeardown alone, for an object whose release needs only the
// display, not a current context (an EGLImage): a server session's twin teardown has no
// context current but a live display, and skipping the release there would leak the image.
Bool InProcessExit();
// Generation of the backend ES context that owns the driver ids currently handed
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
@@ -2099,6 +2103,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// level when the store covers it (notes/p9/W2-REMINT.md).
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES (MGPipeResourceRecord::SharedImageId). True when this call settled the
// storage - the ES name's level 0 IS the image (glEGLImageTargetTexture2DOES), or the
// image could not be bound and the name is left empty - and SyncMipmapsToBackend must
// not go on to allocate or upload. False when the record names no image: a twin that
// was bound to one has been told to re-mint, and the normal storage path takes over.
Bool SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record);
#endif
// Whether this texture's ES storage was minted in an image carrier rather than in the
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
@@ -2224,6 +2236,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// The shared image the ES name's level 0 was last bound to (0 = none; image ids are
// never reused, so this is a lifetime key), and the EGLImage that binding went through,
// which holds the image for as long as the name samples it. Null with a nonzero id when
// the image could not be bound: the name is left empty until the record moves.
Uint64 m_sharedImageId = 0;
SharedImageImpl::EglImageRef m_sharedImage;
#endif
// 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
@@ -560,6 +560,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
Bool BackendObject_DirectVulkan::BlitDefaultFramebufferToSharedImage(const SharedImageView& image) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Same lock as SwapEGLBuffers: the copy reads the current surface target's image.
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!pVulkanRenderer) {
MGLOG_E("DirectVulkan: shared image %llu: the renderer is not initialized",
static_cast<unsigned long long>(image.Id));
return false;
}
return pVulkanRenderer->BlitDefaultFramebufferToSharedImage(image);
#else
(void)image;
return false;
#endif
}
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);
@@ -38,6 +38,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void SetEGLSwapInterval(Int interval) override;
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
@@ -262,7 +262,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
info.format = format;
info.subresourceRange = {aspect, level, levels, layer, layers};
if (!storage) {
const Bool alphaIsOne = ResolveTextureFormatInfo(
// An X-format shared image carries undefined alpha bytes, exactly like an expanded RGB.
const Bool alphaIsOne = resource->sharedImageAlphaOne || ResolveTextureFormatInfo(
static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba;
const auto swizzle = [&](Uint32 channel) {
const auto value = record.Params.Swizzle[channel];
@@ -22,10 +22,16 @@
// (shape) and the server's staged-texture store (texels).
#include <MG_Pipe/PipeApply.h>
#include <MG_Remote/Server/StagedTextureStore.h>
// Shared images: a texture whose level 0 is a server-allocated AHardwareBuffer.
#include <MG_Remote/Server/SharedImageRegistry.h>
#include "../DirectVulkan.h"
// P7 wave 2 package B3: rule I's tally for the silent exits on this file's wire arm.
#include "WireDeclineTally.h"
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#endif
#include <string>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
@@ -2108,6 +2114,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record,
TextureResource& resource, Bool requireStorage) {
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
// Level 0 is a shared image: bind its buffer. An image that cannot be bound here leaves
// the texture the storage its NULL-data definition asked for, below.
if (record.SharedImageId != 0) {
const SharedImageBind bound = SyncWireSharedImage(record, resource, requireStorage);
if (bound != SharedImageBind::Unresolved) return bound == SharedImageBind::Bound;
}
if (desc.StorageKind != static_cast<Uint8>(TextureStorageType::Mipmap)) {
MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: buffer-backed textures are not migrated; declined",
desc.Resource.Slot, desc.Resource.Gen);
@@ -2276,7 +2288,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
: (desc.Levels > 1 ? std::min(std::max<Uint32>(desc.Levels, fullMipLevels), fullMipLevels)
: 1u);
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
// A shared image never stands in for allocated storage: the attach is over (or never
// resolved), so the texture gets storage of its own, and none of the image's texels.
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.sharedImageId == 0 &&
resource.format == format &&
resource.extent.width == desc.Width && resource.extent.height == imageHeight &&
resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType &&
@@ -2291,7 +2306,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// A named-level respecify changes ONLY that level; a BindMask update changes no
// storage at all. Preserve every compatible old level when growing/upgrading the
// allocation. A whole-store redefinition permits (but does not require) old texels.
const Bool preserve = resource.image != VK_NULL_HANDLE && resource.format == format &&
const Bool preserve = resource.image != VK_NULL_HANDLE && resource.sharedImageId == 0 &&
resource.format == format &&
resource.extent.width == desc.Width && resource.extent.height == imageHeight &&
resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType &&
@@ -2769,6 +2785,232 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return MG_Pipe::kMGPipeNullHandle;
}
Bool VkTextureManager::ImportSharedImage(void* nativeBuffer, Uint32 width, Uint32 height,
ImportedSharedImage& out, String& why) {
out = ImportedSharedImage{};
#if defined(__ANDROID__)
auto* ahb = static_cast<AHardwareBuffer*>(nativeBuffer);
if (ahb == nullptr) {
why = "the image has no AHardwareBuffer";
return false;
}
VulkanRenderer::WireAhbImport ctx;
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) {
why = "the device did not take VK_ANDROID_external_memory_android_hardware_buffer";
return false;
}
AHardwareBuffer_Desc desc{};
AHardwareBuffer_describe(ahb, &desc);
if (desc.width != width || desc.height != height || desc.layers != 1) {
why = "the buffer is " + std::to_string(desc.width) + "x" + std::to_string(desc.height) + "x" +
std::to_string(desc.layers) + ", not " + std::to_string(width) + "x" + std::to_string(height);
return false;
}
const auto getProperties =
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
VkAndroidHardwareBufferFormatPropertiesANDROID formatProperties{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
VkAndroidHardwareBufferPropertiesANDROID properties{VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
properties.pNext = &formatProperties;
VkResult result = getProperties(ctx.device, ahb, &properties);
if (result != VK_SUCCESS) {
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(result));
return false;
}
// The RGBA8/RGBX8 buffers the registry allocates have a Vulkan format; an external-only
// format would need a YCbCr conversion and is not a colour buffer this path can write.
if (formatProperties.format == VK_FORMAT_UNDEFINED) {
why = "the buffer has no Vulkan format (external format only)";
return false;
}
// Usage is bounded by the buffer's own usage (the AHardwareBuffer usage equivalence);
// transfers need no buffer usage bit.
VkImageUsageFlags usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) != 0 &&
(formatProperties.formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0)
usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT) != 0 &&
(formatProperties.formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0)
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
VkExternalMemoryImageCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.pNext = &external;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = formatProperties.format;
imageInfo.extent = {width, height, 1};
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = usage;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
result = vkCreateImage(ctx.device, &imageInfo, nullptr, &out.image);
if (result != VK_SUCCESS) {
out.image = VK_NULL_HANDLE;
why = "vkCreateImage(external AHB) -> " + std::to_string(static_cast<int>(result));
return false;
}
Int32 type = -1;
for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) {
if ((properties.memoryTypeBits & (1u << i)) != 0 &&
(ctx.memory.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0)
type = static_cast<Int32>(i);
}
for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) {
if ((properties.memoryTypeBits & (1u << i)) != 0) type = static_cast<Int32>(i);
}
if (type < 0) {
why = "no memory type can import it";
DestroyImportedSharedImage(ctx.device, out);
return false;
}
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
dedicated.image = out.image;
VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import.pNext = &dedicated;
import.buffer = ahb;
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));
DestroyImportedSharedImage(ctx.device, out);
return false;
}
result = vkBindImageMemory(ctx.device, out.image, out.memory, 0);
if (result != VK_SUCCESS) {
why = "vkBindImageMemory -> " + std::to_string(static_cast<int>(result));
DestroyImportedSharedImage(ctx.device, out);
return false;
}
out.format = formatProperties.format;
out.usage = usage;
out.features = formatProperties.formatFeatures;
out.queueFamily = ctx.queueFamily;
return true;
#else
(void)nativeBuffer, (void)width, (void)height;
why = "shared images are imported on Android only";
return false;
#endif
}
void VkTextureManager::DestroyImportedSharedImage(VkDevice device, ImportedSharedImage& image) {
if (image.image != VK_NULL_HANDLE) vkDestroyImage(device, image.image, nullptr);
if (image.memory != VK_NULL_HANDLE) vkFreeMemory(device, image.memory, nullptr);
image = ImportedSharedImage{};
}
VkTextureManager::SharedImageBind VkTextureManager::SyncWireSharedImage(
const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource, Bool requireStorage) {
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
const Uint64 id = record.SharedImageId;
if (requireStorage) {
// The buffer's usage has no storage equivalent; a reallocation would drop the image.
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu is bound as a shader image, "
"which it cannot back; declined",
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id));
return SharedImageBind::Failed;
}
// Already this image: the serial moved for something else (params, the bind mask).
if (resource.sharedImageId == id && resource.image != VK_NULL_HANDLE) return SharedImageBind::Bound;
const auto image = MG_Remote::Server::SharedImages::Find(id);
if (image == nullptr) {
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu is no longer live; the texture "
"keeps its own (undefined) level 0",
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id));
return SharedImageBind::Unresolved;
}
const Bool flat2D = desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D) ||
desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::TexRect);
if (!flat2D || desc.Width != image->Width || desc.Height != image->Height ||
!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) {
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: pipe target %u at %ux%u cannot take shared image "
"%llu (%ux%u); the texture keeps its own level 0",
desc.Resource.Slot, desc.Resource.Gen, static_cast<Uint32>(desc.Target), desc.Width,
desc.Height, static_cast<unsigned long long>(id), image->Width, image->Height);
return SharedImageBind::Unresolved;
}
ImportedSharedImage imported;
String why;
if (!ImportSharedImage(image->Native, image->Width, image->Height, imported, why)) {
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu not imported - %s; the texture "
"keeps its own level 0",
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id), why.c_str());
return SharedImageBind::Unresolved;
}
// ACQUIRE FROM THE FOREIGN FAMILY. The writer (a client session's Present, on its own
// device) leaves the image released to VK_QUEUE_FAMILY_FOREIGN_EXT in GENERAL, which is
// also the layout every wire descriptor samples in. Submitted on its own and waited, like
// the preserve copy: it runs once per attach, before any recorded use of the image.
VkCommandBufferAllocateInfo allocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VK_VERIFY(vkAllocateCommandBuffers(m_device, &allocInfo, &commandBuffer),
"vkAllocateCommandBuffers(shared image acquire)");
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_VERIFY(vkBeginCommandBuffer(commandBuffer, &beginInfo), "vkBeginCommandBuffer(shared image acquire)");
VkImageMemoryBarrier acquire{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
acquire.srcAccessMask = 0;
acquire.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
acquire.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
acquire.newLayout = VK_IMAGE_LAYOUT_GENERAL;
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
acquire.dstQueueFamilyIndex = imported.queueFamily;
acquire.image = imported.image;
acquire.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
0, nullptr, 0, nullptr, 1, &acquire);
VK_VERIFY(vkEndCommandBuffer(commandBuffer), "vkEndCommandBuffer(shared image acquire)");
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
VkFence fence = VK_NULL_HANDLE;
VK_VERIFY(vkCreateFence(m_device, &fenceInfo, nullptr, &fence), "vkCreateFence(shared image acquire)");
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;
VK_VERIFY(vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, fence), "vkQueueSubmit(shared image acquire)");
VK_VERIFY(vkWaitForFences(m_device, 1, &fence, VK_TRUE, UINT64_MAX), "vkWaitForFences(shared image acquire)");
vkDestroyFence(m_device, fence, nullptr);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
TextureResource replacement;
replacement.image = imported.image;
replacement.importedMemory = imported.memory;
replacement.layout = VK_IMAGE_LAYOUT_GENERAL;
replacement.extent = {image->Width, image->Height};
replacement.depth = 1;
replacement.arrayLayers = 1;
replacement.mipLevels = 1;
replacement.sampledBaseMipLevel = 0;
replacement.sampledLevelCount = 1;
replacement.format = imported.format;
replacement.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
replacement.viewType = VK_IMAGE_VIEW_TYPE_2D;
replacement.sampleCount = VK_SAMPLE_COUNT_1_BIT;
replacement.imageCreateFlags = 0;
replacement.usageFlags = imported.usage;
replacement.sharedImageId = id;
replacement.sharedImageOwner = image;
replacement.sharedImageAlphaOne = image->Fourcc == MG_Remote::Server::SharedImages::kFourccXbgr8888;
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
DeferResourceRelease(Move(resource));
std::destroy_at(&resource);
std::construct_at(&resource, Move(replacement));
MGLOG_D("Magma wire texture {slot=%u, gen=%u}: level 0 is shared image %llu (%ux%u)", desc.Resource.Slot,
desc.Resource.Gen, static_cast<unsigned long long>(id), image->Width, image->Height);
return SharedImageBind::Bound;
}
VkTextureManager::TextureResource* VkTextureManager::SyncTextureResourceByHandle(
MG_Pipe::MGPipeHandle handle, Bool renderbuffer, Bool requireStorage) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureResourceByHandle: m_device == VK_NULL_HANDLE");
@@ -357,6 +357,14 @@ public:
// on every mutation, so the first handle sync of a fresh image always re-checks.
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 syncedWireSerial = 0;
// SHARED IMAGES: level 0 is a server shared image (an imported AHardwareBuffer), not a
// VMA allocation. The resource owns the image and its dedicated imported memory, and
// holds the registry's reference so the buffer outlives every use (deferred releases
// included). `sharedImageAlphaOne`: an X-format image, whose alpha bytes are undefined.
VkDeviceMemory importedMemory = VK_NULL_HANDLE;
Uint64 sharedImageId = 0;
SharedPtr<const void> sharedImageOwner;
Bool sharedImageAlphaOne = false;
#endif
TextureResource() = default;
@@ -393,6 +401,10 @@ public:
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
#if MOBILEGL_BUILD_DISAGGREGATED
std::swap(this->syncedWireSerial, that.syncedWireSerial);
std::swap(this->importedMemory, that.importedMemory);
std::swap(this->sharedImageId, that.sharedImageId);
std::swap(this->sharedImageOwner, that.sharedImageOwner);
std::swap(this->sharedImageAlphaOne, that.sharedImageAlphaOne);
#endif
}
@@ -431,6 +443,16 @@ public:
if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator.Get(), image, allocation);
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (importedMemory != VK_NULL_HANDLE) {
if (image != VK_NULL_HANDLE) vkDestroyImage(s_device.Get(), image, nullptr);
vkFreeMemory(s_device.Get(), importedMemory, nullptr);
}
importedMemory = VK_NULL_HANDLE;
sharedImageId = 0;
sharedImageOwner.reset();
sharedImageAlphaOne = false;
#endif
fullView = VK_NULL_HANDLE;
sampledView = VK_NULL_HANDLE;
perMipViews.clear();
@@ -592,6 +614,23 @@ public:
// ranges select cube faces; array and 3D levels share one staged level shadow.
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): a server shared image's
// AHardwareBuffer bound as a single-level 2D colour image on THIS session's device, on dedicated
// memory imported from the buffer. Android only (false with `why` elsewhere, or when the device
// did not take the AHardwareBuffer extension). The image is born UNDEFINED; whoever uses it
// acquires it from the foreign queue family first, and a writer releases it back after.
struct ImportedSharedImage {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageUsageFlags usage = 0;
VkFormatFeatureFlags features = 0;
Uint32 queueFamily = 0;
};
static Bool ImportSharedImage(void* nativeBuffer, Uint32 width, Uint32 height, ImportedSharedImage& out,
String& why);
static void DestroyImportedSharedImage(VkDevice device, ImportedSharedImage& image);
#endif
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
@@ -778,6 +817,12 @@ private:
Bool requireStorage);
Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record,
TextureResource& resource);
// SyncWireTextureShape's arm for a record whose level 0 IS a shared image (SharedImageId):
// binds the imported buffer instead of allocating. Unresolved = the image is gone or cannot be
// imported here; `resource` is then untouched and the caller allocates as for any texture.
enum class SharedImageBind : Uint8 { Bound, Failed, Unresolved };
SharedImageBind SyncWireSharedImage(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource,
Bool requireStorage);
// The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle.
// Node-based for the same reason m_renderbufferResources is: callers hold TextureResource*
// across map-touching calls.
@@ -48,6 +48,8 @@
// P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on.
#include <MG_Remote/Server/StagedTextureStore.h>
#include <MG_Remote/Server/ServerLoop.h>
// Shared images: the present side resolves the image it imports (WireSharedImage.inc).
#include <MG_Remote/Server/SharedImageRegistry.h>
// P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope.
#include <MG_Impl/Pipe/SlotAllocator.h>
// P7 wave 2 package B3: rule I's tally for WireDraw.inc's silent draw drops.
@@ -3510,6 +3512,7 @@ void main() {
DestroyWireColorBlitResources();
DestroyWireDepthMipmapResources();
DestroyWireMultisampleResolveResources();
DestroySharedImagePresentTargets(false);
#endif
OnSubmitsCompletedUpTo(m_submitCounter);
DestroySubmitFencePool();
@@ -8082,6 +8085,8 @@ void main() {
// that file's pass with the box filter taken out and a sample index put in.
#include "WireMultisampleResolve.inc"
#include "WireDraw.inc"
// AFTER WireFramebuffer.inc: the shared-image present is that file's readback, into an image.
#include "WireSharedImage.inc"
void VulkanRenderer::Clear(GLbitfield mask) {
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -179,6 +179,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID
};
Bool GetWireAhbImport(WireAhbImport& out) const;
// SHARED IMAGES (BackendObject::BlitDefaultFramebufferToSharedImage): the active surface
// target's default framebuffer, copied into `image` top row first and scaled to its
// extent. Returns once the copy completed on the GPU. False (logged) when it cannot be
// done here: no AHardwareBuffer import, a quarter-turned surface, an unblittable format.
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image);
#endif
// FrameContext::IRecordingObserver: prepares the frame's timer-query
@@ -642,6 +647,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
Bool m_wireAhbImport = false;
void* m_wireGetAhbProperties = nullptr;
// Shared images this session presents into, imported once each and keyed by the image's
// id (monotonic, never reused). `owner` watches the registry's image: an entry whose image
// died is dropped at the next present, every earlier present having completed.
struct SharedImagePresentTarget {
VkTextureManager::ImportedSharedImage image;
WeakPtr<const void> owner;
};
UnorderedMap<Uint64, SharedImagePresentTarget> m_sharedImagePresentTargets;
void DestroySharedImagePresentTargets(Bool onlyDead);
// 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();
@@ -1390,6 +1390,14 @@
&viewFormat, &layerCount);
auto* resource = m_textureManager->SyncTextureResourceByHandle(handle);
if (!resource) MagmaWireFatal("mipmap-resource");
// A shared image is one level of a buffer someone else allocated; growing the chain would
// reallocate it away from the buffer.
if (resource->sharedImageId != 0) {
MGLOG_E_ONCE("DirectVulkan::GenerateWireMipmap: the texture's level 0 is shared image %llu, which has "
"no mip chain; levels %u..%u are left as they are",
static_cast<unsigned long long>(resource->sharedImageId), logicalBase + 1, logicalEnd - 1);
return;
}
m_textureManager->FlushPendingUploads();
RetireWireDrawPass();
auto& frame = m_frameContext.GetCurrent();
@@ -0,0 +1,187 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc
// 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
// SHARED IMAGES, the present side (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Included by
// VulkanRenderer.cpp inside the backend namespace, after WireFramebuffer.inc, whose readback this
// copy is modelled on: the same flush, the same write-visibility barrier, the same transition of
// the default framebuffer's image and the same submit-and-wait.
//
// OWNERSHIP. The image lives on more than one device: this session writes it, the compositor's
// session (another VulkanRenderer, another VkDevice) samples it. Each present acquires it from
// VK_QUEUE_FAMILY_FOREIGN_EXT (UNDEFINED: the copy overwrites every texel) and releases it back in
// GENERAL, the layout the sampling side acquires it in (VkTextureManager::SyncWireSharedImage).
#if MOBILEGL_BUILD_DISAGGREGATED
void VulkanRenderer::DestroySharedImagePresentTargets(Bool onlyDead) {
for (auto it = m_sharedImagePresentTargets.begin(); it != m_sharedImagePresentTargets.end();) {
if (onlyDead && !it->second.owner.expired()) {
++it;
continue;
}
VkTextureManager::DestroyImportedSharedImage(m_device, it->second.image);
it = m_sharedImagePresentTargets.erase(it);
}
}
// A copy moves bytes; a blit converts through the format. The bytes are right only between
// formats with one memory layout, which for the RGBA8 buffers means RGBA8 itself or its sRGB
// twin (a blit would decode the sRGB bytes on the way).
static Bool SharedImageCopiesAsIs(VkFormat source, VkFormat destination) {
if (source == destination) return true;
return destination == VK_FORMAT_R8G8B8A8_UNORM && source == VK_FORMAT_R8G8B8A8_SRGB;
}
Bool VulkanRenderer::BlitDefaultFramebufferToSharedImage(const SharedImageView& view) {
if (m_device == VK_NULL_HANDLE || view.Width == 0 || view.Height == 0) return false;
// Every earlier present waited for its copy, so an entry whose image has died is idle.
DestroySharedImagePresentTargets(true);
// The default framebuffer's image is stored in display orientation (row 0 is the top of
// the frame) on an identity surface, and turned half round on a 180 one; a blit expresses
// both. A quarter turn transposes, which no copy or blit can - and only the compositor's
// own window is ever quarter-turned, which presents to the display rather than here.
const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform();
if (IsQuarterTurnPreTransform(preTransform)) {
MGLOG_E_ONCE("Magma: shared image %llu: the current surface %llx is quarter-turned (transform %u); a "
"present into a shared image is declined there",
static_cast<unsigned long long>(view.Id), static_cast<unsigned long long>(m_activeTargetKey),
static_cast<Uint32>(preTransform));
return false;
}
auto found = m_sharedImagePresentTargets.find(view.Id);
if (found == m_sharedImagePresentTargets.end()) {
const auto owner = MG_Remote::Server::SharedImages::Find(view.Id);
if (owner == nullptr) {
MGLOG_E_ONCE("Magma: shared image %llu is not live; the present is declined",
static_cast<unsigned long long>(view.Id));
return false;
}
SharedImagePresentTarget target;
String why;
if (!VkTextureManager::ImportSharedImage(view.NativeBuffer, view.Width, view.Height, target.image, why)) {
MGLOG_E_ONCE("Magma: shared image %llu (%ux%u) not imported - %s; the present is declined",
static_cast<unsigned long long>(view.Id), view.Width, view.Height, why.c_str());
return false;
}
target.owner = owner;
found = m_sharedImagePresentTargets.emplace(view.Id, Move(target)).first;
}
const VkTextureManager::ImportedSharedImage& destination = found->second.image;
// The frame's draws reach the queue first; the copy below is a later submission that the
// readback barrier orders behind them.
if (HasPendingRecordedWork() && !FlushPendingCommands()) {
MGLOG_E("Magma: shared image %llu: the frame's recorded work could not be submitted",
static_cast<unsigned long long>(view.Id));
return false;
}
MG_Pipe::MGPFramebufferState defaultFramebuffer{};
defaultFramebuffer.IsDefault = 1;
const MG_Pipe::MGPSurface noSurface{};
WireImage source = ResolveWireImage(defaultFramebuffer, noSurface, VK_IMAGE_ASPECT_COLOR_BIT,
/*isWriteTarget=*/false);
if (source.image == VK_NULL_HANDLE || source.extent.width == 0 || source.extent.height == 0) {
MGLOG_E_ONCE("Magma: shared image %llu: no default framebuffer is current; the present is declined",
static_cast<unsigned long long>(view.Id));
return false;
}
if (!m_swapchainObject.IsImageContentDefined(source.swapchainImageIndex)) {
MGLOG_W_ONCE("Magma: shared image present: no write is recorded for image %u of the default "
"framebuffer; its raw pixels are copied",
source.swapchainImageIndex);
}
const Bool sameExtent = source.extent.width == view.Width && source.extent.height == view.Height;
const Bool halfTurn = preTransform == VK_SURFACE_TRANSFORM_ROTATE_180_BIT_KHR;
const Bool copy = sameExtent && !halfTurn && SharedImageCopiesAsIs(source.format, destination.format);
VkFormatProperties sourceProperties{};
vkGetPhysicalDeviceFormatProperties(m_physicalDevice.handle, source.format, &sourceProperties);
if (!copy) {
if ((sourceProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) == 0 ||
(destination.features & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) {
MGLOG_E_ONCE("Magma: shared image %llu: a %ux%u format-%d default framebuffer needs a blit into "
"the %ux%u format-%d image, which this device cannot do; the present is declined",
static_cast<unsigned long long>(view.Id), source.extent.width, source.extent.height,
static_cast<Int>(source.format), view.Width, view.Height,
static_cast<Int>(destination.format));
return false;
}
if (vkuFormatIsSRGB(source.format) && !vkuFormatIsSRGB(destination.format)) {
MGLOG_W_ONCE("Magma: shared image present: the sRGB default framebuffer is blitted into a UNORM "
"image, which stores linear values");
}
}
m_textureManager->FlushPendingUploads();
RecordWireReadbackWriteBarrier();
TransitionWireImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
auto& frame = m_frameContext.GetCurrent();
VkImageMemoryBarrier acquire{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
acquire.srcAccessMask = 0;
acquire.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
acquire.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
acquire.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
acquire.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
acquire.dstQueueFamilyIndex = destination.queueFamily;
acquire.image = destination.image;
acquire.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
0, nullptr, 0, nullptr, 1, &acquire);
const VkImageSubresourceLayers layers{VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
if (copy) {
VkImageCopy region{};
region.srcSubresource = layers;
region.dstSubresource = layers;
region.extent = {view.Width, view.Height, 1};
vkCmdCopyImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
} else {
// Identity: row 0 is already the top. 180: the stored image is the frame turned half
// round, so the destination rectangle runs backwards on both axes.
const Int32 width = static_cast<Int32>(view.Width);
const Int32 height = static_cast<Int32>(view.Height);
VkImageBlit region{};
region.srcSubresource = layers;
region.srcOffsets[1] = {static_cast<Int32>(source.extent.width), static_cast<Int32>(source.extent.height),
1};
region.dstSubresource = layers;
region.dstOffsets[0] = halfTurn ? VkOffset3D{width, height, 0} : VkOffset3D{0, 0, 0};
region.dstOffsets[1] = halfTurn ? VkOffset3D{0, 0, 1} : VkOffset3D{width, height, 1};
const VkFilter filter =
!sameExtent && (sourceProperties.optimalTilingFeatures &
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT) != 0
? VK_FILTER_LINEAR
: VK_FILTER_NEAREST;
vkCmdBlitImage(frame.commandBuffer, source.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region, filter);
}
VkImageMemoryBarrier release{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
release.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
release.dstAccessMask = 0;
release.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
release.newLayout = VK_IMAGE_LAYOUT_GENERAL;
release.srcQueueFamilyIndex = destination.queueFamily;
release.dstQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;
release.image = destination.image;
release.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
vkCmdPipelineBarrier(frame.commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, 0, nullptr, 0, nullptr, 1, &release);
// The client attaches the buffer to its surface as soon as this returns, and nothing else
// orders the compositor's read after the copy: wait for it here.
if (!SubmitReadbackCommandsAndWait(frame)) {
MGLOG_E("Magma: shared image %llu: the copy could not be submitted or waited for",
static_cast<unsigned long long>(view.Id));
return false;
}
return true;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
+39
View File
@@ -0,0 +1,39 @@
# MobileGL - MobileGL/MG_Gbm/CMakeLists.txt
#
# mobilegl_gbm.so: the GBM backend whose buffers are MobileGL shared images (MobileGLGbm.cpp).
# A loadable module for the system GBM loader, NOT part of libMobileGL.so - it reaches the library
# at run time through dlsym. Built against the system's <gbm.h> and <gbm_backend_abi.h> (MIT,
# shipped with the GBM loader's development files); a system without them builds without it.
#
# Usable standalone too (cmake -S MobileGL/MG_Gbm), for building just the plugin.
cmake_minimum_required(VERSION 3.16)
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
project(mobilegl_gbm CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
endif()
find_path(MOBILEGL_GBM_BACKEND_ABI_INCLUDE_DIR gbm_backend_abi.h)
find_path(MOBILEGL_GBM_INCLUDE_DIR gbm.h)
if (NOT MOBILEGL_GBM_BACKEND_ABI_INCLUDE_DIR OR NOT MOBILEGL_GBM_INCLUDE_DIR)
message(STATUS "MobileGL: gbm.h / gbm_backend_abi.h not found, mobilegl_gbm is not built")
return()
endif()
add_library(mobilegl_gbm MODULE MobileGLGbm.cpp)
set_target_properties(mobilegl_gbm PROPERTIES
PREFIX ""
OUTPUT_NAME mobilegl_gbm
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)
target_include_directories(mobilegl_gbm PRIVATE
${MOBILEGL_GBM_INCLUDE_DIR}
${MOBILEGL_GBM_BACKEND_ABI_INCLUDE_DIR}
# SharedImageApi.h only: the plugin shares a C header with the library, not its code.
${CMAKE_CURRENT_SOURCE_DIR}/../MG_Impl
)
target_link_libraries(mobilegl_gbm PRIVATE ${CMAKE_DL_LIBS})
# Loaded into compositors: an unresolved symbol must fail the link, not the dlopen.
target_link_options(mobilegl_gbm PRIVATE "LINKER:--no-undefined")
+389
View File
@@ -0,0 +1,389 @@
// MobileGL - MobileGL/MG_Gbm/MobileGLGbm.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_gbm.so: a GBM backend (the loader's backend ABI, gbm_backend_abi.h) whose buffer
// objects are MobileGL shared images - colour buffers the MobileGL server allocates and exports
// as dma-bufs (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). Selected with
// GBM_BACKEND=mobilegl; the loader then finds <dir>/mobilegl_gbm.so on its backend path.
//
// THE DEVICE DESCRIPTOR IS OPAQUE. The fd a gbm_device is created on is only the identity the
// caller chose (a render node, or any character device); no ioctl is ever issued on it, because
// the buffers do not come from that device's driver - they come from the server, over the
// process's MobileGL client session. Whatever node the caller holds is therefore equally good,
// and none is vendor-specific.
//
// THE LAYOUT IS THE ALLOCATOR'S. Buffers report DRM_FORMAT_MOD_INVALID and a single plane; a
// request that needs a known layout (LINEAR, or a modifier list without INVALID) is refused
// rather than answered with a buffer whose layout is not what was asked for.
//
// Buffer operations reach the server through libMobileGL.so's shared-image C ABI
// (MG_Impl/SharedImageApi.h), resolved with dlsym on first use - preferably from the instance the
// process has already loaded (its EGL vendor), so the buffers belong to the same session.
#include "SharedImageApi.h"
#include <gbm.h>
#include <gbm_backend_abi.h>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <new>
#include <dlfcn.h>
#include <fcntl.h>
#include <unistd.h>
#define MOBILEGL_GBM_EXPORT extern "C" __attribute__((visibility("default")))
namespace {
constexpr uint64_t kModifierLinear = 0;
constexpr uint64_t kModifierInvalid = 0x00ffffffffffffffull;
// Usages no shared image can honour: a CPU-written cursor (bo_write), a guaranteed-linear
// layout, protected content. Scanout/rendering/texturing are accepted - every caller of
// gbm_bo_create passes them, and a shared image serves all three.
constexpr uint32_t kRefusedUsage = GBM_BO_USE_CURSOR | GBM_BO_USE_WRITE | GBM_BO_USE_LINEAR | GBM_BO_USE_PROTECTED;
const gbm_core* g_core = nullptr;
struct Device {
gbm_device base; // first: the loader's gbm_device* is this object
};
struct Bo {
gbm_bo base; // first: the loader's gbm_bo* is this object
uint64_t imageId;
int fd; // owned; every get_fd hands out a duplicate
uint32_t offset;
uint64_t modifier;
};
// ---- libMobileGL's shared-image ABI -----------------------------------------------------
struct SharedImageApi {
PFN_mobilegl_shared_image_allocate allocate = nullptr;
PFN_mobilegl_shared_image_import import = nullptr;
PFN_mobilegl_shared_image_release release = nullptr;
};
// The library the process already has (by its soname: glvnd loads it by path, which records
// the soname too), else MOBILEGL_GBM_LIBRARY, else the loader's search path. Never closed:
// libMobileGL leaks its globals at exit by design and must not be unloaded under them.
const SharedImageApi* Api() {
static std::once_flag once;
static SharedImageApi api;
static bool resolved = false;
std::call_once(once, [] {
void* lib = dlopen("libMobileGL.so", RTLD_NOW | RTLD_NOLOAD);
if (lib == nullptr) {
const char* path = std::getenv("MOBILEGL_GBM_LIBRARY");
lib = dlopen(path != nullptr && path[0] != '\0' ? path : "libMobileGL.so", RTLD_NOW | RTLD_LOCAL);
}
if (lib == nullptr) {
std::fprintf(stderr, "mobilegl_gbm: libMobileGL.so could not be loaded: %s\n", dlerror());
return;
}
const auto version = reinterpret_cast<PFN_mobilegl_shared_image_abi_version>(
dlsym(lib, "mobilegl_shared_image_abi_version"));
api.allocate = reinterpret_cast<PFN_mobilegl_shared_image_allocate>(dlsym(lib, "mobilegl_shared_image_allocate"));
api.import = reinterpret_cast<PFN_mobilegl_shared_image_import>(dlsym(lib, "mobilegl_shared_image_import"));
api.release = reinterpret_cast<PFN_mobilegl_shared_image_release>(dlsym(lib, "mobilegl_shared_image_release"));
if (version == nullptr || version() < 1 || api.allocate == nullptr || api.import == nullptr ||
api.release == nullptr) {
std::fprintf(stderr, "mobilegl_gbm: this libMobileGL.so has no shared-image ABI\n");
return;
}
resolved = true;
});
return resolved ? &api : nullptr;
}
// ---- formats ----------------------------------------------------------------------------
uint32_t Canonical(uint32_t format) {
if (g_core != nullptr && g_core->v0.format_canonicalize != nullptr) return g_core->v0.format_canonicalize(format);
// The two legacy GBM_BO_FORMAT_* enumerants are the only non-fourcc formats.
if (format == GBM_BO_FORMAT_XRGB8888) return GBM_FORMAT_XRGB8888;
if (format == GBM_BO_FORMAT_ARGB8888) return GBM_FORMAT_ARGB8888;
return format;
}
// What a shared image can be. ARGB8888/XRGB8888 (B, G, R, A in memory) are absent on purpose:
// the server's images hold R, G, B, A, and an image whose bytes disagree with its fourcc is
// worse than no image.
bool FormatSupported(uint32_t format) {
return format == MOBILEGL_SHARED_IMAGE_FOURCC_ABGR8888 || format == MOBILEGL_SHARED_IMAGE_FOURCC_XBGR8888;
}
bool ModifierAcceptable(uint64_t modifier) { return modifier == kModifierInvalid || modifier == kModifierLinear; }
// There is no GEM handle behind a shared image; callers that only use the handle as a key
// (a buffer cache) still need it to be unique.
uint32_t NextHandle() {
static std::atomic<uint32_t> next{1};
uint32_t handle = next.fetch_add(1, std::memory_order_relaxed);
if (handle == 0) handle = next.fetch_add(1, std::memory_order_relaxed);
return handle;
}
gbm_bo* NewBo(gbm_device* gbm, uint64_t imageId, int fd, uint32_t width, uint32_t height, uint32_t format,
uint32_t stride, uint32_t offset, uint64_t modifier) {
auto* bo = new (std::nothrow) Bo{};
if (bo == nullptr) {
errno = ENOMEM;
return nullptr;
}
bo->base.gbm = gbm;
bo->base.v0.width = width;
bo->base.v0.height = height;
bo->base.v0.stride = stride;
bo->base.v0.format = format;
bo->base.v0.handle.u32 = NextHandle();
bo->imageId = imageId;
bo->fd = fd;
bo->offset = offset;
bo->modifier = modifier;
return &bo->base;
}
Bo* AsBo(gbm_bo* bo) { return reinterpret_cast<Bo*>(bo); }
// ---- device -----------------------------------------------------------------------------
void DeviceDestroy(gbm_device* gbm) { delete reinterpret_cast<Device*>(gbm); }
int IsFormatSupported(gbm_device*, uint32_t format, uint32_t usage) {
return FormatSupported(Canonical(format)) && (usage & kRefusedUsage) == 0 ? 1 : 0;
}
int GetFormatModifierPlaneCount(gbm_device*, uint32_t format, uint64_t modifier) {
if (!FormatSupported(Canonical(format)) || !ModifierAcceptable(modifier)) return -1;
return 1;
}
gbm_bo* BoCreate(gbm_device* gbm, uint32_t width, uint32_t height, uint32_t format, uint32_t usage,
const uint64_t* modifiers, const unsigned int count) {
format = Canonical(format);
if (!FormatSupported(format) || (usage & kRefusedUsage) != 0) {
errno = EINVAL;
return nullptr;
}
if (count > 0) {
// The layout is the allocator's, which only a list that admits INVALID accepts.
bool admitsImplicit = false;
for (unsigned int i = 0; i < count; ++i) admitsImplicit |= modifiers[i] == kModifierInvalid;
if (!admitsImplicit) {
errno = EINVAL;
return nullptr;
}
}
const SharedImageApi* api = Api();
if (api == nullptr) {
errno = ENOSYS;
return nullptr;
}
mobilegl_shared_image image{};
image.struct_size = sizeof(image);
image.fd = -1;
const int rc = api->allocate(width, height, format, &image);
if (rc != 0 || image.fd < 0) {
if (image.fd >= 0) ::close(image.fd);
errno = rc < 0 ? -rc : EIO;
return nullptr;
}
gbm_bo* bo = NewBo(gbm, image.id, image.fd, image.width, image.height, image.fourcc, image.stride,
image.offset, image.modifier);
if (bo == nullptr) {
api->release(image.id);
::close(image.fd);
}
return bo;
}
gbm_bo* BoImport(gbm_device* gbm, uint32_t type, void* buffer, uint32_t usage) {
(void)usage;
if (buffer == nullptr) {
errno = EINVAL;
return nullptr;
}
int fd = -1;
uint32_t width = 0, height = 0, format = 0, stride = 0, offset = 0;
uint64_t modifier = kModifierInvalid;
switch (type) {
case GBM_BO_IMPORT_FD: {
const auto* data = static_cast<const gbm_import_fd_data*>(buffer);
fd = data->fd;
width = data->width;
height = data->height;
format = data->format;
stride = data->stride;
break;
}
case GBM_BO_IMPORT_FD_MODIFIER: {
const auto* data = static_cast<const gbm_import_fd_modifier_data*>(buffer);
if (data->num_fds != 1 || !ModifierAcceptable(data->modifier)) {
errno = EINVAL;
return nullptr;
}
fd = data->fds[0];
width = data->width;
height = data->height;
format = data->format;
stride = static_cast<uint32_t>(data->strides[0]);
offset = static_cast<uint32_t>(data->offsets[0]);
modifier = data->modifier;
break;
}
default:
// A wl_buffer or an EGLImage would have to be resolved through a display this backend
// does not own; their owners can export a dma-buf and import that instead.
errno = ENOSYS;
return nullptr;
}
format = Canonical(format);
if (fd < 0 || width == 0 || height == 0 || !FormatSupported(format)) {
errno = EINVAL;
return nullptr;
}
const SharedImageApi* api = Api();
if (api == nullptr) {
errno = ENOSYS;
return nullptr;
}
// The caller keeps its descriptor; the bo holds its own.
const int owned = ::fcntl(fd, F_DUPFD_CLOEXEC, 0);
if (owned < 0) return nullptr;
uint64_t id = 0;
const int rc = api->import(owned, width, height, format, &id);
if (rc != 0) {
::close(owned);
errno = -rc;
return nullptr;
}
gbm_bo* bo = NewBo(gbm, id, owned, width, height, format, stride, offset, modifier);
if (bo == nullptr) {
api->release(id);
::close(owned);
}
return bo;
}
// CPU access is not offered: the layout is opaque, so a mapping would need the server to
// copy through a linear staging buffer, which does not exist yet.
void* BoMap(gbm_bo*, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t*, void**) {
errno = ENOSYS;
return nullptr;
}
void BoUnmap(gbm_bo*, void*) {}
int BoWrite(gbm_bo*, const void*, size_t) {
errno = ENOSYS;
return -1;
}
int BoGetFd(gbm_bo* bo) { return ::fcntl(AsBo(bo)->fd, F_DUPFD_CLOEXEC, 0); }
int BoGetPlanes(gbm_bo*) { return 1; }
gbm_bo_handle BoGetHandle(gbm_bo* bo, int plane) {
gbm_bo_handle handle{};
if (plane != 0) {
errno = EINVAL;
handle.s32 = -1;
return handle;
}
return bo->v0.handle;
}
int BoGetPlaneFd(gbm_bo* bo, int plane) {
if (plane != 0) {
errno = EINVAL;
return -1;
}
return BoGetFd(bo);
}
uint32_t BoGetStride(gbm_bo* bo, int plane) { return plane == 0 ? bo->v0.stride : 0; }
uint32_t BoGetOffset(gbm_bo* bo, int plane) { return plane == 0 ? AsBo(bo)->offset : 0; }
uint64_t BoGetModifier(gbm_bo* bo) { return AsBo(bo)->modifier; }
void BoDestroy(gbm_bo* gbm) {
Bo* bo = AsBo(gbm);
if (const SharedImageApi* api = Api()) api->release(bo->imageId);
if (bo->fd >= 0) ::close(bo->fd);
delete bo;
}
// Window surfaces belong to the EGL platform, not to this allocator.
gbm_surface* SurfaceCreate(gbm_device*, uint32_t, uint32_t, uint32_t, uint32_t, const uint64_t*, const unsigned) {
errno = ENOSYS;
return nullptr;
}
gbm_bo* SurfaceLockFrontBuffer(gbm_surface*) { return nullptr; }
void SurfaceReleaseBuffer(gbm_surface*, gbm_bo*) {}
int SurfaceHasFreeBuffers(gbm_surface*) { return 0; }
void SurfaceDestroy(gbm_surface*) {}
gbm_device* CreateDevice(int fd, uint32_t backendVersion) {
auto* device = new (std::nothrow) Device{};
if (device == nullptr) return nullptr;
gbm_device_v0& v0 = device->base.v0;
v0.backend_version = backendVersion; // the loader checks this is what it asked for
v0.fd = fd;
v0.name = "mobilegl";
v0.destroy = DeviceDestroy;
v0.is_format_supported = IsFormatSupported;
v0.get_format_modifier_plane_count = GetFormatModifierPlaneCount;
v0.bo_create = BoCreate;
v0.bo_import = BoImport;
v0.bo_map = BoMap;
v0.bo_unmap = BoUnmap;
v0.bo_write = BoWrite;
v0.bo_get_fd = BoGetFd;
v0.bo_get_planes = BoGetPlanes;
v0.bo_get_handle = BoGetHandle;
v0.bo_get_plane_fd = BoGetPlaneFd;
v0.bo_get_stride = BoGetStride;
v0.bo_get_offset = BoGetOffset;
v0.bo_get_modifier = BoGetModifier;
v0.bo_destroy = BoDestroy;
v0.surface_create = SurfaceCreate;
v0.surface_lock_front_buffer = SurfaceLockFrontBuffer;
v0.surface_release_buffer = SurfaceReleaseBuffer;
v0.surface_has_free_buffers = SurfaceHasFreeBuffers;
v0.surface_destroy = SurfaceDestroy;
return &device->base;
}
// ABI 1 differs from 0 only in the loader populating usage/flags alongside modifiers, which
// this backend reads as they come; every structure here is the v0 layout.
const gbm_backend kBackend = {
.v0 =
{
.backend_version = GBM_BACKEND_ABI_VERSION,
.backend_name = "mobilegl",
.create_device = CreateDevice,
},
};
} // namespace
MOBILEGL_GBM_EXPORT const gbm_backend* GBM_GET_BACKEND_PROC(const gbm_core* core) {
g_core = core;
return &kBackend;
}
+92 -3
View File
@@ -632,6 +632,10 @@ namespace MobileGL::MG_Impl::EGLImpl {
return EGL_FALSE;
}
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
// The display's dma-buf EGLImages went with it; so do their shared-image references.
for (const Uint64 sharedImageId : state->TakeOrphanedSharedImages()) {
(void)backendObject->ReleaseSharedImage(sharedImageId);
}
backendObject->ReleaseEGLResources();
}
// The last initialized display is gone and nothing is current on any
@@ -732,7 +736,8 @@ namespace MobileGL::MG_Impl::EGLImpl {
// 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;
if (display == EGL_NO_DISPLAY) return kClientExtensionString;
return SharedImagesAvailable() ? kDisplayExtensionStringWithSharedImages : kDisplayExtensionString;
default:
state->SetError(EGL_BAD_PARAMETER);
return nullptr;
@@ -950,13 +955,63 @@ namespace MobileGL::MG_Impl::EGLImpl {
return state->GetSyncAttrib(dpy, sync, attribute, value) ? EGL_TRUE : EGL_FALSE;
}
Bool SharedImagesAvailable() {
#if MOBILEGL_BUILD_DISAGGREGATED
// A split client's backend is the remote one, which forwards the shared-image verbs to
// the server; the server's own apply thread and a monolith have no allocator behind them.
return StreamGateActive() && MG_Backend::pActiveBackendObject != nullptr;
#else
return false;
#endif
}
Bool LookupSharedImage(EGLImage image, Uint64* id, EGLint* width, EGLint* height) {
auto* state = GetState();
MG_State::EGLState::EGLContext::SharedImageInfo info;
if (!state || !state->GetSharedImage(image, &info)) return false;
*id = info.Id;
*width = info.Width;
*height = info.Height;
return true;
}
EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLAttrib* attrib_list) {
auto* state = GetState();
if (!state) {
return EGL_NO_IMAGE;
}
return state->CreateImage(dpy, ctx, target, buffer, attrib_list);
if (target != EGL_LINUX_DMA_BUF_EXT) {
return state->CreateImage(dpy, ctx, target, buffer, attrib_list);
}
// EGL_EXT_image_dma_buf_import. The only dma-bufs this library can bind are the server's
// own shared images (a client's linux-dmabuf wl_buffer): the descriptor goes to the
// server, which names the image it was exported from, or refuses it.
if (!SharedImagesAvailable()) {
state->SetError(EGL_BAD_PARAMETER);
return EGL_NO_IMAGE;
}
MG_State::EGLState::EGLContext::DmaBufImportAttribs request;
if (!state->PrepareDmaBufImport(dpy, ctx, buffer, attrib_list, &request)) {
return EGL_NO_IMAGE;
}
auto* backendObject = GetBackendObject(state);
Uint64 sharedImageId = 0;
if (!backendObject ||
!backendObject->ImportSharedImage(request.Fd, static_cast<Uint32>(request.Width),
static_cast<Uint32>(request.Height), request.Fourcc, &sharedImageId) ||
sharedImageId == 0) {
MGLOG_I_ONCE("eglCreateImage: a %dx%d dma-buf is not one of the server's shared images; refused "
"(EGL_BAD_MATCH)",
request.Width, request.Height);
state->SetError(EGL_BAD_MATCH);
return EGL_NO_IMAGE;
}
const EGLImage image = state->CreateSharedImage(
dpy, {.Id = sharedImageId, .Width = request.Width, .Height = request.Height, .Fourcc = request.Fourcc});
if (image == EGL_NO_IMAGE) (void)backendObject->ReleaseSharedImage(sharedImageId);
return image;
}
EGLBoolean DestroyImage(EGLDisplay dpy, EGLImage image) {
@@ -964,7 +1019,41 @@ namespace MobileGL::MG_Impl::EGLImpl {
if (!state) {
return EGL_FALSE;
}
return state->DestroyImage(dpy, image) ? EGL_TRUE : EGL_FALSE;
Uint64 sharedImageId = 0;
if (!state->DestroyImage(dpy, image, &sharedImageId)) {
return EGL_FALSE;
}
// A texture the image was bound to keeps its own reference on the server (EGLImage
// siblings outlive the image), so only this handle's goes.
if (sharedImageId != 0) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
(void)backendObject->ReleaseSharedImage(sharedImageId);
}
}
return EGL_TRUE;
}
EGLBoolean QueryDmaBufFormats(EGLDisplay dpy, EGLint max_formats, EGLint* formats, EGLint* num_formats) {
auto* state = GetState();
if (!state) {
return EGL_FALSE;
}
return state->QueryDmaBufFormats(dpy, SharedImagesAvailable(), max_formats, formats, num_formats)
? EGL_TRUE
: EGL_FALSE;
}
EGLBoolean QueryDmaBufModifiers(EGLDisplay dpy, EGLint format, EGLint max_modifiers, EGLuint64KHR* modifiers,
EGLBoolean* external_only, EGLint* num_modifiers) {
static_assert(sizeof(EGLuint64KHR) == sizeof(Uint64));
auto* state = GetState();
if (!state) {
return EGL_FALSE;
}
return state->QueryDmaBufModifiers(dpy, SharedImagesAvailable(), format, max_modifiers,
reinterpret_cast<Uint64*>(modifiers), external_only, num_modifiers)
? EGL_TRUE
: EGL_FALSE;
}
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) {
+10
View File
@@ -8,6 +8,7 @@
#pragma once
#include <Includes.h>
#include <EGL/eglext.h>
#include <mutex>
@@ -56,6 +57,15 @@ namespace MobileGL::MG_Impl::EGLImpl {
EGLImage CreateImage(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLAttrib* attrib_list);
EGLBoolean DestroyImage(EGLDisplay dpy, EGLImage image);
EGLBoolean QueryDmaBufFormats(EGLDisplay dpy, EGLint max_formats, EGLint* formats, EGLint* num_formats);
EGLBoolean QueryDmaBufModifiers(EGLDisplay dpy, EGLint format, EGLint max_modifiers, EGLuint64KHR* modifiers,
EGLBoolean* external_only, EGLint* num_modifiers);
// Whether the active backend has shared images (MG_Backend::BackendObject::AllocateSharedImage):
// a split client's does, a monolith's does not. Decides the dma-buf EGL extensions, the
// Wayland linux-dmabuf presentation and GL_OES_EGL_image.
Bool SharedImagesAvailable();
// The shared image an EGLImage names (EGL_LINUX_DMA_BUF_EXT), for glEGLImageTargetTexture2DOES.
Bool LookupSharedImage(EGLImage image, Uint64* id, EGLint* width, EGLint* height);
EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list);
EGLSurface CreatePlatformWindowSurface(EGLDisplay dpy, EGLConfig config, void* native_window,
const EGLAttrib* attrib_list);
@@ -21,6 +21,13 @@ namespace MobileGL::MG_Impl::EGLImpl {
inline constexpr const char* kDisplayExtensionString =
"EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
// The display list again, with the dma-buf import a backend with shared images has (a split
// client: the server allocates the images and names the descriptors that come back). A
// monolith has no allocator to recognise a dma-buf with, so it never advertises these.
inline constexpr const char* kDisplayExtensionStringWithSharedImages =
"EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base "
"EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS
" EGL_KHR_image_base EGL_EXT_image_dma_buf_import EGL_EXT_image_dma_buf_import_modifiers";
inline constexpr const char* kClientExtensionString =
"EGL_EXT_client_extensions EGL_KHR_create_context EGL_EXT_platform_base "
"EGL_KHR_platform_base EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS;
@@ -304,6 +304,24 @@ MOBILEGL_EGL_API EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImage image) {
return MobileGL::MG_Impl::EGLImpl::DestroyImage(dpy, image);
}
// EGL_EXT_image_dma_buf_import_modifiers: the formats eglCreateImage(EGL_LINUX_DMA_BUF_EXT) takes,
// and (none) explicit modifiers - a compositor then imports with the implicit one.
MOBILEGL_EGL_API EGLBoolean eglQueryDmaBufFormatsEXT(EGLDisplay dpy, EGLint max_formats, EGLint* formats,
EGLint* num_formats) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglQueryDmaBufFormatsEXT(dpy=%p, max_formats=%d)", dpy, max_formats);
return MobileGL::MG_Impl::EGLImpl::QueryDmaBufFormats(dpy, max_formats, formats, num_formats);
}
MOBILEGL_EGL_API EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay dpy, EGLint format, EGLint max_modifiers,
EGLuint64KHR* modifiers, EGLBoolean* external_only,
EGLint* num_modifiers) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
MGLOG_D("eglQueryDmaBufModifiersEXT(dpy=%p, format=0x%08x, max_modifiers=%d)", dpy, format, max_modifiers);
return MobileGL::MG_Impl::EGLImpl::QueryDmaBufModifiers(dpy, format, max_modifiers, modifiers, external_only,
num_modifiers);
}
MOBILEGL_EGL_API EGLDisplay eglGetPlatformDisplay(EGLenum platform, void* native_display,
const EGLAttrib* attrib_list) {
const MobileGL::MG_Impl::EGLImpl::StreamLockScope mglStreamLock;
@@ -36,6 +36,11 @@
extern "C" EGLImage eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImage image);
extern "C" EGLBoolean eglQueryDmaBufFormatsEXT(EGLDisplay dpy, EGLint max_formats, EGLint* formats,
EGLint* num_formats);
extern "C" EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay dpy, EGLint format, EGLint max_modifiers,
EGLuint64KHR* modifiers, EGLBoolean* external_only,
EGLint* num_modifiers);
namespace {
using namespace MobileGL;
@@ -228,6 +233,8 @@ namespace {
{"eglDestroyImage", reinterpret_cast<void*>(eglDestroyImage)},
{"eglCreateImageKHR", reinterpret_cast<void*>(eglCreateImageKHR)},
{"eglDestroyImageKHR", reinterpret_cast<void*>(eglDestroyImageKHR)},
{"eglQueryDmaBufFormatsEXT", reinterpret_cast<void*>(eglQueryDmaBufFormatsEXT)},
{"eglQueryDmaBufModifiersEXT", reinterpret_cast<void*>(eglQueryDmaBufModifiersEXT)},
{"eglCreatePlatformWindowSurface", reinterpret_cast<void*>(eglCreatePlatformWindowSurface)},
{"eglCreatePlatformPixmapSurface", reinterpret_cast<void*>(eglCreatePlatformPixmapSurface)},
// The platform and current-state queries: not in glvnd's required list, but the names an
+295 -26
View File
@@ -10,6 +10,8 @@
#if MOBILEGL_WAYLAND_WINDOWS
#include "EGLImpl.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
@@ -67,12 +69,28 @@ namespace MobileGL::MG_Impl::EGLImpl {
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.
// libwayland-client's objects are opaque here: everything goes through wl_proxy_* and the
// interface descriptions, and nothing reads a field of a proxy.
struct wl_proxy;
struct wl_interface;
struct wl_event_queue;
// wayland-util.h's protocol descriptors. libwayland-client exports the core protocol's;
// linux-dmabuf's are written out below (LinuxDmabuf), as its generated code would.
struct wl_interface;
struct wl_message {
const char* name;
const char* signature;
const wl_interface** types;
};
struct wl_interface {
const char* name;
int version;
int method_count;
const wl_message* methods;
int event_count;
const wl_message* events;
};
// 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 {
@@ -106,6 +124,18 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// B, G, R, A for both (little-endian 0xAARRGGBB).
constexpr uint32_t kShmFormatArgb8888 = 0;
constexpr uint32_t kShmFormatXrgb8888 = 1;
// linux-dmabuf (linux-dmabuf-v1.xml) request opcodes.
constexpr uint32_t kDmabufDestroy = 0;
constexpr uint32_t kDmabufCreateParams = 1;
constexpr uint32_t kParamsDestroy = 0;
constexpr uint32_t kParamsAdd = 1;
constexpr uint32_t kParamsCreateImmed = 3; // since version 2
// The highest linux-dmabuf version spoken here: 4 adds feedback objects this never asks for.
constexpr uint32_t kDmabufMaxVersion = 3;
// DRM fourccs of the server's shared images: R, G, B, A in memory - GL's RGBA8, so the
// frame lands in the buffer with no reordering.
constexpr uint32_t kDrmFormatAbgr8888 = 0x34324241u; // 'AB24'
constexpr uint32_t kDrmFormatXbgr8888 = 0x34324258u; // 'XB24'
struct Api {
Bool loaded = false;
@@ -181,6 +211,85 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// 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)};
// zwp_linux_dmabuf_v1 and zwp_linux_buffer_params_v1 as their generated protocol code
// describes them (version 3: what is bound here at most). A request's `types` name the
// interface of each new_id/object argument, null for every other argument.
struct LinuxDmabufProtocol {
const wl_interface* createParamsTypes[1] = {};
const wl_interface* noObjectTypes[6] = {};
const wl_interface* createImmedTypes[5] = {};
const wl_interface* createdTypes[1] = {};
wl_message dmabufRequests[2] = {};
wl_message dmabufEvents[2] = {};
wl_message paramsRequests[4] = {};
wl_message paramsEvents[2] = {};
wl_interface dmabuf = {};
wl_interface params = {};
};
// Null when libwayland-client is not usable. Filled once: the wl_buffer interface it
// refers to is libwayland-client's, known only after it is loaded.
const LinuxDmabufProtocol* LinuxDmabuf() {
static LinuxDmabufProtocol protocol;
static std::once_flag once;
static Bool ready = false;
std::call_once(once, [] {
const Api& api = WaylandApi();
if (!api.loaded) return;
LinuxDmabufProtocol& p = protocol;
p.createParamsTypes[0] = &p.params;
p.createImmedTypes[0] = api.bufferInterface;
p.createdTypes[0] = api.bufferInterface;
p.dmabufRequests[0] = {"destroy", "", p.noObjectTypes};
p.dmabufRequests[1] = {"create_params", "n", p.createParamsTypes};
p.dmabufEvents[0] = {"format", "u", p.noObjectTypes};
p.dmabufEvents[1] = {"modifier", "3uuu", p.noObjectTypes};
p.paramsRequests[0] = {"destroy", "", p.noObjectTypes};
p.paramsRequests[1] = {"add", "huuuuu", p.noObjectTypes};
p.paramsRequests[2] = {"create", "iiuu", p.noObjectTypes};
p.paramsRequests[3] = {"create_immed", "2niiuu", p.createImmedTypes};
p.paramsEvents[0] = {"created", "n", p.createdTypes};
p.paramsEvents[1] = {"failed", "", p.noObjectTypes};
p.dmabuf = {"zwp_linux_dmabuf_v1", static_cast<int>(kDmabufMaxVersion), 2, p.dmabufRequests, 2,
p.dmabufEvents};
p.params = {"zwp_linux_buffer_params_v1", static_cast<int>(kDmabufMaxVersion), 4, p.paramsRequests,
2, p.paramsEvents};
ready = true;
});
return ready ? &protocol : nullptr;
}
// The format table a compositor announces is not consulted: the server's images are one
// of the two formats every linux-dmabuf compositor takes, and a refusal is answered by
// `failed` below anyway.
void OnDmabufFormat(void*, wl_proxy*, uint32_t) {}
void OnDmabufModifier(void*, wl_proxy*, uint32_t, uint32_t, uint32_t) {}
void (*const kDmabufListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnDmabufFormat),
reinterpret_cast<void (*)(void)>(&OnDmabufModifier)};
void OnParamsCreated(void*, wl_proxy*, wl_proxy*) {}
void OnParamsFailed(void* data, wl_proxy*) { *static_cast<Bool*>(data) = true; }
void (*const kParamsListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnParamsCreated),
reinterpret_cast<void (*)(void)>(&OnParamsFailed)};
// One server shared image, shown to the compositor as a linux-dmabuf wl_buffer.
struct DmabufBuffer {
wl_proxy* buffer = nullptr;
Uint64 imageId = 0;
EGLint width = 0;
EGLint height = 0;
Bool busy = false;
};
void OnDmabufBufferRelease(void* data, wl_proxy*) { static_cast<DmabufBuffer*>(data)->busy = false; }
void (*const kDmabufBufferListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnDmabufBufferRelease)};
// MOBILEGL_WAYLAND_DMABUF=0 keeps every window on wl_shm.
Bool DmabufPresentationAllowed() {
const char* value = std::getenv("MOBILEGL_WAYLAND_DMABUF");
return value == nullptr || std::strcmp(value, "0") != 0;
}
} // namespace
struct WindowSurface::Impl {
@@ -197,9 +306,21 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
ShmBuffer buffers[3];
Vector<Uint8> scratch;
// THE LINUX-DMABUF PATH. With a backend that has shared images, the frame never leaves
// the GPU: each buffer is a server-allocated image exported as a dma-buf, the server
// copies the frame into it at the swap, and the compositor (a client of the same server)
// samples the very same image. wl_shm stays bound as the fallback.
Bool dmabufWanted = false;
wl_proxy* dmabuf = nullptr;
Bool useDmabuf = false;
uint32_t fourcc = kDrmFormatXbgr8888;
DmabufBuffer dmabufBuffers[3];
~Impl() {
const Api& api = WaylandApi();
for (auto& buffer : buffers) Release(buffer);
for (auto& buffer : dmabufBuffers) Release(buffer);
if (dmabuf) api.marshalFlags(dmabuf, kDmabufDestroy, nullptr, api.getVersion(dmabuf), kMarshalFlagDestroy);
// Neither wl_shm (version 1) nor wl_registry has a destroy request: the proxies go
// on this side only.
if (shm) api.destroy(shm);
@@ -286,14 +407,147 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
if (api.dispatchQueue(display, queue) < 0) return nullptr;
}
}
// The wl_buffer goes before the image: the compositor's own import holds the image for
// as long as it still shows it.
void Release(DmabufBuffer& buffer) {
const Api& api = WaylandApi();
if (buffer.buffer) api.marshalFlags(buffer.buffer, kBufferDestroy, nullptr, api.getVersion(buffer.buffer),
kMarshalFlagDestroy);
if (buffer.imageId != 0) {
if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) {
(void)backendObject->ReleaseSharedImage(buffer.imageId);
}
}
buffer = DmabufBuffer{};
}
// A shared image of the window's size, as a wl_buffer. create_immed plus a round trip
// rather than a bare create_immed: a compositor that refuses the import says so with
// `failed`, and attaching that buffer would be a protocol error, fatal to the connection.
Bool Allocate(DmabufBuffer& buffer) {
const Api& api = WaylandApi();
const LinuxDmabufProtocol* protocol = LinuxDmabuf();
Release(buffer);
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (protocol == nullptr || dmabuf == nullptr || backendObject == nullptr) return false;
MG_Backend::SharedImageExport image;
if (!backendObject->AllocateSharedImage(static_cast<Uint32>(width), static_cast<Uint32>(height), fourcc,
&image)) {
MGLOG_E("Wayland: the server allocated no %dx%d shared image", width, height);
return false;
}
const auto dropImage = [&] {
if (image.Fd >= 0) close(image.Fd);
(void)backendObject->ReleaseSharedImage(image.Id);
};
if (image.Fd < 0 || image.Width != static_cast<Uint32>(width) ||
image.Height != static_cast<Uint32>(height)) {
MGLOG_E("Wayland: a %dx%d shared image came back as %ux%u (fd %d)", width, height, image.Width,
image.Height, image.Fd);
dropImage();
return false;
}
const uint32_t version = api.getVersion(dmabuf);
wl_proxy* params = api.marshalFlags(dmabuf, kDmabufCreateParams, &protocol->params, version, 0, nullptr);
if (params == nullptr) {
dropImage();
return false;
}
Bool failed = false;
api.addListener(params, const_cast<void (**)(void)>(kParamsListener), &failed);
// The descriptor is duplicated into the request, so it is closed below either way.
api.marshalFlags(params, kParamsAdd, nullptr, version, 0, image.Fd, 0u, image.Offset, image.Stride,
static_cast<uint32_t>(image.Modifier >> 32),
static_cast<uint32_t>(image.Modifier & 0xffffffffu));
wl_proxy* wlBuffer = api.marshalFlags(params, kParamsCreateImmed, api.bufferInterface, version, 0, nullptr,
width, height, fourcc, 0u);
const Bool settled = api.roundtripQueue(display, queue) >= 0;
api.marshalFlags(params, kParamsDestroy, nullptr, version, kMarshalFlagDestroy);
if (!settled || failed || wlBuffer == nullptr) {
MGLOG_E("Wayland: the compositor refused a %dx%d linux-dmabuf buffer", width, height);
if (wlBuffer) api.marshalFlags(wlBuffer, kBufferDestroy, nullptr, version, kMarshalFlagDestroy);
dropImage();
return false;
}
close(image.Fd);
buffer.buffer = wlBuffer;
buffer.imageId = image.Id;
buffer.width = width;
buffer.height = height;
buffer.busy = false;
api.addListener(wlBuffer, const_cast<void (**)(void)>(kDmabufBufferListener), &buffer);
return true;
}
// As NextFree, over the shared images.
DmabufBuffer* NextFreeDmabuf() {
const Api& api = WaylandApi();
for (;;) {
if (api.dispatchQueuePending(display, queue) < 0) return nullptr;
for (auto& buffer : dmabufBuffers) {
if (buffer.buffer == nullptr || buffer.busy) continue;
if (buffer.width != width || buffer.height != height) {
if (!Allocate(buffer)) return nullptr;
}
return &buffer;
}
for (auto& buffer : dmabufBuffers) {
if (buffer.buffer == nullptr) return Allocate(buffer) ? &buffer : nullptr;
}
if (api.dispatchQueue(display, queue) < 0) return nullptr;
}
}
// The rest of this window's life is wl_shm's.
void FallBackToShm() {
for (auto& buffer : dmabufBuffers) Release(buffer);
useDmabuf = false;
}
// `wlBuffer` becomes the window's content: attach, damage, commit.
void Commit(wl_proxy* wlBuffer) {
const Api& api = WaylandApi();
wl_proxy* surface = window->surface;
const uint32_t surfaceVersion = api.getVersion(surface);
api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, wlBuffer, window->dx, window->dy);
if (surfaceVersion >= 4) {
api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, width, height);
} else {
api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, width, height);
}
api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0);
window->attached_width = width;
window->attached_height = height;
window->dx = 0;
window->dy = 0;
api.flush(display);
}
};
namespace {
void OnRegistryGlobal(void* data, wl_proxy* registry, uint32_t name, const char* interface, uint32_t) {
void OnRegistryGlobal(void* data, wl_proxy* registry, uint32_t name, const char* interface,
uint32_t version) {
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);
if (impl->shm == nullptr && std::strcmp(interface, "wl_shm") == 0) {
impl->shm =
api.marshalFlags(registry, kRegistryBind, api.shmInterface, 1, 0, name, "wl_shm", 1u, nullptr);
return;
}
// create_immed is version 2's: an older linux-dmabuf is left unbound.
if (impl->dmabufWanted && impl->dmabuf == nullptr && version >= 2 &&
std::strcmp(interface, "zwp_linux_dmabuf_v1") == 0) {
const LinuxDmabufProtocol* protocol = LinuxDmabuf();
if (protocol == nullptr) return;
const uint32_t bindVersion = std::min(version, kDmabufMaxVersion);
impl->dmabuf = api.marshalFlags(registry, kRegistryBind, &protocol->dmabuf, bindVersion, 0, name,
"zwp_linux_dmabuf_v1", bindVersion, nullptr);
if (impl->dmabuf != nullptr) {
api.addListener(impl->dmabuf, const_cast<void (**)(void)>(kDmabufListener), impl);
}
}
}
void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {}
void (*const kRegistryListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnRegistryGlobal),
@@ -362,6 +616,8 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
impl->width = width;
impl->height = height;
impl->format = hasAlpha ? kShmFormatArgb8888 : kShmFormatXrgb8888;
impl->fourcc = hasAlpha ? kDrmFormatAbgr8888 : kDrmFormatXbgr8888;
impl->dmabufWanted = DmabufPresentationAllowed() && SharedImagesAvailable();
// 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);
@@ -377,10 +633,23 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
MGLOG_E("Wayland: the compositor offers no wl_shm; a window surface cannot be presented");
return nullptr;
}
// The first shared image is made here, so a server or compositor that cannot do the
// dma-buf path is found out before the window shows anything, and the window starts on
// wl_shm instead.
if (impl->dmabuf != nullptr) impl->useDmabuf = impl->Allocate(impl->dmabufBuffers[0]);
if (!impl->useDmabuf && impl->dmabuf != nullptr) {
api.marshalFlags(impl->dmabuf, kDmabufDestroy, nullptr, api.getVersion(impl->dmabuf), kMarshalFlagDestroy);
impl->dmabuf = 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");
if (impl->useDmabuf) {
MGLOG_I("Wayland: window surface %dx%d presented through linux-dmabuf shared images (%s)", width, height,
hasAlpha ? "ABGR8888" : "XBGR8888");
} else {
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)));
}
@@ -398,7 +667,23 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
Bool WindowSurface::Present() {
Impl& impl = *m_impl;
if (impl.window == nullptr) return false;
const Api& api = WaylandApi();
if (impl.useDmabuf) {
// The server copies the frame into the image GPU-side, top row first - a wl_buffer's
// first row - and answers once the copy has completed, so the compositor reads a
// finished frame.
DmabufBuffer* buffer = impl.NextFreeDmabuf();
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (buffer != nullptr && backendObject != nullptr && backendObject->PresentToSharedImage(buffer->imageId)) {
impl.Commit(buffer->buffer);
buffer->busy = true;
return true;
}
MGLOG_E_ONCE("Wayland: a frame could not be presented through a linux-dmabuf shared image; the window "
"falls back to wl_shm");
impl.FallBackToShm();
}
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");
@@ -408,24 +693,8 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A.
ReadBackFrameBGRA(impl.width, impl.height, impl.scratch, static_cast<Uint8*>(buffer->map),
static_cast<SizeT>(buffer->stride));
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);
impl.Commit(buffer->buffer);
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
+16 -9
View File
@@ -12,7 +12,13 @@
// 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.
// swap the frame is put into a wl_buffer and attached to the wl_surface.
//
// Two kinds of wl_buffer. With a backend that has shared images (a split client) and a compositor
// offering zwp_linux_dmabuf_v1, each buffer is a server-allocated image exported as a dma-buf and
// the server copies the frame into it GPU-side: nothing crosses the CPU. Otherwise - no such
// global, no shared images, a refused import, MOBILEGL_WAYLAND_DMABUF=0 - the frame is read back
// into 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.
@@ -54,9 +60,9 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
// 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.
// One window surface's presentation: the dma-buf or 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,
@@ -65,15 +71,16 @@ namespace MobileGL::MG_Impl::EGLImpl::Wayland {
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.
// Puts the current context's default-framebuffer frame on the window: copied into a
// shared image (or read back into a wl_shm buffer, the application's read-framebuffer and
// pack state put back exactly as they were), then attach, damage, commit. A shared-image
// present that fails moves the window to wl_shm for good. False (logged) when the frame
// did not reach the window; the swap itself goes on regardless.
Bool Present();
// wl_egl_window_resize's effect, taken: true (with the new size) when the application gave
// its wl_egl_window a size this presentation is not at yet. The presentation adopts it -
// its next wl_shm buffer is that size - and the caller resizes the drawable behind it.
// its next buffer is that size - and the caller resizes the drawable behind it.
Bool TakeResize(EGLint* width, EGLint* height);
struct Impl;
@@ -192,6 +192,7 @@ DECLARE_GL_FUNCTION_HEAD(void, StencilMaskSeparate, GLenum face, GLuint mask) DE
DECLARE_GL_FUNCTION_HEAD(void, StencilOp, GLenum fail, GLenum zfail, GLenum zpass) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilOp, fail, zfail, zpass)
DECLARE_GL_FUNCTION_HEAD(void, StencilOpSeparate, GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass) DECLARE_GL_FUNCTION_END_NO_RETURN(void, StencilOpSeparate, face, sfail, dpfail, dppass)
DECLARE_GL_FUNCTION_HEAD(void, TexImage2D, GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void* pixels) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexImage2D, target, level, internalformat, width, height, border, format, type, pixels)
DECLARE_GL_FUNCTION_HEAD(void, EGLImageTargetTexture2DOES, GLenum target, GLeglImageOES image) DECLARE_GL_FUNCTION_END_NO_RETURN(void, EGLImageTargetTexture2DOES, target, image)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterf, GLenum target, GLenum pname, GLfloat param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterf, target, pname, param)
DECLARE_GL_FUNCTION_HEAD(void, TexParameterfv, GLenum target, GLenum pname, const GLfloat* params) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameterfv, target, pname, params)
DECLARE_GL_FUNCTION_HEAD(void, TexParameteri, GLenum target, GLenum pname, GLint param) DECLARE_GL_FUNCTION_END_NO_RETURN(void, TexParameteri, target, pname, param)
+19 -1
View File
@@ -38,6 +38,7 @@
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
@@ -590,6 +591,15 @@ namespace MobileGL::MG_Impl::GLImpl {
return maxSamples;
}
namespace {
// GL_OES_EGL_image binds the EGL side's dma-buf images, which only a backend with shared
// images has (EGLImpl::SharedImagesAvailable). The backend's own list - the server's,
// under split - cannot know that, so the extension is appended after it.
const char* ClientSideExtension() {
return MG_Impl::EGLImpl::SharedImagesAvailable() ? "GL_OES_EGL_image" : nullptr;
}
} // namespace
/* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */
const GLubyte* GetString(GLenum name) {
static String vendorString;
@@ -654,6 +664,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
extensionsString += MG_Util::ConvertGLExtToString(ext);
}
if (const char* extra = ClientSideExtension()) {
if (!extensionsString.empty()) extensionsString += " ";
extensionsString += extra;
}
return (const GLubyte*)extensionsString.c_str();
default:
return (const GLubyte*)"Unknown Enum";
@@ -674,6 +688,9 @@ namespace MobileGL::MG_Impl::GLImpl {
const auto& rendererInfo = activeBackendObject->GetRendererInfo();
const auto& exts = rendererInfo.RendererGLInfo.Extensions;
if (index == exts.size()) {
if (const char* extra = ClientSideExtension()) return (const GLubyte*)extra;
}
if (index >= exts.size()) {
return nullptr;
}
@@ -2795,7 +2812,8 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = rendererInfo.RendererGLInfo.TargetGLVersion.Minor;
break;
case GL_NUM_EXTENSIONS:
*params = static_cast<Int>(rendererInfo.RendererGLInfo.Extensions.size());
*params = static_cast<Int>(rendererInfo.RendererGLInfo.Extensions.size()) +
(ClientSideExtension() != nullptr ? 1 : 0);
break;
case GL_POINT_SIZE_GRANULARITY:
*params = static_cast<GLint>(dynamicParameters.PointSizeGranularity);
@@ -32,6 +32,8 @@
#include <MG_Util/Math/FixedPointConversion.h>
#include <MG_State/GLState/TextureState/TextureObjectBuffer.h>
#include <MG_Impl/Pipe/PipeFill.h>
#include <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
#include <MG_Impl/EGLImpl/EGLImpl.h>
// CONTRACT-P5.md §7 / ID-14: a null check on a GLFunctionsTable slot may not survive into the
// client under split - it becomes a caps-mirror read. SlotCaps.h carries the rule and the test
// that decides which of its two spellings a site takes; in a pull build both expand to exactly
@@ -7133,6 +7135,52 @@ namespace MobileGL::MG_Impl::GLImpl {
TexImage2D_State(target, level, internalformat, width, height, border, format, type, pixels);
}
// GL_OES_EGL_image. The only EGLImages with storage behind them are the server's shared images
// (EGL_LINUX_DMA_BUF_EXT): level 0 of the bound texture is defined at the image's size through
// glTexImage2D's own path - so the respecification is recorded and announced as for any other
// - and the backend then swaps that storage for the image. The frontend has no external-image
// target, so GL_TEXTURE_2D is the only one taken.
void EGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image) {
if (target != GL_TEXTURE_2D) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "target must be GL_TEXTURE_2D."));
return;
}
Uint64 sharedImageId = 0;
EGLint width = 0;
EGLint height = 0;
if (!MG_Impl::EGLImpl::LookupSharedImage(static_cast<EGLImage>(image), &sharedImageId, &width, &height)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidValue,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "image is not a valid EGLImage."));
return;
}
auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
const SharedPtr<MG_State::GLState::ITextureObject> textureObject =
activeUnit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (!ValidateTextureMutable(textureObject, __func__)) return;
if (!TextureImpl::ValidateTextureSizeRange(width, height, 1)) return;
// NULL is an offset into a bound unpack buffer; the definition must read nothing.
GLint unpackBuffer = 0;
GetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &unpackBuffer);
if (unpackBuffer != 0) BindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
TexImage2D_State(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
if (unpackBuffer != 0) BindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(unpackBuffer));
auto* backendObject = MG_Backend::pActiveBackendObject.get();
if (backendObject == nullptr ||
!backendObject->AttachSharedImageToTexture(textureObject->GetLifetimeId(), sharedImageId)) {
MGLOG_E_ONCE("glEGLImageTargetTexture2DOES: shared image %llu could not be bound to texture %u",
static_cast<unsigned long long>(sharedImageId), textureObject->GetExternalIndex());
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__, "the image could not be bound."));
}
}
void TexImage1D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLint border, GLenum format,
GLenum type, const GLvoid* pixels) {
TexImage1D_State(target, level, internalFormat, width, border, format, type, pixels);
@@ -111,6 +111,7 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint border, GLenum format, GLenum type, const void* pixels);
void TexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
GLenum format, GLenum type, const void* pixels);
void EGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image);
void TexImage1D(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLint border, GLenum format,
GLenum type, const GLvoid* pixels);
void TexBuffer(GLenum target, GLenum internalformat, GLuint buffer);
+9
View File
@@ -18,6 +18,12 @@ extern "C" EGLDisplay eglGetPlatformDisplayEXT(EGLenum platform, void* native_di
extern "C" EGLImage eglCreateImageKHR(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer,
const EGLint* attrib_list);
extern "C" EGLBoolean eglDestroyImageKHR(EGLDisplay dpy, EGLImage image);
extern "C" EGLBoolean eglQueryDmaBufFormatsEXT(EGLDisplay dpy, EGLint max_formats, EGLint* formats,
EGLint* num_formats);
extern "C" EGLBoolean eglQueryDmaBufModifiersEXT(EGLDisplay dpy, EGLint format, EGLint max_modifiers,
khronos_uint64_t* modifiers, EGLBoolean* external_only,
EGLint* num_modifiers);
extern "C" void glEGLImageTargetTexture2DOES(GLenum target, GLeglImageOES image);
#define GETPROC(name, var) \
if (strcmp(#name, var) == 0) { \
@@ -69,6 +75,8 @@ namespace MobileGL::MG_Impl {
GETPROC(eglDestroyImage, name);
GETPROC(eglCreateImageKHR, name);
GETPROC(eglDestroyImageKHR, name);
GETPROC(eglQueryDmaBufFormatsEXT, name);
GETPROC(eglQueryDmaBufModifiersEXT, name);
GETPROC(eglGetPlatformDisplay, name);
GETPROC(eglGetPlatformDisplayEXT, name);
GETPROC(eglCreatePlatformWindowSurface, name);
@@ -116,6 +124,7 @@ namespace MobileGL::MG_Impl {
GETPROC(glTexParameteriv, name);
GETPROC(glTexImage1D, name);
GETPROC(glTexImage2D, name);
GETPROC(glEGLImageTargetTexture2DOES, name);
GETPROC(glDrawBuffer, name);
GETPROC(glClear, name);
GETPROC(glClearColor, name);
+107
View File
@@ -0,0 +1,107 @@
// MobileGL - MobileGL/MG_Impl/SharedImageApi.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
// The shared-image C ABI (SharedImageApi.h): a thin forward onto the active backend object's
// client-side shared-image calls (BackendObject_Remote under split). Linux-only, split-only: the
// CMake block that lists this file says so, and nothing in the library calls it.
#include "SharedImageApi.h"
#include <Defines.h>
#include <Init.h>
#include <MG_Backend/BackendObjects.h>
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <mutex>
#include <unistd.h>
namespace {
using MobileGL::MG_Backend::BackendObject;
constexpr uint32_t kFormats[] = {MOBILEGL_SHARED_IMAGE_FOURCC_ABGR8888, MOBILEGL_SHARED_IMAGE_FOURCC_XBGR8888};
bool FourccSupported(uint32_t fourcc) {
for (uint32_t format : kFormats) {
if (format == fourcc) return true;
}
return false;
}
// Serializes the calls against each other (not against GL calls: see the header).
std::mutex g_apiMutex;
// Allocate and import bring the session up, exactly as the process's first EGL entry point
// would: the GBM device is created before any EGL display (a compositor's DRM device is what
// its GBM display is made from), so failing here instead would only push the bring-up to a
// caller that has no reason to know it is needed. Release never does - an image can only be
// held through a session that already exists.
BackendObject* ActiveBackend(bool bringUp) {
if (bringUp) MobileGL::EnsureInitialized();
return MobileGL::MG_Backend::pActiveBackendObject.get();
}
} // namespace
MOBILEGL_EXPORT uint32_t mobilegl_shared_image_abi_version(void) {
return MOBILEGL_SHARED_IMAGE_ABI_VERSION;
}
MOBILEGL_EXPORT int mobilegl_shared_image_formats(uint32_t* formats, uint32_t capacity) {
constexpr uint32_t total = sizeof(kFormats) / sizeof(kFormats[0]);
if (formats == nullptr && capacity != 0) return -EINVAL;
for (uint32_t i = 0; i < total && i < capacity; ++i) formats[i] = kFormats[i];
return static_cast<int>(total);
}
MOBILEGL_EXPORT int mobilegl_shared_image_allocate(uint32_t width, uint32_t height, uint32_t fourcc,
struct mobilegl_shared_image* out) {
if (out == nullptr || out->struct_size < sizeof(uint32_t)) return -EINVAL;
if (width == 0 || height == 0 || !FourccSupported(fourcc)) return -EINVAL;
const std::lock_guard<std::mutex> lock(g_apiMutex);
BackendObject* backend = ActiveBackend(true);
if (backend == nullptr) return -EIO;
MobileGL::MG_Backend::SharedImageExport image{};
if (!backend->AllocateSharedImage(width, height, fourcc, &image) || image.Fd < 0) {
if (image.Fd >= 0) ::close(image.Fd);
return -EIO;
}
mobilegl_shared_image answer{};
answer.struct_size = out->struct_size;
answer.width = image.Width;
answer.height = image.Height;
answer.fourcc = image.Fourcc;
answer.id = image.Id;
answer.fd = image.Fd;
answer.stride = image.Stride;
answer.offset = image.Offset;
answer.modifier = image.Modifier;
// An older caller's struct is a prefix of this one; a newer caller's tail stays as it was.
std::memcpy(out, &answer, std::min<size_t>(out->struct_size, sizeof(answer)));
return 0;
}
MOBILEGL_EXPORT int mobilegl_shared_image_import(int fd, uint32_t width, uint32_t height, uint32_t fourcc,
uint64_t* out_id) {
if (fd < 0 || out_id == nullptr || width == 0 || height == 0 || !FourccSupported(fourcc)) return -EINVAL;
const std::lock_guard<std::mutex> lock(g_apiMutex);
BackendObject* backend = ActiveBackend(true);
if (backend == nullptr) return -EIO;
uint64_t id = 0;
if (!backend->ImportSharedImage(fd, width, height, fourcc, &id)) return -EIO;
*out_id = id;
return 0;
}
MOBILEGL_EXPORT int mobilegl_shared_image_release(uint64_t id) {
const std::lock_guard<std::mutex> lock(g_apiMutex);
BackendObject* backend = ActiveBackend(false);
if (backend == nullptr) return -ENODEV;
return backend->ReleaseSharedImage(id) ? 0 : -EIO;
}
+90
View File
@@ -0,0 +1,90 @@
/* MobileGL - MobileGL/MG_Impl/SharedImageApi.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 */
/* SHARED IMAGES, AS A C ABI (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
*
* libMobileGL.so exports these on Linux (not Android) in split builds, so a component that is not
* a GL entry point - the GBM backend (MG_Gbm), above all - can ask the server for an image and
* name one it was handed, through the process's own MobileGL client session. The component
* resolves them with dlsym; nothing links against them.
*
* Images are server-allocated colour buffers exported as dma-buf descriptors. Their memory layout
* is the allocator's: the modifier reported is DRM_FORMAT_MOD_INVALID, and the only meaningful
* thing to do with a descriptor is to hand it back (an import, here or through EGL) - not to map
* it and assume rows.
*
* Errors are negative errno values:
* -EINVAL a bad argument or an unsupported fourcc
* -ENODEV no MobileGL client session (release only; the others bring one up)
* -EIO the session refused or the server declined (its log says why)
*
* THREADS. The calls are serialized against each other, and each one is a round trip on the
* process's client session - the same ring its GL calls ride - so like any GL call they must not
* race the GL calls of another thread. */
#ifndef MOBILEGL_SHARED_IMAGE_API_H
#define MOBILEGL_SHARED_IMAGE_API_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define MOBILEGL_SHARED_IMAGE_ABI_VERSION 1u
/* DRM fourccs (little-endian packed). ABGR8888 is R, G, B, A in memory - GL's RGBA8. */
#define MOBILEGL_SHARED_IMAGE_FOURCC_ABGR8888 0x34324241u /* 'AB24' */
#define MOBILEGL_SHARED_IMAGE_FOURCC_XBGR8888 0x34324258u /* 'XB24' */
/* One allocated image. VERSIONED BY SIZE: the caller sets struct_size to the sizeof it was built
* against and the library writes no byte past it, so fields are only ever appended. */
struct mobilegl_shared_image {
uint32_t struct_size;
uint32_t width;
uint32_t height;
uint32_t fourcc;
uint64_t id; /* the server's name for the image; pass to release */
int32_t fd; /* the exported dma-buf, owned by the caller; -1 on failure */
uint32_t stride; /* plane 0, bytes */
uint32_t offset; /* plane 0, bytes */
uint32_t reserved; /* zero */
uint64_t modifier; /* DRM format modifier of the layout (DRM_FORMAT_MOD_INVALID today) */
};
/* MOBILEGL_SHARED_IMAGE_ABI_VERSION of the library. */
uint32_t mobilegl_shared_image_abi_version(void);
/* The fourccs images can be allocated in. Writes min(capacity, total) entries and returns the
* total (formats may be null when capacity is 0). */
int mobilegl_shared_image_formats(uint32_t* formats, uint32_t capacity);
/* A new width x height image. Brings the process's MobileGL client session up if no EGL/GLX entry
* point has yet (exactly as the first eglGetDisplay would). */
int mobilegl_shared_image_allocate(uint32_t width, uint32_t height, uint32_t fourcc,
struct mobilegl_shared_image* out);
/* Names the image `fd` was exported from (the caller keeps `fd`), taking a reference to it.
* -EIO: not one of the server's images, or the size/format disagree. */
int mobilegl_shared_image_import(int fd, uint32_t width, uint32_t height, uint32_t fourcc, uint64_t* out_id);
/* Drops this process's reference (an allocation's or an import's). */
int mobilegl_shared_image_release(uint64_t id);
/* dlsym-friendly signatures. */
typedef uint32_t (*PFN_mobilegl_shared_image_abi_version)(void);
typedef int (*PFN_mobilegl_shared_image_formats)(uint32_t*, uint32_t);
typedef int (*PFN_mobilegl_shared_image_allocate)(uint32_t, uint32_t, uint32_t, struct mobilegl_shared_image*);
typedef int (*PFN_mobilegl_shared_image_import)(int, uint32_t, uint32_t, uint32_t, uint64_t*);
typedef int (*PFN_mobilegl_shared_image_release)(uint64_t);
#ifdef __cplusplus
}
#endif
#endif /* MOBILEGL_SHARED_IMAGE_API_H */
+2 -2
View File
@@ -79,8 +79,8 @@ namespace MobileGL::MG_Remote::Server {
const MobileGLResult received = m_transport->ReceiveFd(
&fd, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, &sidebandSize, left);
if (received == MOBILEGL_ERR_TIMEOUT) {
why = "no descriptor arrived on the aux socket within " + std::to_string(timeoutMs) +
" ms of its record";
why = std::string(kind == Kind::T0 ? "no store" : "no shared-image descriptor") +
" arrived on the aux socket within " + std::to_string(timeoutMs) + " ms of its record";
return Outcome::TimedOut;
}
if (received != MOBILEGL_OK) {
+217 -1
View File
@@ -297,6 +297,7 @@ namespace MobileGL {
for (auto imageIt = m_images.begin(); imageIt != m_images.end();) {
if (imageIt->second.Display == display) {
if (imageIt->second.Shared.Id != 0) m_orphanedSharedImages.push_back(imageIt->second.Shared.Id);
imageIt = m_images.erase(imageIt);
} else {
++imageIt;
@@ -1558,8 +1559,10 @@ namespace MobileGL {
return image;
}
Bool EGLContext::DestroyImage(EGLDisplayHandle display, EGLImageHandle image) {
Bool EGLContext::DestroyImage(EGLDisplayHandle display, EGLImageHandle image,
Uint64* releasedSharedImageId) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (releasedSharedImageId != nullptr) *releasedSharedImageId = 0;
auto imageIt = m_images.find(image);
if (imageIt == m_images.end()) {
SetError(EGL_BAD_PARAMETER);
@@ -1569,9 +1572,222 @@ namespace MobileGL {
SetError(EGL_BAD_MATCH);
return false;
}
if (releasedSharedImageId != nullptr) *releasedSharedImageId = imageIt->second.Shared.Id;
m_images.erase(imageIt);
return true;
}
Bool EGLContext::IsDmaBufFourccSupported(Uint32 fourcc) {
// Only the GL RGBA8 byte order: an ARGB/XRGB-ordered import would need the
// backend to swizzle the image it binds, which nothing records yet.
return fourcc == kDrmFourccAbgr8888 || fourcc == kDrmFourccXbgr8888;
}
Bool EGLContext::PrepareDmaBufImport(EGLDisplayHandle display, EGLContextHandle context,
EGLClientBuffer buffer, const EGLAttrib* attribList,
DmaBufImportAttribs* out) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!ValidateDisplay(display)) {
SetError(EGL_BAD_DISPLAY);
return false;
}
if (!IsDisplayInitialized(display)) {
SetError(EGL_NOT_INITIALIZED);
return false;
}
// EGL_EXT_image_dma_buf_import: the context must be EGL_NO_CONTEXT and the buffer
// NULL - everything is in the attribute list.
if (context != nullptr || buffer != nullptr) {
SetError(EGL_BAD_PARAMETER);
return false;
}
DmaBufImportAttribs attribs;
Bool hasWidth = false, hasHeight = false, hasFourcc = false, hasFd = false, hasOffset = false,
hasPitch = false, hasModifierLo = false, hasModifierHi = false, otherPlane = false;
Uint64 modifierLo = 0, modifierHi = 0;
for (SizeT i = 0; attribList != nullptr && attribList[i] != EGL_NONE; i += 2) {
const EGLAttrib value = attribList[i + 1];
switch (attribList[i]) {
case EGL_WIDTH:
attribs.Width = static_cast<EGLint>(value);
hasWidth = true;
break;
case EGL_HEIGHT:
attribs.Height = static_cast<EGLint>(value);
hasHeight = true;
break;
case EGL_LINUX_DRM_FOURCC_EXT:
attribs.Fourcc = static_cast<Uint32>(value);
hasFourcc = true;
break;
case EGL_DMA_BUF_PLANE0_FD_EXT:
attribs.Fd = static_cast<int>(value);
hasFd = true;
break;
case EGL_DMA_BUF_PLANE0_OFFSET_EXT:
attribs.Offset = static_cast<EGLint>(value);
hasOffset = true;
break;
case EGL_DMA_BUF_PLANE0_PITCH_EXT:
attribs.Pitch = static_cast<EGLint>(value);
hasPitch = true;
break;
case EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT:
modifierLo = static_cast<Uint32>(value);
hasModifierLo = true;
break;
case EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT:
modifierHi = static_cast<Uint32>(value);
hasModifierHi = true;
break;
case EGL_DMA_BUF_PLANE1_FD_EXT:
case EGL_DMA_BUF_PLANE1_OFFSET_EXT:
case EGL_DMA_BUF_PLANE1_PITCH_EXT:
case EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT:
case EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT:
case EGL_DMA_BUF_PLANE2_FD_EXT:
case EGL_DMA_BUF_PLANE2_OFFSET_EXT:
case EGL_DMA_BUF_PLANE2_PITCH_EXT:
case EGL_DMA_BUF_PLANE2_MODIFIER_LO_EXT:
case EGL_DMA_BUF_PLANE2_MODIFIER_HI_EXT:
case EGL_DMA_BUF_PLANE3_FD_EXT:
case EGL_DMA_BUF_PLANE3_OFFSET_EXT:
case EGL_DMA_BUF_PLANE3_PITCH_EXT:
case EGL_DMA_BUF_PLANE3_MODIFIER_LO_EXT:
case EGL_DMA_BUF_PLANE3_MODIFIER_HI_EXT:
otherPlane = true;
break;
// Hints for YUV formats, and EGL_KHR_image_base's own attribute: meaningless
// for a single-plane RGB image, accepted and ignored.
case EGL_YUV_COLOR_SPACE_HINT_EXT:
case EGL_SAMPLE_RANGE_HINT_EXT:
case EGL_YUV_CHROMA_HORIZONTAL_SITING_HINT_EXT:
case EGL_YUV_CHROMA_VERTICAL_SITING_HINT_EXT:
case EGL_IMAGE_PRESERVED_KHR:
break;
default:
SetError(EGL_BAD_PARAMETER);
return false;
}
}
if (!hasWidth || !hasHeight || !hasFourcc || !hasFd || !hasOffset || !hasPitch ||
hasModifierLo != hasModifierHi) {
SetError(EGL_BAD_PARAMETER);
return false;
}
if (!IsDmaBufFourccSupported(attribs.Fourcc)) {
SetError(EGL_BAD_MATCH);
return false;
}
// Every supported format has exactly one plane.
if (otherPlane) {
SetError(EGL_BAD_ATTRIBUTE);
return false;
}
if (attribs.Width <= 0 || attribs.Height <= 0 || attribs.Fd < 0) {
SetError(EGL_BAD_PARAMETER);
return false;
}
if (attribs.Offset < 0 || attribs.Pitch <= 0) {
SetError(EGL_BAD_ACCESS);
return false;
}
// Any modifier is taken: the image is identified by its descriptor, never read
// through the layout a modifier describes.
attribs.HasModifier = hasModifierLo;
attribs.Modifier = (modifierHi << 32) | modifierLo;
if (out != nullptr) *out = attribs;
return true;
}
EGLContext::EGLImageHandle EGLContext::CreateSharedImage(EGLDisplayHandle display,
const SharedImageInfo& info) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!IsDisplayInitialized(display)) {
SetError(EGL_NOT_INITIALIZED);
return EGL_NO_IMAGE;
}
if (info.Id == 0) {
SetError(EGL_BAD_MATCH);
return EGL_NO_IMAGE;
}
const auto image = EncodeHandle<EGLImageHandle>(m_nextImageHandle++);
m_images[image] = ImageObject{
.Display = display,
.Context = nullptr,
.Target = EGL_LINUX_DMA_BUF_EXT,
.Buffer = nullptr,
.Shared = info,
};
return image;
}
Bool EGLContext::GetSharedImage(EGLImageHandle image, SharedImageInfo* out) const {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
const auto* imageObject = TryGetImage(image);
if (imageObject == nullptr || imageObject->Shared.Id == 0) return false;
if (out != nullptr) *out = imageObject->Shared;
return true;
}
Vector<Uint64> EGLContext::TakeOrphanedSharedImages() {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
Vector<Uint64> orphaned;
orphaned.swap(m_orphanedSharedImages);
return orphaned;
}
Bool EGLContext::QueryDmaBufFormats(EGLDisplayHandle display, Bool available, EGLint maxFormats,
EGLint* formats, EGLint* numFormats) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
if (!ValidateDisplay(display)) {
SetError(EGL_BAD_DISPLAY);
return false;
}
if (!IsDisplayInitialized(display)) {
SetError(EGL_NOT_INITIALIZED);
return false;
}
if (maxFormats < 0 || numFormats == nullptr || (maxFormats > 0 && formats == nullptr)) {
SetError(EGL_BAD_PARAMETER);
return false;
}
static constexpr Uint32 kFormats[] = {kDrmFourccAbgr8888, kDrmFourccXbgr8888};
const EGLint total = available ? static_cast<EGLint>(std::size(kFormats)) : 0;
if (maxFormats == 0) {
*numFormats = total;
return true;
}
const EGLint count = std::min(maxFormats, total);
for (EGLint i = 0; i < count; ++i) formats[i] = static_cast<EGLint>(kFormats[i]);
*numFormats = count;
return true;
}
Bool EGLContext::QueryDmaBufModifiers(EGLDisplayHandle display, Bool available, EGLint format,
EGLint maxModifiers, Uint64* modifiers,
EGLBoolean* externalOnly, EGLint* numModifiers) {
const std::lock_guard<std::recursive_mutex> lock(m_mutex);
(void)externalOnly;
if (!ValidateDisplay(display)) {
SetError(EGL_BAD_DISPLAY);
return false;
}
if (!IsDisplayInitialized(display)) {
SetError(EGL_NOT_INITIALIZED);
return false;
}
if (!available || !IsDmaBufFourccSupported(static_cast<Uint32>(format)) || maxModifiers < 0 ||
numModifiers == nullptr || (maxModifiers > 0 && modifiers == nullptr)) {
SetError(EGL_BAD_PARAMETER);
return false;
}
// No explicit modifiers: an image's layout is its allocator's, which is what an
// import without modifier attributes (the implicit modifier) means.
*numModifiers = 0;
return true;
}
} // namespace EGLState
// Leak-at-exit storage; see GlobalObjects.cpp.
+51 -1
View File
@@ -142,7 +142,54 @@ namespace MobileGL {
// Image
EGLImageHandle CreateImage(EGLDisplayHandle display, EGLContextHandle context, EGLenum target,
EGLClientBuffer buffer, const EGLAttrib* attribList);
Bool DestroyImage(EGLDisplayHandle display, EGLImageHandle image);
// `releasedSharedImageId` (optional) receives the shared image the destroyed image
// named, 0 for any other image: the caller drops the backend's reference to it.
Bool DestroyImage(EGLDisplayHandle display, EGLImageHandle image,
Uint64* releasedSharedImageId = nullptr);
// DMA-BUF IMPORT (EGL_EXT_image_dma_buf_import[_modifiers]). A dma-buf this library
// can name is one of the backend's shared images (MG_Backend::SharedImageExport):
// single plane, DRM ABGR8888 or XBGR8888 - the GL RGBA8 byte order. The state
// validates and records; the backend identifies the descriptor in between.
struct DmaBufImportAttribs {
EGLint Width = 0;
EGLint Height = 0;
Uint32 Fourcc = 0;
int Fd = -1;
EGLint Offset = 0;
EGLint Pitch = 0;
Bool HasModifier = false;
Uint64 Modifier = 0;
};
struct SharedImageInfo {
Uint64 Id = 0;
EGLint Width = 0;
EGLint Height = 0;
Uint32 Fourcc = 0;
};
static constexpr Uint32 kDrmFourccAbgr8888 = 0x34324241u; // 'AB24'
static constexpr Uint32 kDrmFourccXbgr8888 = 0x34324258u; // 'XB24'
static Bool IsDmaBufFourccSupported(Uint32 fourcc);
// The checks eglCreateImage owes an EGL_LINUX_DMA_BUF_EXT target before anything
// is imported: the display, a null context and buffer, and a complete single-plane
// attribute list of a supported format. False with the EGL error set.
Bool PrepareDmaBufImport(EGLDisplayHandle display, EGLContextHandle context, EGLClientBuffer buffer,
const EGLAttrib* attribList, DmaBufImportAttribs* out);
// An EGLImage naming an imported shared image.
EGLImageHandle CreateSharedImage(EGLDisplayHandle display, const SharedImageInfo& info);
// The shared image `image` names; false for anything else (an invalid image, or one
// of another target).
Bool GetSharedImage(EGLImageHandle image, SharedImageInfo* out) const;
// Shared images whose EGLImages went with an eglTerminate: the caller releases them.
Vector<Uint64> TakeOrphanedSharedImages();
// eglQueryDmaBufFormatsEXT / eglQueryDmaBufModifiersEXT. `available` is whether the
// backend has shared images at all; without them the lists are empty.
Bool QueryDmaBufFormats(EGLDisplayHandle display, Bool available, EGLint maxFormats, EGLint* formats,
EGLint* numFormats);
Bool QueryDmaBufModifiers(EGLDisplayHandle display, Bool available, EGLint format,
EGLint maxModifiers, Uint64* modifiers, EGLBoolean* externalOnly,
EGLint* numModifiers);
private:
enum class SurfaceType {
@@ -245,6 +292,8 @@ namespace MobileGL {
EGLContextHandle Context = nullptr;
EGLenum Target = EGL_NONE;
EGLClientBuffer Buffer = nullptr;
// EGL_LINUX_DMA_BUF_EXT only: the backend's shared image (Id != 0).
SharedImageInfo Shared;
};
struct ThreadCurrentState {
@@ -310,6 +359,7 @@ namespace MobileGL {
UnorderedMap<EGLContextHandle, ContextObject> m_contexts;
UnorderedMap<EGLSyncHandle, SyncObject> m_syncs;
UnorderedMap<EGLImageHandle, ImageObject> m_images;
Vector<Uint64> m_orphanedSharedImages;
UnorderedMap<std::thread::id, EGLint> m_threadErrors;
UnorderedMap<std::thread::id, EGLenum> m_threadBoundAPI;
+163
View File
@@ -1,8 +1,10 @@
#include <gtest/gtest.h>
#include <MG_State/EGLState/Core.h>
#include <EGL/eglext.h>
#include <future>
#include <memory>
#include <thread>
#include <vector>
namespace {
using StateContext = MobileGL::MG_State::EGLState::EGLContext;
@@ -132,3 +134,164 @@ TEST(EGLStateProfile, CompatibilityProfileRequiresExplicitCompatBit) {
EXPECT_FALSE(fixture->State.IsCurrentContextOpenGLCompatibilityProfile());
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
}
namespace {
constexpr EGLAttrib kAbgr8888 = StateContext::kDrmFourccAbgr8888;
constexpr EGLAttrib kXbgr8888 = StateContext::kDrmFourccXbgr8888;
constexpr EGLAttrib kArgb8888 = 0x34325241; // 'AR24': not a shared-image byte order
// A complete single-plane import, as a compositor builds it for a linux-dmabuf wl_buffer.
std::vector<EGLAttrib> DmaBufAttribs(EGLAttrib fourcc = kAbgr8888) {
return {EGL_WIDTH, 64, EGL_HEIGHT, 32, EGL_LINUX_DRM_FOURCC_EXT, fourcc, EGL_DMA_BUF_PLANE0_FD_EXT, 7,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0, EGL_DMA_BUF_PLANE0_PITCH_EXT, 256, EGL_NONE};
}
std::vector<EGLAttrib> WithAttrib(std::vector<EGLAttrib> attribs, EGLAttrib name, EGLAttrib value) {
attribs.insert(attribs.end() - 1, {name, value});
return attribs;
}
std::vector<EGLAttrib> WithoutAttrib(const std::vector<EGLAttrib>& attribs, EGLAttrib name) {
std::vector<EGLAttrib> out;
for (size_t i = 0; attribs[i] != EGL_NONE; i += 2) {
if (attribs[i] != name) out.insert(out.end(), {attribs[i], attribs[i + 1]});
}
out.push_back(EGL_NONE);
return out;
}
EGLint PrepareError(EGLFixture& fixture, const std::vector<EGLAttrib>& attribs,
StateContext::EGLContextHandle context = EGL_NO_CONTEXT, EGLClientBuffer buffer = nullptr) {
StateContext::DmaBufImportAttribs parsed;
const bool ok = fixture.State.PrepareDmaBufImport(fixture.Display, context, buffer, attribs.data(), &parsed);
const EGLint error = fixture.State.ConsumeError();
EXPECT_EQ(ok, error == EGL_SUCCESS);
return error;
}
} // namespace
TEST(EGLStateDmaBufImport, ParsesACompleteSinglePlaneImport) {
auto fixture = CreateFixture();
const auto attribs = WithAttrib(WithAttrib(DmaBufAttribs(kXbgr8888), EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT,
static_cast<EGLAttrib>(0xffffffffu)),
EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0x00ffffff);
StateContext::DmaBufImportAttribs parsed;
ASSERT_TRUE(fixture->State.PrepareDmaBufImport(fixture->Display, EGL_NO_CONTEXT, nullptr, attribs.data(), &parsed));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_SUCCESS);
EXPECT_EQ(parsed.Width, 64);
EXPECT_EQ(parsed.Height, 32);
EXPECT_EQ(parsed.Fourcc, static_cast<uint32_t>(kXbgr8888));
EXPECT_EQ(parsed.Fd, 7);
EXPECT_EQ(parsed.Offset, 0);
EXPECT_EQ(parsed.Pitch, 256);
EXPECT_TRUE(parsed.HasModifier);
EXPECT_EQ(parsed.Modifier, 0x00ffffffffffffffull);
// The hints that only mean something for YUV are taken and ignored.
EXPECT_EQ(PrepareError(*fixture, WithAttrib(DmaBufAttribs(), EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_FULL_RANGE_EXT)),
EGL_SUCCESS);
}
TEST(EGLStateDmaBufImport, RefusesWhatTheExtensionRefuses) {
auto fixture = CreateFixture();
// Incomplete lists.
for (const EGLAttrib required : {EGL_WIDTH, EGL_HEIGHT, EGL_LINUX_DRM_FOURCC_EXT, EGL_DMA_BUF_PLANE0_FD_EXT,
EGL_DMA_BUF_PLANE0_OFFSET_EXT, EGL_DMA_BUF_PLANE0_PITCH_EXT}) {
EXPECT_EQ(PrepareError(*fixture, WithoutAttrib(DmaBufAttribs(), required)), EGL_BAD_PARAMETER)
<< "missing 0x" << std::hex << required;
}
EXPECT_EQ(PrepareError(*fixture, WithAttrib(DmaBufAttribs(), EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0)),
EGL_BAD_PARAMETER);
// A context or a buffer, which this target takes neither of.
EXPECT_EQ(PrepareError(*fixture, DmaBufAttribs(), fixture->Context), EGL_BAD_PARAMETER);
int notABuffer = 0;
EXPECT_EQ(PrepareError(*fixture, DmaBufAttribs(), EGL_NO_CONTEXT, &notABuffer), EGL_BAD_PARAMETER);
// An attribute no dma-buf import has.
EXPECT_EQ(PrepareError(*fixture, WithAttrib(DmaBufAttribs(), EGL_CONFIG_ID, 1)), EGL_BAD_PARAMETER);
// Formats outside the shared images' byte order, and planes a single-plane format lacks.
EXPECT_EQ(PrepareError(*fixture, DmaBufAttribs(kArgb8888)), EGL_BAD_MATCH);
EXPECT_EQ(PrepareError(*fixture, WithAttrib(DmaBufAttribs(), EGL_DMA_BUF_PLANE1_FD_EXT, 8)), EGL_BAD_ATTRIBUTE);
// Values no image can have.
auto zeroWidth = WithoutAttrib(DmaBufAttribs(), EGL_WIDTH);
zeroWidth = WithAttrib(zeroWidth, EGL_WIDTH, 0);
EXPECT_EQ(PrepareError(*fixture, zeroWidth), EGL_BAD_PARAMETER);
auto badFd = WithAttrib(WithoutAttrib(DmaBufAttribs(), EGL_DMA_BUF_PLANE0_FD_EXT), EGL_DMA_BUF_PLANE0_FD_EXT, -1);
EXPECT_EQ(PrepareError(*fixture, badFd), EGL_BAD_PARAMETER);
auto badPitch =
WithAttrib(WithoutAttrib(DmaBufAttribs(), EGL_DMA_BUF_PLANE0_PITCH_EXT), EGL_DMA_BUF_PLANE0_PITCH_EXT, 0);
EXPECT_EQ(PrepareError(*fixture, badPitch), EGL_BAD_ACCESS);
// An uninitialized display.
ASSERT_TRUE(fixture->State.TerminateDisplay(fixture->Display));
EXPECT_EQ(PrepareError(*fixture, DmaBufAttribs()), EGL_NOT_INITIALIZED);
}
TEST(EGLStateDmaBufImport, SharedImageLifetime) {
auto fixture = CreateFixture();
const StateContext::SharedImageInfo info{.Id = 42, .Width = 64, .Height = 32, .Fourcc = StateContext::kDrmFourccAbgr8888};
const EGLImage image = fixture->State.CreateSharedImage(fixture->Display, info);
ASSERT_NE(image, EGL_NO_IMAGE);
StateContext::SharedImageInfo found;
ASSERT_TRUE(fixture->State.GetSharedImage(image, &found));
EXPECT_EQ(found.Id, 42u);
EXPECT_EQ(found.Width, 64);
EXPECT_EQ(found.Height, 32);
// An image of another target names no shared image, and destroying it releases none.
const EGLImage plain =
fixture->State.CreateImage(fixture->Display, EGL_NO_CONTEXT, EGL_GL_TEXTURE_2D, nullptr, nullptr);
ASSERT_NE(plain, EGL_NO_IMAGE);
EXPECT_FALSE(fixture->State.GetSharedImage(plain, nullptr));
uint64_t released = 99;
EXPECT_TRUE(fixture->State.DestroyImage(fixture->Display, plain, &released));
EXPECT_EQ(released, 0u);
EXPECT_TRUE(fixture->State.DestroyImage(fixture->Display, image, &released));
EXPECT_EQ(released, 42u);
EXPECT_FALSE(fixture->State.GetSharedImage(image, nullptr));
EXPECT_FALSE(fixture->State.DestroyImage(fixture->Display, image, &released));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_PARAMETER);
// eglTerminate drops the display's images; their shared images are handed back once.
ASSERT_NE(fixture->State.CreateSharedImage(fixture->Display, {.Id = 7, .Width = 1, .Height = 1}), EGL_NO_IMAGE);
EXPECT_TRUE(fixture->State.TakeOrphanedSharedImages().empty());
ASSERT_TRUE(fixture->State.TerminateDisplay(fixture->Display));
EXPECT_EQ(fixture->State.TakeOrphanedSharedImages(), std::vector<uint64_t>{7});
EXPECT_TRUE(fixture->State.TakeOrphanedSharedImages().empty());
}
TEST(EGLStateDmaBufImport, FormatAndModifierQueries) {
auto fixture = CreateFixture();
EGLint count = -1;
ASSERT_TRUE(fixture->State.QueryDmaBufFormats(fixture->Display, true, 0, nullptr, &count));
EXPECT_EQ(count, 2);
EGLint formats[4] = {};
ASSERT_TRUE(fixture->State.QueryDmaBufFormats(fixture->Display, true, 4, formats, &count));
ASSERT_EQ(count, 2);
EXPECT_EQ(formats[0], static_cast<EGLint>(kAbgr8888));
EXPECT_EQ(formats[1], static_cast<EGLint>(kXbgr8888));
ASSERT_TRUE(fixture->State.QueryDmaBufFormats(fixture->Display, true, 1, formats, &count));
EXPECT_EQ(count, 1);
// Without shared images there is nothing to import.
ASSERT_TRUE(fixture->State.QueryDmaBufFormats(fixture->Display, false, 0, nullptr, &count));
EXPECT_EQ(count, 0);
EXPECT_FALSE(fixture->State.QueryDmaBufFormats(fixture->Display, true, -1, formats, &count));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_PARAMETER);
// No explicit modifiers: importers fall back to the implicit one.
uint64_t modifiers[2] = {};
EGLBoolean externalOnly[2] = {};
count = -1;
ASSERT_TRUE(fixture->State.QueryDmaBufModifiers(fixture->Display, true, static_cast<EGLint>(kAbgr8888), 2,
modifiers, externalOnly, &count));
EXPECT_EQ(count, 0);
EXPECT_FALSE(fixture->State.QueryDmaBufModifiers(fixture->Display, true, static_cast<EGLint>(kArgb8888), 0,
nullptr, nullptr, &count));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_BAD_PARAMETER);
ASSERT_TRUE(fixture->State.TerminateDisplay(fixture->Display));
EXPECT_FALSE(fixture->State.QueryDmaBufFormats(fixture->Display, true, 0, nullptr, &count));
EXPECT_EQ(fixture->State.ConsumeError(), EGL_NOT_INITIALIZED);
}
+21 -8
View File
@@ -151,7 +151,7 @@ TEST(PipeCatalogue, GeneratedTablesHoldTheWholeCatalogue) {
// + P14 S1's bind_context (docs/Disaggregated/design/11-state-ownership.md), the in-band
// context binding - a kScreen row for applier_reset's reason: a context switch is a
// whole-session edge, not a context-family verb.
EXPECT_EQ(ClassCount<kScreen>(), 13u);
EXPECT_EQ(ClassCount<kScreen>(), 14u); // + shared_image (opcode 85), a kScreen table row
EXPECT_EQ(ClassCount<kCtxQuery>(), 8u);
EXPECT_EQ(ClassCount<kCtxCso>(), 13u);
EXPECT_EQ(ClassCount<kCtxState>(), 19u);
@@ -652,7 +652,13 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
// kScreen control record with the applier_reset shape - one POD, no blob, no tail, no reply
// and no MGPipeApply* entry point - so the row beside it is pinned the same way.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::BindContext), 84);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 85);
// Shared images (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) appended opcode 85 by the
// same rule: a kScreen row with a reply slot - every operation is answered.
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::SharedImage), 85);
EXPECT_EQ(static_cast<Uint16>(MGPWireOp::kOpCount), 86);
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SharedImage), static_cast<Uint32>(kReplySlot));
EXPECT_EQ(MGPipeCallClassFor(MGPWireOp::SharedImage), kScreen);
EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::SharedImage), kWaitReply);
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast<Uint32>(kNone));
EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast<Uint32>(kNone));
EXPECT_EQ(sizeof(MGPReadbackToBuffer), 104u);
@@ -693,6 +699,11 @@ TEST(PipeCatalogue, LateArrivalsAreAppendedWithoutRenumbering) {
// the two planes cannot disagree about how much of a token is real.
EXPECT_EQ(sizeof(MGPBindContext), 8u);
EXPECT_EQ(sizeof(MGPBindContext::ClientContextToken), 8u);
// Shared images: the request, its answer and the aux-socket sideband are fixed-width PODs; the
// sideband is the T0 Offer's size so one inbox reads both.
EXPECT_EQ(sizeof(MGPSharedImageOp), 32u);
EXPECT_EQ(sizeof(MGPSharedImageReply), 40u);
EXPECT_EQ(sizeof(MGPSharedImageFdOffer), 32u);
EXPECT_EQ(sizeof(MGPHandleOnly), 16u);
// rv's two (§5.3/§7.6): the residual-value POD - 2 + 15 Uint32s, 2 Uint8s and 2 pad bytes,
// then the three Uint64s - and the AMENDED attribute carrier, which grew 24 -> 56 to carry
@@ -826,7 +837,7 @@ TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) {
EXPECT_EQ(MGPipeWaitClassFor(op), kWaitNone) << WireOpNameForDiag(op);
}
// The partition, by count. 14 + 1 + 10 = 25 rows wait; every other row of the catalogue does
// The partition, by count. 15 + 1 + 10 = 26 rows wait (shared_image made the reply rows 15); every other row of the catalogue does
// not. A row that changed class moves two of these numbers at once - which is why the
// kWaitApplied count went 9 -> 10 and the kWaitNone offset 24 -> 25 in the SAME commit that
// moved resource_copy_region (P5e gl, ID-118).
@@ -840,10 +851,10 @@ TEST(PipeCatalogue, EveryRowCarriesTheWaitClassTheContractGivesIt) {
default: ++other; break;
}
}
EXPECT_EQ(reply, 14u);
EXPECT_EQ(reply, 15u);
EXPECT_EQ(applied, 10u);
EXPECT_EQ(present, 1u);
EXPECT_EQ(none, static_cast<SizeT>(kMGPipeCallCount) - 25u);
EXPECT_EQ(none, static_cast<SizeT>(kMGPipeCallCount) - 26u);
EXPECT_EQ(other, 0u) << "a row carries the kWaitClassCount terminator as its class";
// And the reply half of the partition BOTH WAYS, over the whole catalogue: exactly the rows
@@ -1059,7 +1070,8 @@ TEST(PipeCatalogue, SixValueStructsHaveFieldLists) {
// appended bind_context's MGPBindContext (docs/Disaggregated/design/11-state-ownership.md):
// 84. A payload without a list here is one the comparator cannot see, so the row is not
// optional bookkeeping - gen_pipe.py refuses the build without it.
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 84u);
// + shared_image's MGPSharedImageOp: 85.
EXPECT_EQ(kMGPipeVerifiedPayloadCount, 85u);
static_assert(MGPipeHasFieldVerifier<RenderStateParameters>::value);
static_assert(MGPipeHasFieldVerifier<PixelStoreParameters>::value);
static_assert(MGPipeHasFieldVerifier<PerBufferBlendState>::value);
@@ -1345,13 +1357,14 @@ TEST(PipeCatalogue, ResourceRespecifyAcksOnlyImmutableStorage) {
// FOURTEEN. Ten answers that were always declared - get_caps, map_persistent, the two fence
// reads, the three query reads, the two readbacks and read_pixels - plus the FOUR ACCEPTANCE
// ROWS, whose applier entry points return a Bool the client acts on destructively and which
// carried no flag because in monolith that answer is a direct call's return value.
// carried no flag because in monolith that answer is a direct call's return value. FIFTEEN since
// shared_image, whose every operation is answered.
Uint32 replySlotCalls = 0;
#define MGP_COUNT_REPLY_SLOT_CALLS(Name, Payload, Class, Flags, Wait) \
if ((static_cast<Uint32>(Flags) & static_cast<Uint32>(kReplySlot)) != 0) ++replySlotCalls;
MGP_CALL_LIST(MGP_COUNT_REPLY_SLOT_CALLS)
#undef MGP_COUNT_REPLY_SLOT_CALLS
EXPECT_EQ(replySlotCalls, 14u);
EXPECT_EQ(replySlotCalls, 15u);
// And the four by name, because a count alone would let a row lose the flag while another
// gained one. These are exactly the MGPipeApply* entry points that return Bool
+3
View File
@@ -48,6 +48,9 @@ if (NOT WIN32)
# the in-process queue and a real SCM_RIGHTS socket pair: in order, out of order, late, stale,
# missing (bounded), not-an-Offer.
list(APPEND MOBILEGL_WIRE_TESTS AdoptInboxTest)
# Shared images: the registry (memfd-backed on a host), identification of a descriptor that
# crossed a socket, session holders, and the inbox keeping their descriptors apart from T0's.
list(APPEND MOBILEGL_WIRE_TESTS SharedImageTest)
# P9 W3: the graded log forward - the level mapping both ways, the logger's level reaching the
# forwarder, and the channel against a transport whose writes can be held (and a real socket
# pair nobody reads). POSIX for the socket pair.
+158
View File
@@ -0,0 +1,158 @@
// MobileGL - MobileGL/MG_Test/Wire/SharedImageTest.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
// Shared images (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): the server's registry, a
// session's references, and the aux-socket inbox that carries an import's descriptor beside T0's
// Offers. On a host the image memory is a memfd, which is enough for everything here: what is
// under test is that a descriptor which travelled through SCM_RIGHTS still names its image.
#include <MG_Pipe/MGPipeTypes.h>
#include <MG_Remote/Server/AdoptInbox.h>
#include <MG_Remote/Server/SharedImageRegistry.h>
#include <MG_Remote/Transport/AdoptT0.h>
#include <MG_Remote/Transport/FdPassing.h>
#include <MG_Remote/Transport/SocketTransport.h>
#include <gtest/gtest.h>
#include <memory>
#include <string>
#include <sys/mman.h>
#include <unistd.h>
using namespace MobileGL;
using namespace MobileGL::MG_Remote;
namespace SI = MobileGL::MG_Remote::Server::SharedImages;
namespace {
// A descriptor for `image` as a peer process would hold it: through a real socket pair.
int ThroughSocket(int fd) {
std::unique_ptr<Transport::SocketTransport> a, b;
EXPECT_EQ(Transport::SocketTransport::CreatePair(a, b), MOBILEGL_OK);
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, 1, 0, 0};
EXPECT_EQ(a->ShareFd(fd, MobileGLByteSpan{&offer, sizeof(offer)}), MOBILEGL_OK);
int received = -1;
Uint8 sideband[Transport::FdPassing::kMaxSidebandBytes] = {};
std::uint64_t size = 0;
EXPECT_EQ(b->ReceiveFd(&received, MobileGLMutableByteSpan{sideband, sizeof(sideband)}, &size, 1000),
MOBILEGL_OK);
return received;
}
} // namespace
TEST(SharedImage, AnExportedDescriptorIdentifiesItsImageAfterCrossingASocket) {
std::string why;
SI::ImageRef image = SI::Allocate(64, 32, SI::kFourccAbgr8888, why);
ASSERT_NE(image, nullptr) << why;
EXPECT_EQ(image->Width, 64u);
EXPECT_EQ(image->Height, 32u);
EXPECT_GE(image->Stride, 64u * 4u);
EXPECT_EQ(image->Modifier, SI::kModifierInvalid);
const int peer = ThroughSocket(image->Fd);
ASSERT_GE(peer, 0);
SI::ImageRef found = SI::Identify(peer, why);
::close(peer);
ASSERT_NE(found, nullptr) << why;
EXPECT_EQ(found->Id, image->Id);
EXPECT_EQ(SI::Find(image->Id), image);
}
// The negative control: a descriptor of the same kind this server never exported is refused.
TEST(SharedImage, AForeignDescriptorIsNotIdentified) {
std::string why;
SI::ImageRef image = SI::Allocate(16, 16, SI::kFourccXbgr8888, why);
ASSERT_NE(image, nullptr) << why;
const int foreign = ::memfd_create("not-a-shared-image", MFD_CLOEXEC);
ASSERT_GE(foreign, 0);
EXPECT_EQ(SI::Identify(foreign, why), nullptr);
EXPECT_FALSE(why.empty());
::close(foreign);
}
TEST(SharedImage, AnImageDiesWithItsLastReferenceAndIsNoLongerIdentified) {
std::string why;
SI::ImageRef image = SI::Allocate(8, 8, SI::kFourccAbgr8888, why);
ASSERT_NE(image, nullptr) << why;
const Uint64 id = image->Id;
const int kept = ::dup(image->Fd);
const SizeT live = SI::LiveCount();
image.reset();
EXPECT_EQ(SI::LiveCount(), live - 1);
EXPECT_EQ(SI::Find(id), nullptr);
EXPECT_EQ(SI::Identify(kept, why), nullptr);
::close(kept);
}
TEST(SharedImage, UnsupportedFormatsAndSizesAreRefused) {
std::string why;
EXPECT_EQ(SI::Allocate(16, 16, SI::FourCC('N', 'V', '1', '2'), why), nullptr);
EXPECT_EQ(SI::Allocate(0, 16, SI::kFourccAbgr8888, why), nullptr);
EXPECT_EQ(SI::Allocate(16, 1u << 20, SI::kFourccAbgr8888, why), nullptr);
}
// A session's holder keeps an image alive until its release or the session's end, and releases
// only its own reference: another holder's stays.
TEST(SharedImage, AHolderKeepsItsImagesUntilReleasedOrCleared) {
std::string why;
SI::SessionHolder producer, consumer;
SI::ImageRef image = SI::Allocate(32, 32, SI::kFourccAbgr8888, why);
ASSERT_NE(image, nullptr) << why;
const Uint64 id = image->Id;
producer.Hold(image);
consumer.Hold(image);
image.reset();
EXPECT_NE(SI::Find(id), nullptr);
EXPECT_TRUE(producer.Release(id));
EXPECT_FALSE(producer.Release(id));
EXPECT_NE(SI::Find(id), nullptr);
EXPECT_NE(consumer.Get(id), nullptr);
consumer.Clear();
EXPECT_EQ(consumer.Count(), 0u);
EXPECT_EQ(SI::Find(id), nullptr);
}
// The aux socket carries T0 Offers and shared-image descriptors at once; each Take finds its own
// kind for its own record, keeping the other for later.
TEST(SharedImage, TheInboxKeepsSharedImageAndT0DescriptorsApart) {
std::unique_ptr<Transport::SocketTransport> client, server;
ASSERT_EQ(Transport::SocketTransport::CreatePair(client, server), MOBILEGL_OK);
Server::AdoptInbox inbox;
inbox.Attach(server.get());
std::string why;
SI::ImageRef image = SI::Allocate(16, 16, SI::kFourccAbgr8888, why);
ASSERT_NE(image, nullptr) << why;
// The T0 Offer for the LATER record (6) arrives first; the import (5) is awaited first, as
// records apply in order, and the Offer must be kept for its own record.
int pipeFds[2] = {-1, -1};
ASSERT_EQ(::pipe(pipeFds), 0);
Transport::AdoptT0::Offer t0{};
t0.magic = Transport::AdoptT0::kOfferMagic;
t0.version = Transport::AdoptT0::kOfferVersion;
t0.seq = 6;
ASSERT_EQ(client->ShareFd(pipeFds[0], MobileGLByteSpan{&t0, sizeof(t0)}), MOBILEGL_OK);
MG_Pipe::MGPSharedImageFdOffer offer{MG_Pipe::kMGPSharedImageFdMagic, 1, 5, 0, 0};
ASSERT_EQ(client->ShareFd(image->Fd, MobileGLByteSpan{&offer, sizeof(offer)}), MOBILEGL_OK);
::close(pipeFds[0]);
::close(pipeFds[1]);
int fd = -1;
ASSERT_EQ(inbox.TakeSharedImageFd(5, 1000, &fd, why), Server::AdoptInbox::Outcome::Taken) << why;
SI::ImageRef found = SI::Identify(fd, why);
::close(fd);
ASSERT_NE(found, nullptr) << why;
EXPECT_EQ(found->Id, image->Id);
EXPECT_EQ(inbox.Kept(), 1u);
Transport::AdoptT0::Offer taken{};
ASSERT_EQ(inbox.Take(6, 0, &fd, &taken, why), Server::AdoptInbox::Outcome::Taken) << why;
EXPECT_EQ(taken.seq, 6u);
::close(fd);
// A shared-image record whose descriptor never comes is a bounded wait, not a hang.
EXPECT_EQ(inbox.TakeSharedImageFd(7, 20, &fd, why), Server::AdoptInbox::Outcome::TimedOut);
}