From 32c79f41350186274b6d545083656d564c0ea80f Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Thu, 1 Oct 2026 10:59:07 -0400 Subject: [PATCH] [Feature] (EGL): Wayland window surfaces, presented through wl_shm A Wayland client's window is its own wl_surface, and what the compositor shows is the wl_buffer attached to it - but the pixels of a MobileGL frame are drawn where the backend is, on a split build in another process on another OS. So far a wl_egl_window was taken for an X11 window and its client-side address sent to the Android server (EGL_BAD_NATIVE_WINDOW at best), and under glvnd a Wayland display never reached this vendor at all. A window on a Wayland display - asked for as EGL_PLATFORM_WAYLAND, or a wl_display recognised by its first word being wl_display_interface - is now a window surface in the EGL state at the wl_egl_window's size with a pbuffer of that size behind it in the backend, whatever MOBILEGL_IPC_SURFACE says. At every swap the frame is read back out of the default framebuffer through this library's own glReadPixels (the application's read framebuffer, pack buffer and pack state put back as they were), its rows flipped and swizzled into a wl_shm buffer, and attached, damaged and committed on the application's wl_surface. Three buffers rotate on their release events, dispatched on an event queue of the surface's own. libwayland-client is loaded at run time, so neither the build nor the library depends on it. EGL_KHR_platform_wayland and EGL_EXT_platform_wayland are announced on Linux. Measured: glmark2-wayland runs its build scene at 13 FPS (400x300) and the window shows the frame under kwin. A window resize is not followed yet; the surface keeps its creation size. --- CMakeLists.txt | 1 + MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp | 96 +++++ .../MG_Impl/EGLImpl/EGLPlatformExtensions.h | 81 +--- MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp | 398 ++++++++++++++++++ MobileGL/MG_Impl/EGLImpl/WaylandWindow.h | 72 ++++ MobileGL/MG_State/EGLState/Core.cpp | 10 + MobileGL/MG_State/EGLState/Core.h | 3 + 7 files changed, 594 insertions(+), 67 deletions(-) create mode 100644 MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp create mode 100644 MobileGL/MG_Impl/EGLImpl/WaylandWindow.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 31b30115..2012fa7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,6 +406,7 @@ set(SOURCE_FILES MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp + MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp # @INSERTION_POINT:SOURCE_FILE_GLIMPL@ # MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp diff --git a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp index da6b938e..c1e2fa3c 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp +++ b/MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp @@ -8,6 +8,7 @@ #include "EGLImpl.h" #include "EGLPlatformExtensions.h" +#include "WaylandWindow.h" #include "../GetProcAddress.h" #include #include @@ -117,6 +118,73 @@ namespace MobileGL::MG_Impl::EGLImpl { return reinterpret_cast(static_cast(nativeHandle)); } } + +#if MOBILEGL_WAYLAND_WINDOWS + // Every Wayland window surface's presentation, by surface. Guarded by EGLOperationMutex, + // which every entry point that reaches it holds. + UnorderedMap>& WaylandSurfaces() { + static auto* surfaces = new UnorderedMap>(); + return *surfaces; + } + + // A WINDOW ON A WAYLAND DISPLAY. Whatever MOBILEGL_IPC_SURFACE says, the window is this + // application's own wl_surface, so neither the server's window nor the display host's frame + // is what it shows: the backend draws into a pbuffer of the window's size, and each swap + // reads the frame back and attaches it to the wl_surface (WaylandWindow.h). Answers false + // when the display is not a Wayland one; otherwise *out is the surface, or EGL_NO_SURFACE + // with the error set. + template + Bool CreateWaylandWindowSurface(State* state, EGLDisplay dpy, EGLConfig config, void* window, + Bool platformWindow, EGLSurface* out) { + Uint64 nativeDisplay = 0; + EGLenum platform = EGL_NONE; + if (!state->GetDisplayNative(dpy, &nativeDisplay, &platform) || + !Wayland::IsWaylandDisplay(nativeDisplay, platform)) { + return false; + } + *out = EGL_NO_SURFACE; + if (!state->IsDisplayInitialized(dpy)) { + state->SetError(EGL_NOT_INITIALIZED); + return true; + } + if (!state->ValidateConfigOnDisplay(dpy, config)) { + state->SetError(EGL_BAD_CONFIG); + return true; + } + EGLint width = 0; + EGLint height = 0; + if (!Wayland::WindowSize(window, &width, &height)) { + state->SetError(EGL_BAD_NATIVE_WINDOW); + return true; + } + EGLint alpha = 0; + (void)state->GetConfigAttrib(dpy, config, EGL_ALPHA_SIZE, &alpha); + auto presentation = + Wayland::WindowSurface::Create(reinterpret_cast(static_cast(nativeDisplay)), window, + width, height, alpha > 0); + if (!presentation) { + state->SetError(EGL_BAD_NATIVE_WINDOW); + return true; + } + const EGLSurface surface = + platformWindow ? state->CreatePlatformWindowSurface(dpy, config, window, nullptr) + : state->CreateWindowSurface( + dpy, config, (NativeWindowType)(reinterpret_cast(window)), nullptr); + if (surface == EGL_NO_SURFACE) return true; + (void)state->ResizeSurface(dpy, surface, width, height); + auto* backendObject = GetBackendObject(state); + if (!backendObject || !backendObject->CreateEGLPbufferSurface(surface, width, height)) { + MGLOG_E("eglCreateWindowSurface: the backend refused the %dx%d drawable behind a Wayland window", + width, height); + state->DestroySurface(dpy, surface); + state->SetError(EGL_BAD_ALLOC); + return true; + } + WaylandSurfaces()[surface] = std::move(presentation); + *out = surface; + return true; + } +#endif } // namespace EGLSurface CreateWindowSurface(EGLDisplay dpy, EGLConfig config, NativeWindowType window, @@ -125,6 +193,16 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { return EGL_NO_SURFACE; } +#if MOBILEGL_WAYLAND_WINDOWS + { + const std::lock_guard operationLock(EGLOperationMutex()); + EGLSurface waylandSurface = EGL_NO_SURFACE; + if (CreateWaylandWindowSurface(state, dpy, config, ToVoidHandle(window), /*platformWindow=*/false, + &waylandSurface)) { + return waylandSurface; + } + } +#endif if (!state->IsDisplayInitialized(dpy)) { state->SetError(EGL_NOT_INITIALIZED); return EGL_NO_SURFACE; @@ -221,6 +299,11 @@ namespace MobileGL::MG_Impl::EGLImpl { MGLOG_E_ONCE("activeBackendObject not initialized!"); return EGL_FALSE; } +#if MOBILEGL_WAYLAND_WINDOWS + // A Wayland window shows what is attached to it: the frame is read back and attached + // BEFORE the swap, while it is still the drawable's content. + if (auto it = WaylandSurfaces().find(draw); it != WaylandSurfaces().end()) (void)it->second->Present(); +#endif if (!backendObject->SwapEGLBuffers(dpy, draw)) { MGLOG_E_ONCE("eglSwapBuffers failed on thread=%s dpy=%p draw=%p", CurrentThreadIdString().c_str(), dpy, draw); state->SetError(EGL_BAD_SURFACE); @@ -356,6 +439,9 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state->DestroySurface(dpy, surface)) { return EGL_FALSE; } +#if MOBILEGL_WAYLAND_WINDOWS + WaylandSurfaces().erase(surface); +#endif if (auto* backendObject = MG_Backend::pActiveBackendObject.get()) { backendObject->ReleaseEGLSurface(surface); } @@ -718,6 +804,16 @@ namespace MobileGL::MG_Impl::EGLImpl { if (!state) { return EGL_NO_SURFACE; } +#if MOBILEGL_WAYLAND_WINDOWS + { + const std::lock_guard operationLock(EGLOperationMutex()); + EGLSurface waylandSurface = EGL_NO_SURFACE; + if (CreateWaylandWindowSurface(state, dpy, config, native_window, /*platformWindow=*/true, + &waylandSurface)) { + return waylandSurface; + } + } +#endif #if MOBILEGL_BUILD_DISAGGREGATED // P12 (on-screen server window), D1: as CreateWindowSurface - with MOBILEGL_IPC_SURFACE=server // the window is the server's, and a headless client's NULL is accepted. diff --git a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h index d60b640a..6709d854 100644 --- a/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h +++ b/MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h @@ -1,75 +1,22 @@ -// MobileGL - MobileGL/MG_Impl/EGLImpl/EGLPlatformExtensions.h -// Copyright (c) 2025-2026 MobileGL-Dev -// Licensed under the GNU Lesser General Public License v3.0: -// https://www.gnu.org/licenses/gpl-3.0.txt -// https://www.gnu.org/licenses/lgpl-3.0.txt // SPDX-License-Identifier: LGPL-3.0-only -// End of Source File Header - #pragma once namespace MobileGL::MG_Impl::EGLImpl { - // THE PLATFORMS THIS IMPLEMENTATION SERVES, in the words EGL spells them. - // - // Two callers need exactly the same list, and they disagree if it is written twice: - // eglQueryString(EGL_EXTENSIONS) is what an application reads BEFORE it decides to call - // eglGetPlatformDisplay at all - libepoxy refuses to use eglGetPlatformDisplayEXT unless the - // string names EGL_EXT_platform_base - and the glvnd vendor string is what the system's - // libEGL reads to decide which vendor library a platform request should be routed to. A - // vendor that does not claim the platform is never asked for it, and the caller is told there - // is no provider: the library can be loaded, initialized, and still never called. - // - // GBM is on the list because a compositor asks for its display on the GBM device it owns. - // This side treats that device the way it treats every other native display - the frames come - // from the display host, not from the device - so the claim is exact rather than optimistic. - inline constexpr const char* kDeviceExtensionString = - "EGL_EXT_device_base " - "EGL_EXT_device_enumeration " - "EGL_EXT_device_query " - "EGL_EXT_device_drm " - "EGL_EXT_device_drm_render_node"; - + // Keep libglvnd routing and direct queries consistent. Device/GBM extensions + // are absent until their entry points and image paths exist. + inline constexpr const char* kDeviceExtensionString = ""; +#if defined(__linux__) && !defined(__ANDROID__) +#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS " EGL_KHR_platform_wayland EGL_EXT_platform_wayland" +#else +#define MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS "" +#endif inline constexpr const char* kPlatformExtensionString = - "EGL_EXT_platform_base " - "EGL_KHR_platform_base " - "EGL_KHR_platform_gbm " - "EGL_EXT_platform_gbm " - "EGL_MESA_platform_gbm " - "EGL_EXT_platform_device " - "EGL_MESA_platform_surfaceless"; - - // What a display answers for EGL_EXTENSIONS: the extensions this implementation provides that - // are not platform ones, then the platform list above. + "EGL_EXT_client_extensions EGL_EXT_platform_base EGL_KHR_platform_base " + "EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS; inline constexpr const char* kDisplayExtensionString = - "EGL_KHR_create_context " - "EGL_EXT_device_base " - "EGL_EXT_device_enumeration " - "EGL_EXT_device_query " - "EGL_EXT_device_drm " - "EGL_EXT_device_drm_render_node " - "EGL_EXT_platform_device " - "EGL_EXT_platform_base " - "EGL_KHR_platform_base " - "EGL_KHR_platform_gbm " - "EGL_EXT_platform_gbm " - "EGL_MESA_platform_gbm " - "EGL_MESA_platform_surfaceless"; - - // ... and what EGL_NO_DISPLAY answers: the same, plus the client-extension marker that tells a - // caller the no-display query is meaningful here. + "EGL_KHR_create_context EGL_EXT_platform_base EGL_KHR_platform_base " + "EGL_MESA_platform_surfaceless" MOBILEGL_PLATFORM_WAYLAND_EXTENSIONS; inline constexpr const char* kClientExtensionString = - "EGL_EXT_client_extensions " - "EGL_KHR_create_context " - "EGL_EXT_device_base " - "EGL_EXT_device_enumeration " - "EGL_EXT_device_query " - "EGL_EXT_device_drm " - "EGL_EXT_device_drm_render_node " - "EGL_EXT_platform_device " - "EGL_EXT_platform_base " - "EGL_KHR_platform_base " - "EGL_KHR_platform_gbm " - "EGL_EXT_platform_gbm " - "EGL_MESA_platform_gbm " - "EGL_MESA_platform_surfaceless"; + "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/WaylandWindow.cpp b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp new file mode 100644 index 00000000..29e029ba --- /dev/null +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp @@ -0,0 +1,398 @@ +// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "WaylandWindow.h" + +#if MOBILEGL_WAYLAND_WINDOWS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace MobileGL::MG_Impl::EGLImpl::Wayland { + namespace { + // libwayland-client's types are opaque here: everything goes through wl_proxy_* and the + // exported interface descriptions, and nothing reads a field of either. + struct wl_proxy; + struct wl_interface; + struct wl_event_queue; + + // wayland-egl-backend.h, WL_EGL_WINDOW_VERSION 3: the struct libwayland-egl allocates for + // wl_egl_window_create and the driver reads. The ABI is versioned by its first word. + struct EglWindow { + const intptr_t version; + int width; + int height; + int dx; + int dy; + int attached_width; + int attached_height; + void* driver_private; + void (*resize_callback)(EglWindow*, void*); + void (*destroy_window_callback)(void*); + wl_proxy* surface; + }; + constexpr intptr_t kEglWindowVersion = 3; + + // Request opcodes (wayland.xml, the order the requests are declared in). + constexpr uint32_t kDisplayGetRegistry = 1; + constexpr uint32_t kRegistryBind = 0; + constexpr uint32_t kShmCreatePool = 0; + constexpr uint32_t kShmPoolCreateBuffer = 0; + constexpr uint32_t kShmPoolDestroy = 1; + constexpr uint32_t kBufferDestroy = 0; + constexpr uint32_t kSurfaceAttach = 1; + constexpr uint32_t kSurfaceDamage = 2; + constexpr uint32_t kSurfaceCommit = 6; + constexpr uint32_t kSurfaceDamageBuffer = 9; // since wl_surface version 4 + constexpr uint32_t kMarshalFlagDestroy = 1u << 0; + // wl_shm formats. The two every compositor must support; byte order in memory is + // B, G, R, A for both (little-endian 0xAARRGGBB). + constexpr uint32_t kShmFormatArgb8888 = 0; + constexpr uint32_t kShmFormatXrgb8888 = 1; + + struct Api { + Bool loaded = false; + const wl_interface* displayInterface = nullptr; + const wl_interface* registryInterface = nullptr; + const wl_interface* shmInterface = nullptr; + const wl_interface* shmPoolInterface = nullptr; + const wl_interface* bufferInterface = nullptr; + wl_proxy* (*marshalFlags)(wl_proxy*, uint32_t, const wl_interface*, uint32_t, uint32_t, ...) = nullptr; + int (*addListener)(wl_proxy*, void (**)(void), void*) = nullptr; + void (*destroy)(wl_proxy*) = nullptr; + uint32_t (*getVersion)(wl_proxy*) = nullptr; + void* (*createWrapper)(void*) = nullptr; + void (*wrapperDestroy)(void*) = nullptr; + void (*setQueue)(wl_proxy*, wl_event_queue*) = nullptr; + wl_event_queue* (*createQueue)(void*) = nullptr; + void (*queueDestroy)(wl_event_queue*) = nullptr; + int (*roundtripQueue)(void*, wl_event_queue*) = nullptr; + int (*dispatchQueue)(void*, wl_event_queue*) = nullptr; + int (*dispatchQueuePending)(void*, wl_event_queue*) = nullptr; + int (*flush)(void*) = nullptr; + }; + + // Loaded once. RTLD_NOLOAD first: a Wayland client has the library already, and the copy + // it is using is the one whose wl_display this must talk to. + const Api& WaylandApi() { + static Api api; + static std::once_flag once; + std::call_once(once, [] { + void* lib = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_NOLOAD); + if (lib == nullptr) lib = dlopen("libwayland-client.so.0", RTLD_NOW); + if (lib == nullptr) return; + const auto sym = [lib](const char* name) { return dlsym(lib, name); }; + api.displayInterface = static_cast(sym("wl_display_interface")); + api.registryInterface = static_cast(sym("wl_registry_interface")); + api.shmInterface = static_cast(sym("wl_shm_interface")); + api.shmPoolInterface = static_cast(sym("wl_shm_pool_interface")); + api.bufferInterface = static_cast(sym("wl_buffer_interface")); + api.marshalFlags = reinterpret_cast(sym("wl_proxy_marshal_flags")); + api.addListener = reinterpret_cast(sym("wl_proxy_add_listener")); + api.destroy = reinterpret_cast(sym("wl_proxy_destroy")); + api.getVersion = reinterpret_cast(sym("wl_proxy_get_version")); + api.createWrapper = reinterpret_cast(sym("wl_proxy_create_wrapper")); + api.wrapperDestroy = reinterpret_cast(sym("wl_proxy_wrapper_destroy")); + api.setQueue = reinterpret_cast(sym("wl_proxy_set_queue")); + api.createQueue = reinterpret_cast(sym("wl_display_create_queue")); + api.queueDestroy = reinterpret_cast(sym("wl_event_queue_destroy")); + api.roundtripQueue = reinterpret_cast(sym("wl_display_roundtrip_queue")); + api.dispatchQueue = reinterpret_cast(sym("wl_display_dispatch_queue")); + api.dispatchQueuePending = + reinterpret_cast(sym("wl_display_dispatch_queue_pending")); + api.flush = reinterpret_cast(sym("wl_display_flush")); + api.loaded = api.displayInterface && api.registryInterface && api.shmInterface && + api.shmPoolInterface && api.bufferInterface && api.marshalFlags && api.addListener && api.destroy && + api.getVersion && api.createWrapper && api.wrapperDestroy && api.setQueue && + api.createQueue && api.queueDestroy && api.roundtripQueue && api.dispatchQueue && + api.dispatchQueuePending && api.flush; + if (!api.loaded) MGLOG_E("Wayland: libwayland-client.so.0 lacks an entry point this library uses"); + }); + return api; + } + + struct ShmBuffer { + wl_proxy* buffer = nullptr; + void* map = nullptr; + SizeT size = 0; + EGLint width = 0; + EGLint height = 0; + EGLint stride = 0; + Bool busy = false; + }; + + // The compositor is done with a buffer: it may be drawn into again. + void OnBufferRelease(void* data, wl_proxy*) { static_cast(data)->busy = false; } + void (*const kBufferListener[])(void) = {reinterpret_cast(&OnBufferRelease)}; + } // namespace + + struct WindowSurface::Impl { + void* display = nullptr; + EglWindow* window = nullptr; + wl_event_queue* queue = nullptr; + void* displayWrapper = nullptr; + wl_proxy* registry = nullptr; + wl_proxy* shm = nullptr; + uint32_t format = kShmFormatXrgb8888; + EGLint width = 0; + EGLint height = 0; + // Three: one on the glass, one the compositor may still be reading, one to draw into. + ShmBuffer buffers[3]; + Vector scratch; + + ~Impl() { + const Api& api = WaylandApi(); + for (auto& buffer : buffers) Release(buffer); + // Neither wl_shm (version 1) nor wl_registry has a destroy request: the proxies go + // on this side only. + if (shm) api.destroy(shm); + if (registry) api.destroy(registry); + if (displayWrapper) api.wrapperDestroy(displayWrapper); + if (queue) api.queueDestroy(queue); + if (window && window->driver_private == this) { + window->driver_private = nullptr; + window->destroy_window_callback = nullptr; + } + } + + void Release(ShmBuffer& buffer) { + const Api& api = WaylandApi(); + if (buffer.buffer) api.marshalFlags(buffer.buffer, kBufferDestroy, nullptr, api.getVersion(buffer.buffer), + kMarshalFlagDestroy); + if (buffer.map) munmap(buffer.map, buffer.size); + buffer = ShmBuffer{}; + } + + Bool Allocate(ShmBuffer& buffer) { + const Api& api = WaylandApi(); + Release(buffer); + const EGLint stride = width * 4; + const SizeT size = static_cast(stride) * static_cast(height); + const int fd = memfd_create("mobilegl-wayland", MFD_CLOEXEC); + if (fd < 0) { + MGLOG_E("Wayland: memfd_create for a %dx%d buffer failed: %s", width, height, std::strerror(errno)); + return false; + } + if (ftruncate(fd, static_cast(size)) != 0) { + MGLOG_E("Wayland: sizing a %dx%d buffer failed: %s", width, height, std::strerror(errno)); + close(fd); + return false; + } + void* map = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (map == MAP_FAILED) { + MGLOG_E("Wayland: mapping a %dx%d buffer failed: %s", width, height, std::strerror(errno)); + close(fd); + return false; + } + // One pool per buffer, destroyed at once: the buffer keeps the memory alive. + wl_proxy* pool = api.marshalFlags(shm, kShmCreatePool, api.shmPoolInterface, api.getVersion(shm), 0, nullptr, + fd, static_cast(size)); + close(fd); + if (pool == nullptr) { + munmap(map, size); + return false; + } + wl_proxy* wlBuffer = api.marshalFlags(pool, kShmPoolCreateBuffer, api.bufferInterface, api.getVersion(pool), + 0, nullptr, 0, width, height, stride, format); + api.marshalFlags(pool, kShmPoolDestroy, nullptr, api.getVersion(pool), kMarshalFlagDestroy); + if (wlBuffer == nullptr) { + munmap(map, size); + return false; + } + buffer.buffer = wlBuffer; + buffer.map = map; + buffer.size = size; + buffer.width = width; + buffer.height = height; + buffer.stride = stride; + buffer.busy = false; + api.addListener(wlBuffer, const_cast(kBufferListener), &buffer); + return true; + } + + // A buffer the compositor is not reading: the events already received first, then (all + // three still held) a blocking wait for the next release. + ShmBuffer* NextFree() { + const Api& api = WaylandApi(); + for (;;) { + if (api.dispatchQueuePending(display, queue) < 0) return nullptr; + for (auto& buffer : buffers) { + if (buffer.buffer == nullptr || buffer.busy) continue; + if (buffer.width != width || buffer.height != height) { + if (!Allocate(buffer)) return nullptr; + } + return &buffer; + } + for (auto& buffer : buffers) { + if (buffer.buffer == nullptr) return Allocate(buffer) ? &buffer : nullptr; + } + if (api.dispatchQueue(display, queue) < 0) return nullptr; + } + } + }; + + namespace { + void OnRegistryGlobal(void* data, wl_proxy* registry, uint32_t name, const char* interface, uint32_t) { + auto* impl = static_cast(data); + if (impl->shm != nullptr || std::strcmp(interface, "wl_shm") != 0) return; + const Api& api = WaylandApi(); + impl->shm = api.marshalFlags(registry, kRegistryBind, api.shmInterface, 1, 0, name, "wl_shm", 1u, nullptr); + } + void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {} + void (*const kRegistryListener[])(void) = {reinterpret_cast(&OnRegistryGlobal), + reinterpret_cast(&OnRegistryGlobalRemove)}; + + // The application destroyed its wl_egl_window first: nothing may be read from it after this. + void OnWindowDestroyed(void* data) { static_cast(data)->window = nullptr; } + } // namespace + + Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform) { + if (platform == kPlatformWayland) return true; + if (nativeDisplay == 0 || (platform != EGL_NONE && platform != 0)) return false; + const Api& api = WaylandApi(); + if (!api.loaded) return false; + return *reinterpret_cast(static_cast(nativeDisplay)) == api.displayInterface; + } + + Bool IsWaylandWindow(const void* window) { + if (window == nullptr) return false; + const auto* eglWindow = static_cast(window); + return eglWindow->version >= kEglWindowVersion && eglWindow->surface != nullptr; + } + + Bool WindowSize(const void* window, EGLint* width, EGLint* height) { + if (!IsWaylandWindow(window)) return false; + const auto* eglWindow = static_cast(window); + *width = eglWindow->width; + *height = eglWindow->height; + return eglWindow->width > 0 && eglWindow->height > 0; + } + + WindowSurface::WindowSurface(UniquePtr impl) : m_impl(std::move(impl)) {} + WindowSurface::~WindowSurface() = default; + + UniquePtr WindowSurface::Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height, + Bool hasAlpha) { + const Api& api = WaylandApi(); + if (!api.loaded) { + MGLOG_E("Wayland: a window surface needs libwayland-client.so.0, and it could not be used"); + return nullptr; + } + if (wlDisplay == nullptr || !IsWaylandWindow(wlEglWindow) || width <= 0 || height <= 0) return nullptr; + auto impl = MakeUnique(); + impl->display = wlDisplay; + impl->window = static_cast(wlEglWindow); + impl->width = width; + impl->height = height; + impl->format = hasAlpha ? kShmFormatArgb8888 : kShmFormatXrgb8888; + // A queue of this surface's own, so the application's dispatching never sees these events + // and this never dispatches the application's. + impl->queue = api.createQueue(wlDisplay); + impl->displayWrapper = api.createWrapper(wlDisplay); + if (impl->queue == nullptr || impl->displayWrapper == nullptr) return nullptr; + api.setQueue(static_cast(impl->displayWrapper), impl->queue); + auto* wrapper = static_cast(impl->displayWrapper); + impl->registry = api.marshalFlags(wrapper, kDisplayGetRegistry, api.registryInterface, api.getVersion(wrapper), + 0, nullptr); + if (impl->registry == nullptr) return nullptr; + api.addListener(impl->registry, const_cast(kRegistryListener), impl.get()); + if (api.roundtripQueue(wlDisplay, impl->queue) < 0 || impl->shm == nullptr) { + MGLOG_E("Wayland: the compositor offers no wl_shm; a window surface cannot be presented"); + return nullptr; + } + impl->window->driver_private = impl.get(); + impl->window->destroy_window_callback = &OnWindowDestroyed; + MGLOG_I("Wayland: window surface %dx%d presented through wl_shm (%s)", width, height, + hasAlpha ? "ARGB8888" : "XRGB8888"); + return UniquePtr(new WindowSurface(std::move(impl))); + } + + Bool WindowSurface::Present() { + Impl& impl = *m_impl; + if (impl.window == nullptr) return false; + const Api& api = WaylandApi(); + ShmBuffer* buffer = impl.NextFree(); + if (buffer == nullptr) { + MGLOG_E_ONCE("Wayland: no wl_shm buffer could be taken for the frame; it is not shown"); + return false; + } + + // The frame, out of the default framebuffer, through this library's own entry points: the + // application's read framebuffer, pack buffer and pack state are saved and put back, so + // the readback is invisible to it. + namespace GL = MG_Impl::GLImpl; + GLint readFramebuffer = 0, packBuffer = 0, alignment = 4, rowLength = 0, skipRows = 0, skipPixels = 0; + GL::GetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFramebuffer); + GL::GetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &packBuffer); + GL::GetIntegerv(GL_PACK_ALIGNMENT, &alignment); + GL::GetIntegerv(GL_PACK_ROW_LENGTH, &rowLength); + GL::GetIntegerv(GL_PACK_SKIP_ROWS, &skipRows); + GL::GetIntegerv(GL_PACK_SKIP_PIXELS, &skipPixels); + if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, 0); + if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, 0); + GL::PixelStorei(GL_PACK_ALIGNMENT, 4); + GL::PixelStorei(GL_PACK_ROW_LENGTH, 0); + GL::PixelStorei(GL_PACK_SKIP_ROWS, 0); + GL::PixelStorei(GL_PACK_SKIP_PIXELS, 0); + const SizeT rowBytes = static_cast(impl.width) * 4; + impl.scratch.resize(rowBytes * static_cast(impl.height)); + GL::ReadPixels(0, 0, impl.width, impl.height, GL_RGBA, GL_UNSIGNED_BYTE, impl.scratch.data()); + GL::PixelStorei(GL_PACK_ALIGNMENT, alignment); + GL::PixelStorei(GL_PACK_ROW_LENGTH, rowLength); + GL::PixelStorei(GL_PACK_SKIP_ROWS, skipRows); + GL::PixelStorei(GL_PACK_SKIP_PIXELS, skipPixels); + if (packBuffer != 0) GL::BindBuffer(GL_PIXEL_PACK_BUFFER, static_cast(packBuffer)); + if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, static_cast(readFramebuffer)); + + // GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A. + auto* dst = static_cast(buffer->map); + for (EGLint y = 0; y < impl.height; ++y) { + const Uint8* src = impl.scratch.data() + static_cast(impl.height - 1 - y) * rowBytes; + Uint8* row = dst + static_cast(y) * static_cast(buffer->stride); + for (EGLint x = 0; x < impl.width; ++x) { + row[x * 4 + 0] = src[x * 4 + 2]; + row[x * 4 + 1] = src[x * 4 + 1]; + row[x * 4 + 2] = src[x * 4 + 0]; + row[x * 4 + 3] = src[x * 4 + 3]; + } + } + + wl_proxy* surface = impl.window->surface; + const uint32_t surfaceVersion = api.getVersion(surface); + api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, buffer->buffer, impl.window->dx, + impl.window->dy); + if (surfaceVersion >= 4) { + api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, impl.width, + impl.height); + } else { + api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, impl.width, impl.height); + } + api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0); + buffer->busy = true; + impl.window->attached_width = impl.width; + impl.window->attached_height = impl.height; + impl.window->dx = 0; + impl.window->dy = 0; + api.flush(impl.display); + return true; + } +} // namespace MobileGL::MG_Impl::EGLImpl::Wayland + +#endif diff --git a/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h new file mode 100644 index 00000000..41b02819 --- /dev/null +++ b/MobileGL/MG_Impl/EGLImpl/WaylandWindow.h @@ -0,0 +1,72 @@ +// MobileGL - MobileGL/MG_Impl/EGLImpl/WaylandWindow.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once + +// WAYLAND WINDOW SURFACES. A Wayland client hands eglCreateWindowSurface a wl_egl_window, and what +// the compositor shows is whatever wl_buffer the client attaches to that window's wl_surface. The +// pixels of a MobileGL frame are drawn where the backend is - on a split build, in another process +// on another OS - so a window surface here is a pbuffer the backend draws into, plus this: at every +// swap the frame is read back and attached to the wl_surface as a wl_shm buffer. +// +// libwayland-client is loaded at run time (it is already in every Wayland client's process), so the +// library neither links it nor needs its headers to build. + +#include + +#if defined(__linux__) && !defined(__ANDROID__) +#define MOBILEGL_WAYLAND_WINDOWS 1 +#else +#define MOBILEGL_WAYLAND_WINDOWS 0 +#endif + +#if MOBILEGL_WAYLAND_WINDOWS + +#include + +namespace MobileGL::MG_Impl::EGLImpl::Wayland { + // EGL_PLATFORM_WAYLAND_KHR and EGL_PLATFORM_WAYLAND_EXT are the same enum. + inline constexpr EGLenum kPlatformWayland = 0x31D8; + + // Whether a display is a Wayland one: asked for as EGL_PLATFORM_WAYLAND, or (eglGetDisplay with + // no platform) a native display that is a wl_display - recognised the way Mesa does, by its + // first word being libwayland-client's wl_display_interface. + Bool IsWaylandDisplay(Uint64 nativeDisplay, EGLenum platform); + + // Whether `window` looks like a wl_egl_window this library can present to. + Bool IsWaylandWindow(const void* window); + + // The size the application last gave its wl_egl_window. + Bool WindowSize(const void* window, EGLint* width, EGLint* height); + + // One window surface's presentation: the shm buffers, the event queue they are dispatched on, + // and the readback scratch. Created for a wl_egl_window on a wl_display; null with the reason + // logged when libwayland-client or the compositor's wl_shm is not there. + class WindowSurface { + public: + static UniquePtr Create(void* wlDisplay, void* wlEglWindow, EGLint width, EGLint height, + Bool hasAlpha); + ~WindowSurface(); + WindowSurface(const WindowSurface&) = delete; + WindowSurface& operator=(const WindowSurface&) = delete; + + // Reads the frame back out of the current context's default framebuffer and puts it on + // the window: attach, damage, commit. The application's read-framebuffer and pack state + // are put back exactly as they were. False (logged) when the frame did not reach the + // window; the swap itself goes on regardless. + Bool Present(); + + struct Impl; + + private: + explicit WindowSurface(UniquePtr impl); + UniquePtr m_impl; + }; +} // namespace MobileGL::MG_Impl::EGLImpl::Wayland + +#endif diff --git a/MobileGL/MG_State/EGLState/Core.cpp b/MobileGL/MG_State/EGLState/Core.cpp index 133d21b8..5e4d01d5 100644 --- a/MobileGL/MG_State/EGLState/Core.cpp +++ b/MobileGL/MG_State/EGLState/Core.cpp @@ -334,6 +334,16 @@ namespace MobileGL { return displayObject && displayObject->Initialized; } + Bool EGLContext::GetDisplayNative(EGLDisplayHandle display, Uint64* nativeDisplayKey, + EGLenum* platform) const { + const std::lock_guard lock(m_mutex); + const auto* displayObject = TryGetDisplay(display); + if (!displayObject) return false; + *nativeDisplayKey = displayObject->NativeDisplayKey; + *platform = displayObject->Platform; + return true; + } + Bool EGLContext::InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor) { const std::lock_guard lock(m_mutex); auto* displayObject = TryGetDisplay(display); diff --git a/MobileGL/MG_State/EGLState/Core.h b/MobileGL/MG_State/EGLState/Core.h index 3c4b24ba..9af78056 100644 --- a/MobileGL/MG_State/EGLState/Core.h +++ b/MobileGL/MG_State/EGLState/Core.h @@ -37,6 +37,9 @@ namespace MobileGL { EGLDisplayHandle GetPlatformDisplay(EGLenum platform, void* nativeDisplay); Bool ValidateDisplay(EGLDisplayHandle display) const; Bool IsDisplayInitialized(EGLDisplayHandle display) const; + // The native display a display was made for and the platform it was asked for on + // (EGL_NONE for eglGetDisplay): what decides how its window surfaces are presented. + Bool GetDisplayNative(EGLDisplayHandle display, Uint64* nativeDisplayKey, EGLenum* platform) const; Bool InitializeDisplay(EGLDisplayHandle display, EGLint* major, EGLint* minor); Bool TerminateDisplay(EGLDisplayHandle display); // Whole-library idle checks used by EGLImpl::Terminate to decide