From 0dad3fcaa1b43db92f19df07a6222cf19475365d Mon Sep 17 00:00:00 2001 From: gold Date: Thu, 1 Oct 2026 02:36:53 +0800 Subject: [PATCH] [Feature] (EGL): MobileGL serves EGL through libglvnd On a system with libglvnd the libEGL.so.1 an application links is a dispatcher, not an implementation, and the supported way to be the implementation it talks to is to be a vendor library it loads and routes to. On this machine it is also the ONLY way that reaches a caller which does not link EGL directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its entry points at startup, so a library injected with LD_PRELOAD is invisible to it however loudly it is named - measured, with the client in kwin's maps and not one call into it. So this is the vendor half of that ABI: __egl_Main answers glvnd with the entry points by name, the platforms this implementation serves, and the device it is associated with - the render node of the machine, because that is what lets a compositor match its own node to the display it is about to draw into, and because glvnd drops a vendor that does not answer at all. Two details are the difference between a vendor that is loaded and a vendor that is used. glvnd requires setDispatchIndex to exist even when a vendor has nothing to record, which this one has - its entry points are the implementation, they never re-enter glvnd. And a vendor must hand over ITS OWN entry points: with default visibility a reference inside this library resolves against the whole process, where another libEGL that loaded first wins. That is what -Bsymbolic-functions is for; declaring the entry points protected says the same thing but does not compile against the platform headers, which declare them default. --- CMakeLists.txt | 18 + MobileGL/Defines.h | 5 + .../MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp | 311 ++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8575b6b6..31b30115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -403,6 +403,8 @@ set(SOURCE_FILES MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp + + MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp # @INSERTION_POINT:SOURCE_FILE_GLIMPL@ # @@ -778,6 +780,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # target, and above all no flatc in the build graph. protocol_generated.h # is committed and regenerated by scripts/gen_protocol.py. list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include) + # The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library + # exposes so the system libEGL - which IS glvnd - can dispatch to it. + list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include) endif() add_library(${CMAKE_PROJECT_NAME} SHARED @@ -888,6 +893,19 @@ if (TRACY_ENABLE) target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE) endif () +# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE. +# +# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to - +# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so +# an address that resolves to another libEGL in the process is a call back into the dispatcher. +# With default visibility every reference inside this library resolves against the whole process, +# and a process that loaded the system libEGL first wins. Marking the entry points protected says +# the same thing in the source, except the platform's own GL/EGL headers declare them default and +# the two visibilities do not mix; this flag is that promise made by the linker instead. +if (UNIX AND NOT APPLE) + target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions") +endif() + if (ANDROID) target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC android diff --git a/MobileGL/Defines.h b/MobileGL/Defines.h index b6f54e56..3fabfc82 100644 --- a/MobileGL/Defines.h +++ b/MobileGL/Defines.h @@ -34,6 +34,11 @@ #ifdef _WIN32 #define MOBILEGL_EXPORT extern "C" __declspec(dllexport) #else +// Default visibility, which is what the platform's own GL and EGL headers declare their entry +// points with. The one place where that is not enough - a vendor library handing glvnd its own +// entry points, where a reference resolving against the process instead of this library would be a +// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than +// here, because a second visibility on the same declaration does not compile. #define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default"))) #endif diff --git a/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp new file mode 100644 index 00000000..3865736a --- /dev/null +++ b/MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp @@ -0,0 +1,311 @@ +// MobileGL - MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +// MOBILEGL AS A GLVND EGL VENDOR. +// +// On a system with libglvnd, the libEGL.so.1 an application links is not an implementation at all: +// it is a dispatcher, and the implementations are vendor libraries it loads from +// /usr/share/glvnd/egl_vendor.d (or from whatever __EGL_VENDOR_LIBRARY_FILENAMES names) and routes +// every EGL call to. That is the supported way to be the implementation an application actually +// talks to, and on this machine it is the ONLY way that reaches a caller which does not link EGL +// directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its entry points at +// startup, so a library injected with LD_PRELOAD is invisible to it however loudly it is named. +// +// What glvnd asks a vendor for is the other half of that ABI, and this file is it: glvnd calls +// __egl_Main once, the library answers with the functions it wants dispatched to it, and - the part +// that decides whether a compositor can use it at all - with the string naming the PLATFORMS it +// serves. A vendor that does not claim EGL_PLATFORM_GBM_KHR is never asked for a GBM display, and +// the caller that asked for one is told there is no usable device: the client sitting in its maps, +// loaded and never called, is exactly what that looks like from the outside. +// +// The entry points below are this library's own (MG_Impl::EGLImpl, the same functions +// Exporting/Definitions.cpp exports), so a vendor call and a direct call cannot drift apart. + +#include +#include + +#include "../EGLImpl.h" +#include "../EGLPlatformExtensions.h" + +namespace { + using namespace MobileGL; + + const __EGLapiExports* g_eglExports = nullptr; + + // The platforms this vendor serves. GBM is the one that matters here: the compositor asks for + // a display on the GBM device it opened, and a vendor that does not name the platform is not + // asked at all. This side treats a GBM device as it treats every other native display - the + // frames come from the display host, not from the device - so claiming the platform is honest + // rather than optimistic. + constexpr const char* kPlatformExtensions = MG_Impl::EGLImpl::kPlatformExtensionString; + + // glvnd asks for the client-only extension list separately from the platform one; the split is + // its own invention, and this side answers both from the same query. + const char* EGLAPIENTRY VendorQueryString(EGLDisplay dpy, EGLint name) { + return MG_Impl::EGLImpl::QueryString(dpy, name); + } + + EGLDisplay EGLAPIENTRY VendorGetPlatformDisplay(EGLenum platform, void* native_display, + const EGLAttrib* attrib_list) { + // EGL_NONE means the application called eglGetDisplay(EGL_DEFAULT_DISPLAY): the vendor is + // being asked to name its own default display. + if (platform == EGL_NONE) { + return MG_Impl::EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY); + } + return MG_Impl::EGLImpl::GetPlatformDisplay(platform, native_display, attrib_list); + } + + EGLBoolean EGLAPIENTRY VendorGetSupportsAPI(EGLenum api) { + // What this library answers for EGL_CLIENT_APIS, said as a question. + return (api == EGL_OPENGL_ES_API || api == EGL_OPENGL_API) ? EGL_TRUE : EGL_FALSE; + } + + const char* EGLAPIENTRY VendorGetVendorString(int name) { + if (name == __EGL_VENDOR_STRING_PLATFORM_EXTENSIONS) { + return kPlatformExtensions; + } + return nullptr; + } + + // GLVND ASKS FOR EVERY EGL ENTRY POINT BY NAME, AND DROPS THE VENDOR IF ONE IS MISSING: the + // first eight functions the table below names are looked up before this library is ever asked + // for a display, and a single null answer means glvnd quietly stops considering it - the + // library is loaded, __egl_Main has run, and nothing is ever dispatched to it. + struct EntryPoint { + const char* name; + void* address; + }; + + // ---- the device family ------------------------------------------------------------------- + // + // A compositor does not ask for a display and hope. It asks which devices exist, reads the DRM + // node off the one it wants, and only then asks for a display ON that device - so a vendor that + // answers "no devices" is one it cannot use, however well the vendor draws. The message it + // gives is "no usable DRM render device", which names where it stopped rather than why. + // + // This implementation therefore names one device: the render node of the machine it runs on. + // That is an honest answer even though the frames do not come from that node - they come from + // the display host on the other end of the socket, which is why every surface here is a host + // frame - because the question is which device this display is associated with, and the answer + // is what lets the compositor match its own node to the display it is about to draw into. + const char* DrmRenderNode() { + // MOBILEGL_DEVICE_DRM_NODE names another node: a machine with more than one GPU, or a test + // that wants the mismatch refused rather than papered over. + const char* configured = std::getenv("MOBILEGL_DEVICE_DRM_NODE"); + return (configured != nullptr && configured[0] != '\0') ? configured : "/dev/dri/renderD128"; + } + + // One device, one identity. A pointer to a private object rather than a number, so that a + // device from somewhere else cannot compare equal to this one by accident. + struct DeviceToken { int unused; }; + DeviceToken g_deviceToken; + + EGLBoolean EGLAPIENTRY VendorQueryDevicesEXT(EGLint max_devices, EGLDeviceEXT* devices, EGLint* num_devices) { + if (num_devices == nullptr) { + return EGL_FALSE; + } + *num_devices = 1; + if (devices != nullptr && max_devices >= 1) { + devices[0] = reinterpret_cast(&g_deviceToken); + } + return EGL_TRUE; + } + + EGLBoolean EGLAPIENTRY VendorQueryDeviceAttribEXT(EGLDeviceEXT device, EGLint attribute, EGLAttrib* value) { + if (device != reinterpret_cast(&g_deviceToken) || value == nullptr) { + return EGL_FALSE; + } + switch (attribute) { + case EGL_DRM_DEVICE_FILE_EXT: + case EGL_DRM_RENDER_NODE_FILE_EXT: + *value = reinterpret_cast(DrmRenderNode()); + return EGL_TRUE; + default: + return EGL_FALSE; + } + } + + const char* EGLAPIENTRY VendorQueryDeviceStringEXT(EGLDeviceEXT device, EGLint name) { + if (device != reinterpret_cast(&g_deviceToken)) { + return nullptr; + } + // EGL_EXT_device_drm asks for the node as a STRING, not as an attribute, and that query is + // the one a compositor actually makes before it opens anything - it is how the display it is + // about to create is matched to a node it already holds. Answering only EGL_EXTENSIONS here + // leaves that lookup empty, and the failure surfaces one layer down as the node "not being + // openable", which is the compositor's wording for having had nothing to open. + if (name == EGL_DRM_DEVICE_FILE_EXT || name == EGL_DRM_RENDER_NODE_FILE_EXT) { + return DrmRenderNode(); + } + if (name == EGL_EXTENSIONS) { + return "EGL_EXT_device_drm EGL_EXT_device_drm_render_node"; + } + return nullptr; + } + + EGLBoolean EGLAPIENTRY VendorQueryDisplayAttribEXT(EGLDisplay dpy, EGLint attribute, EGLAttrib* value) { + (void)dpy; + if (value == nullptr) { + return EGL_FALSE; + } + if (attribute == EGL_DEVICE_EXT) { + *value = reinterpret_cast(&g_deviceToken); + return EGL_TRUE; + } + return EGL_FALSE; + } + + // Three of the names glvnd insists on are debug extensions this implementation does not + // provide, and glvnd insists on them all the same - a vendor that answers null for any of them + // is dropped. They are answered honestly rather than pretended: KHR_debug is not in the + // extension strings above, so a caller that reads them will not call these at all. + EGLint EGLAPIENTRY VendorDebugMessageControlKHR(EGLDEBUGPROCKHR callback, const EGLAttrib* attrib_list) { + (void)callback; + (void)attrib_list; + return EGL_FALSE; + } + + EGLBoolean EGLAPIENTRY VendorQueryDebugKHR(EGLint attribute, EGLAttrib* value) { + (void)attribute; + (void)value; + return EGL_FALSE; + } + + EGLint EGLAPIENTRY VendorLabelObjectKHR(EGLDisplay dpy, EGLenum objectType, EGLObjectKHR object, EGLLabelKHR label) { + (void)dpy; + (void)objectType; + (void)object; + (void)label; + return EGL_BAD_PARAMETER; + } + + const EntryPoint kEglEntryPoints[] = { + // The core of EGL 1.5, every one of which glvnd looks up by name before it considers this + // library usable. + {"eglInitialize", reinterpret_cast(eglInitialize)}, + {"eglChooseConfig", reinterpret_cast(eglChooseConfig)}, + {"eglCopyBuffers", reinterpret_cast(eglCopyBuffers)}, + {"eglCreateContext", reinterpret_cast(eglCreateContext)}, + {"eglCreatePbufferSurface", reinterpret_cast(eglCreatePbufferSurface)}, + {"eglCreatePixmapSurface", reinterpret_cast(eglCreatePixmapSurface)}, + {"eglCreateWindowSurface", reinterpret_cast(eglCreateWindowSurface)}, + {"eglDestroyContext", reinterpret_cast(eglDestroyContext)}, + {"eglDestroySurface", reinterpret_cast(eglDestroySurface)}, + {"eglGetConfigAttrib", reinterpret_cast(eglGetConfigAttrib)}, + {"eglGetConfigs", reinterpret_cast(eglGetConfigs)}, + {"eglMakeCurrent", reinterpret_cast(eglMakeCurrent)}, + {"eglQueryContext", reinterpret_cast(eglQueryContext)}, + {"eglQuerySurface", reinterpret_cast(eglQuerySurface)}, + {"eglSwapBuffers", reinterpret_cast(eglSwapBuffers)}, + {"eglTerminate", reinterpret_cast(eglTerminate)}, + {"eglWaitGL", reinterpret_cast(eglWaitGL)}, + {"eglWaitNative", reinterpret_cast(eglWaitNative)}, + {"eglBindTexImage", reinterpret_cast(eglBindTexImage)}, + {"eglReleaseTexImage", reinterpret_cast(eglReleaseTexImage)}, + {"eglSurfaceAttrib", reinterpret_cast(eglSurfaceAttrib)}, + {"eglSwapInterval", reinterpret_cast(eglSwapInterval)}, + {"eglBindAPI", reinterpret_cast(eglBindAPI)}, + {"eglCreatePbufferFromClientBuffer", reinterpret_cast(eglCreatePbufferFromClientBuffer)}, + {"eglReleaseThread", reinterpret_cast(eglReleaseThread)}, + {"eglWaitClient", reinterpret_cast(eglWaitClient)}, + {"eglGetError", reinterpret_cast(eglGetError)}, + {"eglCreateSync", reinterpret_cast(eglCreateSync)}, + {"eglDestroySync", reinterpret_cast(eglDestroySync)}, + {"eglClientWaitSync", reinterpret_cast(eglClientWaitSync)}, + {"eglGetSyncAttrib", reinterpret_cast(eglGetSyncAttrib)}, + {"eglWaitSync", reinterpret_cast(eglWaitSync)}, + {"eglCreateImage", reinterpret_cast(eglCreateImage)}, + {"eglDestroyImage", reinterpret_cast(eglDestroyImage)}, + {"eglCreatePlatformWindowSurface", reinterpret_cast(eglCreatePlatformWindowSurface)}, + {"eglCreatePlatformPixmapSurface", reinterpret_cast(eglCreatePlatformPixmapSurface)}, + // The platform and current-state queries: not in glvnd's required list, but the names an + // application that talks to a vendor through a dispatcher will ask for by name. + {"eglGetDisplay", reinterpret_cast(eglGetDisplay)}, + {"eglGetCurrentDisplay", reinterpret_cast(eglGetCurrentDisplay)}, + {"eglGetCurrentSurface", reinterpret_cast(eglGetCurrentSurface)}, + {"eglGetCurrentContext", reinterpret_cast(eglGetCurrentContext)}, + {"eglQueryAPI", reinterpret_cast(eglQueryAPI)}, + {"eglGetProcAddress", reinterpret_cast(eglGetProcAddress)}, + // The two queries whose answer depends on which side of glvnd is asking. + {"eglQueryString", reinterpret_cast(VendorQueryString)}, + {"eglGetPlatformDisplay", reinterpret_cast(VendorGetPlatformDisplay)}, + {"eglGetPlatformDisplayEXT", reinterpret_cast(VendorGetPlatformDisplay)}, + // What glvnd requires and this implementation does not otherwise have. + {"eglQueryDevicesEXT", reinterpret_cast(VendorQueryDevicesEXT)}, + {"eglQueryDeviceAttribEXT", reinterpret_cast(VendorQueryDeviceAttribEXT)}, + {"eglQueryDeviceStringEXT", reinterpret_cast(VendorQueryDeviceStringEXT)}, + {"eglQueryDisplayAttribEXT", reinterpret_cast(VendorQueryDisplayAttribEXT)}, + {"eglDebugMessageControlKHR", reinterpret_cast(VendorDebugMessageControlKHR)}, + {"eglQueryDebugKHR", reinterpret_cast(VendorQueryDebugKHR)}, + {"eglLabelObjectKHR", reinterpret_cast(VendorLabelObjectKHR)}, + }; + + void* EGLAPIENTRY VendorGetProcAddress(const char* procName) { + if (procName == nullptr) { + return nullptr; + } + for (const EntryPoint& entry : kEglEntryPoints) { + if (std::strcmp(entry.name, procName) == 0) { + return entry.address; + } + } + // Anything that is not EGL is a GL or GLES entry point, and the tree's own resolver is the + // table for those. + return reinterpret_cast(MG_Impl::EGLImpl::GetProcAddress(procName)); + } + + // glvnd hands a vendor the index each EGL entry point occupies in its dispatch table, so that a + // vendor whose entry points are thin wrappers around glvnd can call back into the right slot. + // This library's entry points ARE the implementation - they never re-enter glvnd - so there is + // nothing to record; the callback exists because glvnd refuses a vendor that does not provide + // one, and refusing it means the library is loaded and never dispatched to. + void EGLAPIENTRY VendorSetDispatchIndex(const char* procName, int index) { + (void)procName; + (void)index; + } + + // glvnd builds its GL/GLES dispatch out of these: one address per entry point, taken from the + // vendor that owns the current display. This library exports them all, so the answer is its + // own table. + void* EGLAPIENTRY VendorGetDispatchAddress(const char* procName) { + if (procName == nullptr) { + return nullptr; + } + return reinterpret_cast(MG_Impl::EGLImpl::GetProcAddress(procName)); + } +} // namespace + +extern "C" MOBILEGL_EGL_API EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports* exports, + __EGLvendorInfo* vendor, __EGLapiImports* imports) { + (void)vendor; + if (EGL_VENDOR_ABI_GET_MAJOR_VERSION(version) != EGL_VENDOR_ABI_MAJOR_VERSION) { + // A vendor that does not know the ABI it is being handed must say so, not guess: the + // structures on either side are the ABI. + MGLOG_E("__egl_Main: unsupported glvnd vendor ABI version 0x%08x (this library implements %u.%u)", version, + static_cast(EGL_VENDOR_ABI_MAJOR_VERSION), + static_cast(EGL_VENDOR_ABI_MINOR_VERSION)); + return EGL_FALSE; + } + if (exports == nullptr || imports == nullptr) { + MGLOG_E("__egl_Main: glvnd handed no exports/imports table"); + return EGL_FALSE; + } + g_eglExports = exports; + + imports->getPlatformDisplay = VendorGetPlatformDisplay; + imports->getSupportsAPI = VendorGetSupportsAPI; + imports->getVendorString = VendorGetVendorString; + imports->getProcAddress = VendorGetProcAddress; + imports->getDispatchAddress = VendorGetDispatchAddress; + imports->setDispatchIndex = VendorSetDispatchIndex; + + MGLOG_I("glvnd vendor: MobileGL is serving EGL through libglvnd (ABI %u.%u, platforms: %s)", + static_cast(EGL_VENDOR_ABI_MAJOR_VERSION), static_cast(EGL_VENDOR_ABI_MINOR_VERSION), + kPlatformExtensions); + return EGL_TRUE; +}