From 3d17e8c10bd77b5972bd8737e15bb3a88035e009 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 2 Oct 2026 08:39:59 -0400 Subject: [PATCH] [Feature] (SharedImage): zero-copy Wayland windows via linux-dmabuf, EGL dma-buf import, GBM backend, and Espryt/Magma AHB present and texture binding --- CMakeLists.txt | 9 + .../DirectGLES/BackendObject_DirectGLES.cpp | 9 + .../DirectGLES/BackendObject_DirectGLES.h | 3 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 418 ++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 26 ++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 98 ++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 20 + .../BackendObject_DirectVulkan.cpp | 16 + .../DirectVulkan/BackendObject_DirectVulkan.h | 1 + .../DirectVulkan/Renderer/UniformManager.cpp | 3 +- .../Renderer/VkTextureManager.cpp | 246 ++++++++++- .../DirectVulkan/Renderer/VkTextureManager.h | 45 ++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 5 + .../DirectVulkan/Renderer/VulkanRenderer.h | 14 + .../DirectVulkan/Renderer/WireFramebuffer.inc | 8 + .../DirectVulkan/Renderer/WireSharedImage.inc | 187 ++++++++ MobileGL/MG_Gbm/CMakeLists.txt | 39 ++ MobileGL/MG_Gbm/MobileGLGbm.cpp | 389 ++++++++++++++++ MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 95 +++- MobileGL/MG_Impl/EGLImpl/EGLImpl.h | 10 + .../MG_Impl/EGLImpl/EGLPlatformExtensions.h | 7 + .../MG_Impl/EGLImpl/Exporting/Definitions.cpp | 18 + .../MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp | 7 + MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp | 321 ++++++++++++-- MobileGL/MG_Impl/EGLImpl/WaylandWindow.h | 25 +- .../MG_Impl/GLImpl/Exporting/Definitions.cpp | 1 + MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 20 +- .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 48 ++ MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h | 1 + MobileGL/MG_Impl/GetProcAddress.cpp | 9 + MobileGL/MG_Impl/SharedImageApi.cpp | 107 +++++ MobileGL/MG_Impl/SharedImageApi.h | 90 ++++ MobileGL/MG_Remote/Server/AdoptInbox.cpp | 4 +- MobileGL/MG_State/EGLState/Core.cpp | 218 ++++++++- MobileGL/MG_State/EGLState/Core.h | 52 ++- MobileGL/MG_Test/EGLState/EGLStateTest.cpp | 163 +++++++ MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp | 29 +- MobileGL/MG_Test/Wire/CMakeLists.txt | 3 + MobileGL/MG_Test/Wire/SharedImageTest.cpp | 158 +++++++ 39 files changed, 2868 insertions(+), 54 deletions(-) create mode 100644 MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc create mode 100644 MobileGL/MG_Gbm/CMakeLists.txt create mode 100644 MobileGL/MG_Gbm/MobileGLGbm.cpp create mode 100644 MobileGL/MG_Impl/SharedImageApi.cpp create mode 100644 MobileGL/MG_Impl/SharedImageApi.h create mode 100644 MobileGL/MG_Test/Wire/SharedImageTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9869d457..3bd3781c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 50a904b7..2be53a57 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -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); } diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 717b244a..e880bcf5 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 748ea872..0408fdf5 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -32,6 +32,11 @@ #if MOBILEGL_BUILD_DISAGGREGATED // P5c (tx): §1's server-side per-level extent derivation, for GenerateMipmap's shape reads. #include +// Shared images: the registry the EGLImages are built from (SharedImageImpl below). +#include +#if defined(__ANDROID__) +#include +#endif #endif #include #include @@ -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 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 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(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( + g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID")); + entry->CreateImage = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR")); + entry->DestroyImage = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR")); + entry->TargetTexture2D = + reinterpret_cast(g_EGLFuncs.eglGetProcAddress("glEGLImageTargetTexture2DOES")); + entry->TargetRenderbuffer = reinterpret_cast( + 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(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(g_EGLFuncs.eglGetError()) : 0u); + why = text; + return nullptr; + } + auto result = MakeShared(); + 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 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 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(previous)); + if (error != GL_NO_ERROR) { + char text[96]; + std::snprintf(text, sizeof(text), "glEGLImageTargetRenderbufferStorageOES -> 0x%04x", + static_cast(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 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(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(sourceWidth) == view.Width && + static_cast(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(view.Height), static_cast(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(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(error)); + } + } else { + MGLOG_E_ONCE("Shared image present: image %llu's framebuffer is incomplete (0x%04x)", + static_cast(view.Id), static_cast(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 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 diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 3cc6ed63..37df0e3b 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -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; + + // 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; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index d4cda8ee..b8ab6e70 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -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> 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(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(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(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& 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 diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 32591853..bc58606c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -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 diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index 1fa4cbbe..f66bfd7c 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -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 lock(m_eglStateMutex); + if (!pVulkanRenderer) { + MGLOG_E("DirectVulkan: shared image %llu: the renderer is not initialized", + static_cast(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 lock(m_eglStateMutex); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h index 94f86b7a..7811d861 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h @@ -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; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp index b5badf83..3fc013f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp @@ -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(record.Desc.InternalFormat)).expandRgbToRgba; const auto swizzle = [&](Uint32 channel) { const auto value = record.Params.Swizzle[channel]; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 631d2fbe..696f95de 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -22,10 +22,16 @@ // (shape) and the server's staged-texture store (texels). #include #include +// Shared images: a texture whose level 0 is a server-allocated AHardwareBuffer. +#include #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 #endif +#endif +#include #include #include #include @@ -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(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(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(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(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(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(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(i); + } + for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) { + if ((properties.memoryTypeBits & (1u << i)) != 0) type = static_cast(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(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(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(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(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(id)); + return SharedImageBind::Unresolved; + } + const Bool flat2D = desc.Target == static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D) || + desc.Target == static_cast(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(desc.Target), desc.Width, + desc.Height, static_cast(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(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(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"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index c450041c..6d5c03e4 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -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 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. diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 6ff1d6c9..044ab86f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -48,6 +48,8 @@ // P5c (T5 / tx): the server's staged-texture shadow GenerateMipmap defines its chain on. #include #include +// Shared images: the present side resolves the image it imports (WireSharedImage.inc). +#include // P5c (G6): the named-blit arm's endpoint resolution runs inside the frontend-keyed scope. #include // 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 diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 0fb85b79..513ffc14 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -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 owner; + }; + UnorderedMap 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(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc index 2bdc3ff6..f1a8505f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireFramebuffer.inc @@ -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(resource->sharedImageId), logicalBase + 1, logicalEnd - 1); + return; + } m_textureManager->FlushPendingUploads(); RetireWireDrawPass(); auto& frame = m_frameContext.GetCurrent(); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc new file mode 100644 index 00000000..499ae357 --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/WireSharedImage.inc @@ -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(view.Id), static_cast(m_activeTargetKey), + static_cast(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(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(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(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(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(view.Id), source.extent.width, source.extent.height, + static_cast(source.format), view.Width, view.Height, + static_cast(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, ®ion); + } 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(view.Width); + const Int32 height = static_cast(view.Height); + VkImageBlit region{}; + region.srcSubresource = layers; + region.srcOffsets[1] = {static_cast(source.extent.width), static_cast(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, ®ion, 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(view.Id)); + return false; + } + return true; + } +#endif // MOBILEGL_BUILD_DISAGGREGATED diff --git a/MobileGL/MG_Gbm/CMakeLists.txt b/MobileGL/MG_Gbm/CMakeLists.txt new file mode 100644 index 00000000..ea5126ba --- /dev/null +++ b/MobileGL/MG_Gbm/CMakeLists.txt @@ -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 and (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") diff --git a/MobileGL/MG_Gbm/MobileGLGbm.cpp b/MobileGL/MG_Gbm/MobileGLGbm.cpp new file mode 100644 index 00000000..0975ed4f --- /dev/null +++ b/MobileGL/MG_Gbm/MobileGLGbm.cpp @@ -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 /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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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( + dlsym(lib, "mobilegl_shared_image_abi_version")); + api.allocate = reinterpret_cast(dlsym(lib, "mobilegl_shared_image_allocate")); + api.import = reinterpret_cast(dlsym(lib, "mobilegl_shared_image_import")); + api.release = reinterpret_cast(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 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); } + + // ---- device ----------------------------------------------------------------------------- + + void DeviceDestroy(gbm_device* gbm) { delete reinterpret_cast(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(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(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(data->strides[0]); + offset = static_cast(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; +} diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index 9310c7e9..95e06b6b 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -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(request.Width), + static_cast(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(modifiers), external_only, num_modifiers) + ? EGL_TRUE + : EGL_FALSE; } EGLDisplay GetPlatformDisplay(EGLenum platform, void* native_display, const EGLAttrib* attrib_list) { diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h index b2ac4a08..47ffeb2d 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include @@ -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); diff --git a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h index 09b3ea52..7f33dfd2 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h @@ -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; diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp index f0413844..34cdc33e 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp @@ -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; diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp index 9d85948b..002587e9 100644 --- a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp @@ -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(eglDestroyImage)}, {"eglCreateImageKHR", reinterpret_cast(eglCreateImageKHR)}, {"eglDestroyImageKHR", reinterpret_cast(eglDestroyImageKHR)}, + {"eglQueryDmaBufFormatsEXT", reinterpret_cast(eglQueryDmaBufFormatsEXT)}, + {"eglQueryDmaBufModifiersEXT", reinterpret_cast(eglQueryDmaBufModifiersEXT)}, {"eglCreatePlatformWindowSurface", reinterpret_cast(eglCreatePlatformWindowSurface)}, {"eglCreatePlatformPixmapSurface", reinterpret_cast(eglCreatePlatformPixmapSurface)}, // The platform and current-state queries: not in glvnd's required list, but the names an diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp index 0384d57b..d108ec88 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp @@ -10,6 +10,8 @@ #if MOBILEGL_WAYLAND_WINDOWS +#include "EGLImpl.h" +#include #include #include #include @@ -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(data)->busy = false; } void (*const kBufferListener[])(void) = {reinterpret_cast(&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(kDmabufMaxVersion), 2, p.dmabufRequests, 2, + p.dmabufEvents}; + p.params = {"zwp_linux_buffer_params_v1", static_cast(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(&OnDmabufFormat), + reinterpret_cast(&OnDmabufModifier)}; + + void OnParamsCreated(void*, wl_proxy*, wl_proxy*) {} + void OnParamsFailed(void* data, wl_proxy*) { *static_cast(data) = true; } + void (*const kParamsListener[])(void) = {reinterpret_cast(&OnParamsCreated), + reinterpret_cast(&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(data)->busy = false; } + void (*const kDmabufBufferListener[])(void) = {reinterpret_cast(&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 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(width), static_cast(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(width) || + image.Height != static_cast(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(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(image.Modifier >> 32), + static_cast(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(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(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(kDmabufListener), impl); + } + } } void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {} void (*const kRegistryListener[])(void) = {reinterpret_cast(&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(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(buffer->map), static_cast(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 diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h index 384f41ff..9fc1f8dc 100644 --- a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h @@ -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 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; diff --git a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp index d38f161e..e19d92a9 100644 --- a/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp @@ -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) diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index e47779ea..bbc1dbf0 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -38,6 +38,7 @@ #include #include #include +#include // 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(rendererInfo.RendererGLInfo.Extensions.size()); + *params = static_cast(rendererInfo.RendererGLInfo.Extensions.size()) + + (ClientSideExtension() != nullptr ? 1 : 0); break; case GL_POINT_SIZE_GRANULARITY: *params = static_cast(dynamicParameters.PointSizeGranularity); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 682ce101..2f64b75a 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -32,6 +32,8 @@ #include #include #include +#include +#include // 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("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(image), &sharedImageId, &width, &height)) { + MG_State::pGLContext->RecordError( + ErrorCode::InvalidValue, + MakeUnique("MG_Impl/GLImpl", __func__, "image is not a valid EGLImage.")); + return; + } + auto& activeUnit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit()); + const SharedPtr 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(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(sharedImageId), textureObject->GetExternalIndex()); + MG_State::pGLContext->RecordError( + ErrorCode::InvalidOperation, + MakeUnique("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); diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h index b29cdbde..e14e7480 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.h @@ -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); diff --git a/MobileGL/MG_Impl/GetProcAddress.cpp b/MobileGL/MG_Impl/GetProcAddress.cpp index f74a5721..c9a46478 100644 --- a/MobileGL/MG_Impl/GetProcAddress.cpp +++ b/MobileGL/MG_Impl/GetProcAddress.cpp @@ -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); diff --git a/MobileGL/MG_Impl/SharedImageApi.cpp b/MobileGL/MG_Impl/SharedImageApi.cpp new file mode 100644 index 00000000..292ffc77 --- /dev/null +++ b/MobileGL/MG_Impl/SharedImageApi.cpp @@ -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 +#include +#include + +#include +#include +#include +#include + +#include + +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(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 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(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 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 lock(g_apiMutex); + BackendObject* backend = ActiveBackend(false); + if (backend == nullptr) return -ENODEV; + return backend->ReleaseSharedImage(id) ? 0 : -EIO; +} diff --git a/MobileGL/MG_Impl/SharedImageApi.h b/MobileGL/MG_Impl/SharedImageApi.h new file mode 100644 index 00000000..6c57fdc5 --- /dev/null +++ b/MobileGL/MG_Impl/SharedImageApi.h @@ -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 + +#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 */ diff --git a/MobileGL/MG_Remote/Server/AdoptInbox.cpp b/MobileGL/MG_Remote/Server/AdoptInbox.cpp index 65838c27..5d647c72 100644 --- a/MobileGL/MG_Remote/Server/AdoptInbox.cpp +++ b/MobileGL/MG_Remote/Server/AdoptInbox.cpp @@ -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) { diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index b67f264c..ead4557d 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -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 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 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(value); + hasWidth = true; + break; + case EGL_HEIGHT: + attribs.Height = static_cast(value); + hasHeight = true; + break; + case EGL_LINUX_DRM_FOURCC_EXT: + attribs.Fourcc = static_cast(value); + hasFourcc = true; + break; + case EGL_DMA_BUF_PLANE0_FD_EXT: + attribs.Fd = static_cast(value); + hasFd = true; + break; + case EGL_DMA_BUF_PLANE0_OFFSET_EXT: + attribs.Offset = static_cast(value); + hasOffset = true; + break; + case EGL_DMA_BUF_PLANE0_PITCH_EXT: + attribs.Pitch = static_cast(value); + hasPitch = true; + break; + case EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT: + modifierLo = static_cast(value); + hasModifierLo = true; + break; + case EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT: + modifierHi = static_cast(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 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(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 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 EGLContext::TakeOrphanedSharedImages() { + const std::lock_guard lock(m_mutex); + Vector orphaned; + orphaned.swap(m_orphanedSharedImages); + return orphaned; + } + + Bool EGLContext::QueryDmaBufFormats(EGLDisplayHandle display, Bool available, EGLint maxFormats, + EGLint* formats, EGLint* numFormats) { + const std::lock_guard 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(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(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 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(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. diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index cc422c0c..34716777 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -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 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 m_contexts; UnorderedMap m_syncs; UnorderedMap m_images; + Vector m_orphanedSharedImages; UnorderedMap m_threadErrors; UnorderedMap m_threadBoundAPI; diff --git a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp index d53b08a9..b55ce920 100644 --- a/MobileGL/MG_Test/EGLState/EGLStateTest.cpp +++ b/MobileGL/MG_Test/EGLState/EGLStateTest.cpp @@ -1,8 +1,10 @@ #include #include +#include #include #include #include +#include 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 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 WithAttrib(std::vector attribs, EGLAttrib name, EGLAttrib value) { + attribs.insert(attribs.end() - 1, {name, value}); + return attribs; + } + + std::vector WithoutAttrib(const std::vector& attribs, EGLAttrib name) { + std::vector 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& 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(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(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, ¬ABuffer), 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{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(kAbgr8888)); + EXPECT_EQ(formats[1], static_cast(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(kAbgr8888), 2, + modifiers, externalOnly, &count)); + EXPECT_EQ(count, 0); + EXPECT_FALSE(fixture->State.QueryDmaBufModifiers(fixture->Display, true, static_cast(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); +} diff --git a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp index 61b5452d..3746d310 100644 --- a/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp +++ b/MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp @@ -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(), 13u); + EXPECT_EQ(ClassCount(), 14u); // + shared_image (opcode 85), a kScreen table row EXPECT_EQ(ClassCount(), 8u); EXPECT_EQ(ClassCount(), 13u); EXPECT_EQ(ClassCount(), 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(MGPWireOp::BindContext), 84); - EXPECT_EQ(static_cast(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(MGPWireOp::SharedImage), 85); + EXPECT_EQ(static_cast(MGPWireOp::kOpCount), 86); + EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::SharedImage), static_cast(kReplySlot)); + EXPECT_EQ(MGPipeCallClassFor(MGPWireOp::SharedImage), kScreen); + EXPECT_EQ(MGPipeWaitClassFor(MGPWireOp::SharedImage), kWaitReply); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::ReadPixelsToBuffer), static_cast(kNone)); EXPECT_EQ(MGPipeCallFlagsFor(MGPWireOp::GetTextureImageToBuffer), static_cast(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(kMGPipeCallCount) - 25u); + EXPECT_EQ(none, static_cast(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::value); static_assert(MGPipeHasFieldVerifier::value); static_assert(MGPipeHasFieldVerifier::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(Flags) & static_cast(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 diff --git a/MobileGL/MG_Test/Wire/CMakeLists.txt b/MobileGL/MG_Test/Wire/CMakeLists.txt index f7173472..e2f94043 100644 --- a/MobileGL/MG_Test/Wire/CMakeLists.txt +++ b/MobileGL/MG_Test/Wire/CMakeLists.txt @@ -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. diff --git a/MobileGL/MG_Test/Wire/SharedImageTest.cpp b/MobileGL/MG_Test/Wire/SharedImageTest.cpp new file mode 100644 index 00000000..6e1bf041 --- /dev/null +++ b/MobileGL/MG_Test/Wire/SharedImageTest.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +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 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 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); +}