mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-15 05:53:23 +09:00
413 lines
20 KiB
C++
413 lines
20 KiB
C++
// 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 <MG_Impl/GLImpl/Buffer/GL_Buffer.h>
|
|
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
|
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
|
|
#include <MG_Impl/GLImpl/RenderState/GL_RenderState.h>
|
|
#include <MG_Util/Debug/Log.h>
|
|
#include <cerrno>
|
|
#include <cstdarg>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <dlfcn.h>
|
|
#include <fcntl.h>
|
|
#include <mutex>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
namespace MobileGL::MG_Impl::EGLImpl {
|
|
void ReadBackFrameBGRA(EGLint width, EGLint height, Vector<Uint8>& scratch, Uint8* dst, SizeT dstStride) {
|
|
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<SizeT>(width) * 4;
|
|
scratch.resize(rowBytes * static_cast<SizeT>(height));
|
|
GL::ReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 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<GLuint>(packBuffer));
|
|
if (readFramebuffer != 0) GL::BindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readFramebuffer));
|
|
|
|
for (EGLint y = 0; y < height; ++y) {
|
|
const Uint8* src = scratch.data() + static_cast<SizeT>(height - 1 - y) * rowBytes;
|
|
Uint8* row = dst + static_cast<SizeT>(y) * dstStride;
|
|
for (EGLint x = 0; x < 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];
|
|
}
|
|
}
|
|
}
|
|
} // 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.
|
|
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<const wl_interface*>(sym("wl_display_interface"));
|
|
api.registryInterface = static_cast<const wl_interface*>(sym("wl_registry_interface"));
|
|
api.shmInterface = static_cast<const wl_interface*>(sym("wl_shm_interface"));
|
|
api.shmPoolInterface = static_cast<const wl_interface*>(sym("wl_shm_pool_interface"));
|
|
api.bufferInterface = static_cast<const wl_interface*>(sym("wl_buffer_interface"));
|
|
api.marshalFlags = reinterpret_cast<decltype(api.marshalFlags)>(sym("wl_proxy_marshal_flags"));
|
|
api.addListener = reinterpret_cast<decltype(api.addListener)>(sym("wl_proxy_add_listener"));
|
|
api.destroy = reinterpret_cast<decltype(api.destroy)>(sym("wl_proxy_destroy"));
|
|
api.getVersion = reinterpret_cast<decltype(api.getVersion)>(sym("wl_proxy_get_version"));
|
|
api.createWrapper = reinterpret_cast<decltype(api.createWrapper)>(sym("wl_proxy_create_wrapper"));
|
|
api.wrapperDestroy = reinterpret_cast<decltype(api.wrapperDestroy)>(sym("wl_proxy_wrapper_destroy"));
|
|
api.setQueue = reinterpret_cast<decltype(api.setQueue)>(sym("wl_proxy_set_queue"));
|
|
api.createQueue = reinterpret_cast<decltype(api.createQueue)>(sym("wl_display_create_queue"));
|
|
api.queueDestroy = reinterpret_cast<decltype(api.queueDestroy)>(sym("wl_event_queue_destroy"));
|
|
api.roundtripQueue = reinterpret_cast<decltype(api.roundtripQueue)>(sym("wl_display_roundtrip_queue"));
|
|
api.dispatchQueue = reinterpret_cast<decltype(api.dispatchQueue)>(sym("wl_display_dispatch_queue"));
|
|
api.dispatchQueuePending =
|
|
reinterpret_cast<decltype(api.dispatchQueuePending)>(sym("wl_display_dispatch_queue_pending"));
|
|
api.flush = reinterpret_cast<decltype(api.flush)>(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<ShmBuffer*>(data)->busy = false; }
|
|
void (*const kBufferListener[])(void) = {reinterpret_cast<void (*)(void)>(&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<Uint8> 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<SizeT>(stride) * static_cast<SizeT>(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<off_t>(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<int32_t>(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<void (**)(void)>(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<WindowSurface::Impl*>(data);
|
|
if (impl->shm != nullptr || std::strcmp(interface, "wl_shm") != 0) return;
|
|
const Api& api = WaylandApi();
|
|
impl->shm = api.marshalFlags(registry, kRegistryBind, api.shmInterface, 1, 0, name, "wl_shm", 1u, nullptr);
|
|
}
|
|
void OnRegistryGlobalRemove(void*, wl_proxy*, uint32_t) {}
|
|
void (*const kRegistryListener[])(void) = {reinterpret_cast<void (*)(void)>(&OnRegistryGlobal),
|
|
reinterpret_cast<void (*)(void)>(&OnRegistryGlobalRemove)};
|
|
|
|
// The application destroyed its wl_egl_window first: nothing may be read from it after this.
|
|
void OnWindowDestroyed(void* data) { static_cast<WindowSurface::Impl*>(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<const void* const*>(static_cast<uintptr_t>(nativeDisplay)) == api.displayInterface;
|
|
}
|
|
|
|
Bool IsWaylandWindow(const void* window) {
|
|
if (window == nullptr) return false;
|
|
const auto* eglWindow = static_cast<const EglWindow*>(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<const EglWindow*>(window);
|
|
*width = eglWindow->width;
|
|
*height = eglWindow->height;
|
|
return eglWindow->width > 0 && eglWindow->height > 0;
|
|
}
|
|
|
|
WindowSurface::WindowSurface(UniquePtr<Impl> impl) : m_impl(std::move(impl)) {}
|
|
WindowSurface::~WindowSurface() = default;
|
|
|
|
UniquePtr<WindowSurface> 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>();
|
|
impl->display = wlDisplay;
|
|
impl->window = static_cast<EglWindow*>(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<wl_proxy*>(impl->displayWrapper), impl->queue);
|
|
auto* wrapper = static_cast<wl_proxy*>(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<void (**)(void)>(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<WindowSurface>(new WindowSurface(std::move(impl)));
|
|
}
|
|
|
|
Bool WindowSurface::TakeResize(EGLint* width, EGLint* height) {
|
|
Impl& impl = *m_impl;
|
|
if (impl.window == nullptr || impl.window->width <= 0 || impl.window->height <= 0) return false;
|
|
if (impl.window->width == impl.width && impl.window->height == impl.height) return false;
|
|
impl.width = impl.window->width;
|
|
impl.height = impl.window->height;
|
|
*width = impl.width;
|
|
*height = impl.height;
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// GL's rows run bottom-up and RGBA; a wl_shm buffer's run top-down and B, G, R, A.
|
|
ReadBackFrameBGRA(impl.width, impl.height, impl.scratch, static_cast<Uint8*>(buffer->map),
|
|
static_cast<SizeT>(buffer->stride));
|
|
|
|
wl_proxy* surface = impl.window->surface;
|
|
const uint32_t surfaceVersion = api.getVersion(surface);
|
|
api.marshalFlags(surface, kSurfaceAttach, nullptr, surfaceVersion, 0, buffer->buffer, impl.window->dx,
|
|
impl.window->dy);
|
|
if (surfaceVersion >= 4) {
|
|
api.marshalFlags(surface, kSurfaceDamageBuffer, nullptr, surfaceVersion, 0, 0, 0, impl.width,
|
|
impl.height);
|
|
} else {
|
|
api.marshalFlags(surface, kSurfaceDamage, nullptr, surfaceVersion, 0, 0, 0, impl.width, impl.height);
|
|
}
|
|
api.marshalFlags(surface, kSurfaceCommit, nullptr, surfaceVersion, 0);
|
|
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
|