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