mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
1113 lines
41 KiB
C++
1113 lines
41 KiB
C++
// MobileGL - MobileGL/MG_Impl/GLXImpl/GLXImpl.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 "GLXImpl.h"
|
|
|
|
#if defined(__linux__) && !defined(__ANDROID__)
|
|
#include "../EGLImpl/EGLImpl.h"
|
|
#include "../GetProcAddress.h"
|
|
#include <Init.h>
|
|
|
|
namespace MobileGL::MG_Impl::GLXImpl {
|
|
namespace {
|
|
// ---- GLX tokens (defined locally: GL/glx.h drags in a conflicting gl.h) ----
|
|
constexpr int GLX_USE_GL = 1;
|
|
constexpr int GLX_BUFFER_SIZE = 2;
|
|
constexpr int GLX_LEVEL = 3;
|
|
constexpr int GLX_RGBA = 4;
|
|
constexpr int GLX_DOUBLEBUFFER = 5;
|
|
constexpr int GLX_STEREO = 6;
|
|
constexpr int GLX_AUX_BUFFERS = 7;
|
|
constexpr int GLX_RED_SIZE = 8;
|
|
constexpr int GLX_GREEN_SIZE = 9;
|
|
constexpr int GLX_BLUE_SIZE = 10;
|
|
constexpr int GLX_ALPHA_SIZE = 11;
|
|
constexpr int GLX_DEPTH_SIZE = 12;
|
|
constexpr int GLX_STENCIL_SIZE = 13;
|
|
constexpr int GLX_ACCUM_RED_SIZE = 14;
|
|
constexpr int GLX_ACCUM_GREEN_SIZE = 15;
|
|
constexpr int GLX_ACCUM_BLUE_SIZE = 16;
|
|
constexpr int GLX_ACCUM_ALPHA_SIZE = 17;
|
|
// glXGetConfig/glXGetFBConfigAttrib error returns
|
|
constexpr int GLX_BAD_ATTRIBUTE = 2;
|
|
// glXGetClientString/glXQueryServerString names
|
|
constexpr int GLX_VENDOR = 1;
|
|
constexpr int GLX_VERSION = 2;
|
|
constexpr int GLX_EXTENSIONS = 3;
|
|
// GLX 1.3+ FBConfig attributes
|
|
constexpr int GLX_CONFIG_CAVEAT = 0x20;
|
|
constexpr int GLX_X_VISUAL_TYPE = 0x22;
|
|
constexpr int GLX_TRANSPARENT_TYPE = 0x23;
|
|
constexpr int GLX_TRANSPARENT_INDEX_VALUE = 0x24;
|
|
constexpr int GLX_TRANSPARENT_RED_VALUE = 0x25;
|
|
constexpr int GLX_TRANSPARENT_GREEN_VALUE = 0x26;
|
|
constexpr int GLX_TRANSPARENT_BLUE_VALUE = 0x27;
|
|
constexpr int GLX_TRANSPARENT_ALPHA_VALUE = 0x28;
|
|
constexpr int GLX_DONT_CARE = static_cast<int>(0xFFFFFFFF);
|
|
constexpr int GLX_NONE = 0x8000;
|
|
constexpr int GLX_TRUE_COLOR = 0x8002;
|
|
constexpr int GLX_VISUAL_ID = 0x800B;
|
|
constexpr int GLX_SCREEN = 0x800C;
|
|
constexpr int GLX_DRAWABLE_TYPE = 0x8010;
|
|
constexpr int GLX_RENDER_TYPE = 0x8011;
|
|
constexpr int GLX_X_RENDERABLE = 0x8012;
|
|
constexpr int GLX_FBCONFIG_ID = 0x8013;
|
|
constexpr int GLX_RGBA_TYPE = 0x8014;
|
|
constexpr int GLX_MAX_PBUFFER_WIDTH = 0x8016;
|
|
constexpr int GLX_MAX_PBUFFER_HEIGHT = 0x8017;
|
|
constexpr int GLX_MAX_PBUFFER_PIXELS = 0x8018;
|
|
constexpr int GLX_WIDTH = 0x801D;
|
|
constexpr int GLX_HEIGHT = 0x801E;
|
|
constexpr int GLX_WINDOW_BIT = 0x00000001;
|
|
constexpr int GLX_RGBA_BIT = 0x00000001;
|
|
constexpr int GLX_SAMPLE_BUFFERS = 100000;
|
|
constexpr int GLX_SAMPLES = 100001;
|
|
// GLX_EXT_swap_control
|
|
constexpr int GLX_SWAP_INTERVAL_EXT = 0x20F1;
|
|
constexpr int GLX_MAX_SWAP_INTERVAL_EXT = 0x20F2;
|
|
// GLX_ARB_create_context / _profile / _no_error
|
|
constexpr int GLX_CONTEXT_MAJOR_VERSION_ARB = 0x2091;
|
|
constexpr int GLX_CONTEXT_MINOR_VERSION_ARB = 0x2092;
|
|
constexpr int GLX_CONTEXT_FLAGS_ARB = 0x2094;
|
|
constexpr int GLX_CONTEXT_PROFILE_MASK_ARB = 0x9126;
|
|
constexpr int GLX_CONTEXT_DEBUG_BIT_ARB = 0x0001;
|
|
constexpr int GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB = 0x0002;
|
|
constexpr int GLX_CONTEXT_CORE_PROFILE_BIT_ARB = 0x00000001;
|
|
constexpr int GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB = 0x00000002;
|
|
constexpr int GLX_CONTEXT_OPENGL_NO_ERROR_ARB = 0x31B3;
|
|
|
|
constexpr const char* kGLXExtensions =
|
|
"GLX_ARB_create_context GLX_ARB_create_context_no_error GLX_ARB_create_context_profile "
|
|
"GLX_ARB_get_proc_address GLX_EXT_swap_control GLX_MESA_swap_control GLX_SGI_swap_control";
|
|
|
|
// ---- Xlib access (dlopen'd at runtime, same convention as the backends;
|
|
// libX11 is never a link-time dependency of libMobileGL) ----
|
|
|
|
// ABI-stable mirror of XVisualInfo (Xutil.h is not includable here: the
|
|
// Bool/Status macros it needs were popped after the vulkan include).
|
|
struct XVisualInfoCompat {
|
|
void* visual;
|
|
VisualID visualid;
|
|
int screen;
|
|
int depth;
|
|
int c_class;
|
|
unsigned long red_mask;
|
|
unsigned long green_mask;
|
|
unsigned long blue_mask;
|
|
int colormap_size;
|
|
int bits_per_rgb;
|
|
};
|
|
constexpr long kVisualIDMask = 0x1;
|
|
constexpr long kVisualScreenMask = 0x2;
|
|
constexpr long kVisualDepthMask = 0x4;
|
|
constexpr long kVisualClassMask = 0x8;
|
|
constexpr int kTrueColor = 4;
|
|
|
|
struct X11Functions {
|
|
void* Library = nullptr;
|
|
int (*GetGeometry)(Display*, GLXDrawableHandle, GLXDrawableHandle*, int*, int*, unsigned int*,
|
|
unsigned int*, unsigned int*, unsigned int*) = nullptr;
|
|
XVisualInfoCompat* (*GetVisualInfo)(Display*, long, XVisualInfoCompat*, int*) = nullptr;
|
|
int (*GetDefaultScreen)(Display*) = nullptr;
|
|
void* (*GetDefaultVisual)(Display*, int) = nullptr;
|
|
VisualID (*VisualIDFromVisual)(void*) = nullptr;
|
|
int (*Free)(void*) = nullptr;
|
|
int (*Sync)(Display*, int) = nullptr;
|
|
|
|
Bool Valid() const {
|
|
return GetGeometry && GetVisualInfo && GetDefaultScreen && GetDefaultVisual &&
|
|
VisualIDFromVisual && Free;
|
|
}
|
|
};
|
|
|
|
const X11Functions& X11() {
|
|
static const X11Functions* functions = [] {
|
|
auto* fns = new X11Functions();
|
|
for (const char* name : {"libX11.so.6", "libX11.so"}) {
|
|
fns->Library = dlopen(name, RTLD_LOCAL | RTLD_NOW);
|
|
if (fns->Library) {
|
|
break;
|
|
}
|
|
}
|
|
if (fns->Library) {
|
|
fns->GetGeometry = reinterpret_cast<decltype(fns->GetGeometry)>(
|
|
dlsym(fns->Library, "XGetGeometry"));
|
|
fns->GetVisualInfo = reinterpret_cast<decltype(fns->GetVisualInfo)>(
|
|
dlsym(fns->Library, "XGetVisualInfo"));
|
|
fns->GetDefaultScreen = reinterpret_cast<decltype(fns->GetDefaultScreen)>(
|
|
dlsym(fns->Library, "XDefaultScreen"));
|
|
fns->GetDefaultVisual = reinterpret_cast<decltype(fns->GetDefaultVisual)>(
|
|
dlsym(fns->Library, "XDefaultVisual"));
|
|
fns->VisualIDFromVisual = reinterpret_cast<decltype(fns->VisualIDFromVisual)>(
|
|
dlsym(fns->Library, "XVisualIDFromVisual"));
|
|
fns->Free = reinterpret_cast<decltype(fns->Free)>(dlsym(fns->Library, "XFree"));
|
|
fns->Sync = reinterpret_cast<decltype(fns->Sync)>(dlsym(fns->Library, "XSync"));
|
|
}
|
|
if (!fns->Valid()) {
|
|
MGLOG_E_ONCE("glx: failed to load libX11 entry points");
|
|
}
|
|
return fns;
|
|
}();
|
|
return *functions;
|
|
}
|
|
|
|
// ---- FBConfigs: mirror the two EGLState configs, stencil-8 first so
|
|
// stencil-wanting choosers (GLFW's default hints) match config 1 ----
|
|
struct FBConfigInfo {
|
|
int FBConfigId;
|
|
GLint StencilBits;
|
|
};
|
|
constexpr FBConfigInfo kFBConfigs[] = {
|
|
{1, 8},
|
|
{2, 0},
|
|
};
|
|
constexpr int kFBConfigCount = static_cast<int>(std::size(kFBConfigs));
|
|
|
|
const FBConfigInfo* TryGetFBConfig(GLXFBConfigHandle config) {
|
|
const auto* info = static_cast<const FBConfigInfo*>(config);
|
|
return (info >= kFBConfigs && info < kFBConfigs + kFBConfigCount) ? info : nullptr;
|
|
}
|
|
|
|
struct ContextObject {
|
|
Display* XDisplay = nullptr;
|
|
EGLDisplay Display = EGL_NO_DISPLAY;
|
|
EGLConfig Config = nullptr;
|
|
EGLContext Context = EGL_NO_CONTEXT;
|
|
const FBConfigInfo* FBConfig = nullptr;
|
|
};
|
|
|
|
struct DrawableSurface {
|
|
EGLDisplay Display = EGL_NO_DISPLAY;
|
|
EGLSurface Surface = EGL_NO_SURFACE;
|
|
Uint32 Width = 0;
|
|
Uint32 Height = 0;
|
|
std::chrono::steady_clock::time_point LastSizePoll{};
|
|
};
|
|
|
|
std::recursive_mutex& RegistryMutex() {
|
|
static auto* mutex = new std::recursive_mutex();
|
|
return *mutex;
|
|
}
|
|
|
|
UnorderedMap<GLXContextHandle, ContextObject>& Contexts() {
|
|
static auto* contexts = new UnorderedMap<GLXContextHandle, ContextObject>();
|
|
return *contexts;
|
|
}
|
|
|
|
UnorderedMap<GLXDrawableHandle, DrawableSurface>& DrawableSurfaces() {
|
|
static auto* surfaces = new UnorderedMap<GLXDrawableHandle, DrawableSurface>();
|
|
return *surfaces;
|
|
}
|
|
|
|
UnorderedMap<GLXDrawableHandle, const FBConfigInfo*>& WindowFBConfigs() {
|
|
static auto* configs = new UnorderedMap<GLXDrawableHandle, const FBConfigInfo*>();
|
|
return *configs;
|
|
}
|
|
|
|
Uint64& NextContextHandle() {
|
|
static auto* handle = new Uint64(0x10000);
|
|
return *handle;
|
|
}
|
|
|
|
struct ThreadCurrent {
|
|
Display* XDisplay = nullptr;
|
|
GLXDrawableHandle Draw = 0;
|
|
GLXDrawableHandle Read = 0;
|
|
GLXContextHandle Context = nullptr;
|
|
};
|
|
thread_local ThreadCurrent t_current;
|
|
|
|
Int& SwapIntervalShadow() {
|
|
static auto* interval = new Int(1);
|
|
return *interval;
|
|
}
|
|
|
|
void EnsureInitialized() {
|
|
// MobileGL::EnsureInitialized (not a local once_flag) so a fresh init
|
|
// can follow a full teardown from the last eglTerminate.
|
|
MobileGL::EnsureInitialized();
|
|
}
|
|
|
|
EGLDisplay EnsureDisplay() {
|
|
EnsureInitialized();
|
|
EGLDisplay display = EGLImpl::GetDisplay(EGL_DEFAULT_DISPLAY);
|
|
if (display == EGL_NO_DISPLAY) {
|
|
return EGL_NO_DISPLAY;
|
|
}
|
|
if (!EGLImpl::Initialize(display, nullptr, nullptr)) {
|
|
return EGL_NO_DISPLAY;
|
|
}
|
|
return display;
|
|
}
|
|
|
|
GLXContextHandle EncodeContext(Uint64 handle) {
|
|
return reinterpret_cast<GLXContextHandle>(static_cast<SizeT>(handle));
|
|
}
|
|
|
|
ContextObject* TryGetContext(GLXContextHandle context) {
|
|
auto& contexts = Contexts();
|
|
auto it = contexts.find(context);
|
|
return it == contexts.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
Bool QueryDrawableSize(Display* dpy, GLXDrawableHandle drawable, Uint32& width, Uint32& height) {
|
|
const auto& x11 = X11();
|
|
if (!x11.Valid() || !dpy || drawable == 0) {
|
|
return false;
|
|
}
|
|
GLXDrawableHandle root = 0;
|
|
int x = 0;
|
|
int y = 0;
|
|
unsigned int w = 0;
|
|
unsigned int h = 0;
|
|
unsigned int border = 0;
|
|
unsigned int depth = 0;
|
|
if (!x11.GetGeometry(dpy, drawable, &root, &x, &y, &w, &h, &border, &depth)) {
|
|
return false;
|
|
}
|
|
width = std::max(w, 1u);
|
|
height = std::max(h, 1u);
|
|
return true;
|
|
}
|
|
|
|
// The backends never query the X window size themselves; the GLX layer
|
|
// owns size discovery and pushes changes through the internal resize hook
|
|
// (same contract as the WGL and CGL layers). Polling XGetGeometry is a
|
|
// server round trip, so throttle steady-state swaps.
|
|
void SyncSurfaceSize(Display* dpy, GLXDrawableHandle drawable, DrawableSurface& surface,
|
|
Bool force = false) {
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (!force && now - surface.LastSizePoll < std::chrono::milliseconds(250)) {
|
|
return;
|
|
}
|
|
surface.LastSizePoll = now;
|
|
Uint32 width = 0;
|
|
Uint32 height = 0;
|
|
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
|
return;
|
|
}
|
|
if (width == surface.Width && height == surface.Height) {
|
|
return;
|
|
}
|
|
if (EGLImpl::ResizePlatformWindowSurface(surface.Display, surface.Surface,
|
|
static_cast<EGLint>(width),
|
|
static_cast<EGLint>(height))) {
|
|
surface.Width = width;
|
|
surface.Height = height;
|
|
}
|
|
}
|
|
|
|
DrawableSurface* EnsureDrawableSurface(Display* dpy, GLXDrawableHandle drawable,
|
|
const ContextObject& context) {
|
|
auto& surfaces = DrawableSurfaces();
|
|
auto it = surfaces.find(drawable);
|
|
if (it != surfaces.end()) {
|
|
SyncSurfaceSize(dpy, drawable, it->second, true);
|
|
return &it->second;
|
|
}
|
|
|
|
Uint32 width = 0;
|
|
Uint32 height = 0;
|
|
if (!QueryDrawableSize(dpy, drawable, width, height)) {
|
|
MGLOG_E_ONCE("glx: XGetGeometry failed for drawable 0x%lx", drawable);
|
|
return nullptr;
|
|
}
|
|
|
|
const EGLAttrib attribs[] = {
|
|
EGL_WIDTH, static_cast<EGLAttrib>(width),
|
|
EGL_HEIGHT, static_cast<EGLAttrib>(height),
|
|
EGL_NONE,
|
|
};
|
|
EGLSurface surface = EGLImpl::CreatePlatformWindowSurface(
|
|
context.Display, context.Config, reinterpret_cast<void*>(drawable), attribs);
|
|
if (surface == EGL_NO_SURFACE) {
|
|
MGLOG_E_ONCE("glx: failed to create window surface for drawable 0x%lx (%ux%u)", drawable,
|
|
width, height);
|
|
return nullptr;
|
|
}
|
|
|
|
DrawableSurface record;
|
|
record.Display = context.Display;
|
|
record.Surface = surface;
|
|
record.Width = width;
|
|
record.Height = height;
|
|
record.LastSizePoll = std::chrono::steady_clock::now();
|
|
auto [inserted, _] = surfaces.emplace(drawable, record);
|
|
return &inserted->second;
|
|
}
|
|
|
|
GLXContextHandle CreateContextFromEGLAttribs(Display* dpy, const FBConfigInfo* fbconfig,
|
|
GLXContextHandle share,
|
|
const EGLint* contextAttribs) {
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
EGLDisplay display = EnsureDisplay();
|
|
if (display == EGL_NO_DISPLAY) {
|
|
MGLOG_E_ONCE("glx: no EGL display");
|
|
return nullptr;
|
|
}
|
|
EGLImpl::BindAPI(EGL_OPENGL_API);
|
|
|
|
EGLContext shareContext = EGL_NO_CONTEXT;
|
|
if (share) {
|
|
auto* shareObject = TryGetContext(share);
|
|
if (!shareObject) {
|
|
return nullptr;
|
|
}
|
|
shareContext = shareObject->Context;
|
|
}
|
|
|
|
const EGLint configAttribs[] = {
|
|
EGL_RED_SIZE, 8,
|
|
EGL_GREEN_SIZE, 8,
|
|
EGL_BLUE_SIZE, 8,
|
|
EGL_ALPHA_SIZE, 8,
|
|
EGL_DEPTH_SIZE, 24,
|
|
EGL_STENCIL_SIZE, fbconfig->StencilBits,
|
|
EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT,
|
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
|
EGL_NONE,
|
|
};
|
|
EGLConfig config = nullptr;
|
|
EGLint configCount = 0;
|
|
if (!EGLImpl::ChooseConfig(display, configAttribs, &config, 1, &configCount) ||
|
|
configCount <= 0) {
|
|
MGLOG_E_ONCE("glx: eglChooseConfig failed");
|
|
return nullptr;
|
|
}
|
|
|
|
EGLContext eglContext = EGLImpl::CreateContext(display, config, shareContext, contextAttribs);
|
|
if (eglContext == EGL_NO_CONTEXT) {
|
|
MGLOG_E_ONCE("glx: eglCreateContext failed");
|
|
return nullptr;
|
|
}
|
|
|
|
ContextObject object;
|
|
object.XDisplay = dpy;
|
|
object.Display = display;
|
|
object.Config = config;
|
|
object.Context = eglContext;
|
|
object.FBConfig = fbconfig;
|
|
const auto handle = EncodeContext(NextContextHandle()++);
|
|
Contexts()[handle] = object;
|
|
MGLOG_I("glx: created context %p (EGL context %p, stencil %d)", handle, eglContext,
|
|
fbconfig->StencilBits);
|
|
return handle;
|
|
}
|
|
|
|
int FBConfigAttribValue(Display* dpy, const FBConfigInfo& info, int attribute, int* value) {
|
|
const auto& x11 = X11();
|
|
switch (attribute) {
|
|
case GLX_FBCONFIG_ID:
|
|
*value = info.FBConfigId;
|
|
return 0;
|
|
case GLX_BUFFER_SIZE:
|
|
*value = 32;
|
|
return 0;
|
|
case GLX_LEVEL:
|
|
*value = 0;
|
|
return 0;
|
|
case GLX_DOUBLEBUFFER:
|
|
*value = 1;
|
|
return 0;
|
|
case GLX_STEREO:
|
|
*value = 0;
|
|
return 0;
|
|
case GLX_AUX_BUFFERS:
|
|
*value = 0;
|
|
return 0;
|
|
case GLX_RED_SIZE:
|
|
case GLX_GREEN_SIZE:
|
|
case GLX_BLUE_SIZE:
|
|
case GLX_ALPHA_SIZE:
|
|
*value = 8;
|
|
return 0;
|
|
case GLX_DEPTH_SIZE:
|
|
*value = 24;
|
|
return 0;
|
|
case GLX_STENCIL_SIZE:
|
|
*value = info.StencilBits;
|
|
return 0;
|
|
case GLX_ACCUM_RED_SIZE:
|
|
case GLX_ACCUM_GREEN_SIZE:
|
|
case GLX_ACCUM_BLUE_SIZE:
|
|
case GLX_ACCUM_ALPHA_SIZE:
|
|
*value = 0;
|
|
return 0;
|
|
case GLX_RENDER_TYPE:
|
|
*value = GLX_RGBA_BIT;
|
|
return 0;
|
|
case GLX_DRAWABLE_TYPE:
|
|
*value = GLX_WINDOW_BIT;
|
|
return 0;
|
|
case GLX_X_RENDERABLE:
|
|
*value = 1;
|
|
return 0;
|
|
case GLX_X_VISUAL_TYPE:
|
|
*value = GLX_TRUE_COLOR;
|
|
return 0;
|
|
case GLX_CONFIG_CAVEAT:
|
|
case GLX_TRANSPARENT_TYPE:
|
|
*value = GLX_NONE;
|
|
return 0;
|
|
case GLX_TRANSPARENT_INDEX_VALUE:
|
|
case GLX_TRANSPARENT_RED_VALUE:
|
|
case GLX_TRANSPARENT_GREEN_VALUE:
|
|
case GLX_TRANSPARENT_BLUE_VALUE:
|
|
case GLX_TRANSPARENT_ALPHA_VALUE:
|
|
*value = 0;
|
|
return 0;
|
|
case GLX_VISUAL_ID: {
|
|
*value = 0;
|
|
if (x11.Valid() && dpy) {
|
|
const int screen = x11.GetDefaultScreen(dpy);
|
|
void* visual = x11.GetDefaultVisual(dpy, screen);
|
|
if (visual) {
|
|
*value = static_cast<int>(x11.VisualIDFromVisual(visual));
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
case GLX_SCREEN:
|
|
*value = (x11.Valid() && dpy) ? x11.GetDefaultScreen(dpy) : 0;
|
|
return 0;
|
|
case GLX_MAX_PBUFFER_WIDTH:
|
|
case GLX_MAX_PBUFFER_HEIGHT:
|
|
*value = 16384;
|
|
return 0;
|
|
case GLX_MAX_PBUFFER_PIXELS:
|
|
*value = 16384 * 16384;
|
|
return 0;
|
|
case GLX_SAMPLE_BUFFERS:
|
|
case GLX_SAMPLES:
|
|
*value = 0;
|
|
return 0;
|
|
default:
|
|
*value = 0;
|
|
return GLX_BAD_ATTRIBUTE;
|
|
}
|
|
}
|
|
|
|
// Resolve the X visual all our configs render to: the default visual of
|
|
// the screen (matches EGLState's EGL_NATIVE_VISUAL_ID policy), falling
|
|
// back to any 24-bit TrueColor visual. Caller frees with XFree.
|
|
XVisualInfoCompat* ResolveVisual(Display* dpy, int screen) {
|
|
const auto& x11 = X11();
|
|
if (!x11.Valid() || !dpy) {
|
|
return nullptr;
|
|
}
|
|
if (screen < 0) {
|
|
screen = x11.GetDefaultScreen(dpy);
|
|
}
|
|
if (void* visual = x11.GetDefaultVisual(dpy, screen)) {
|
|
XVisualInfoCompat tmpl{};
|
|
tmpl.visualid = x11.VisualIDFromVisual(visual);
|
|
int count = 0;
|
|
XVisualInfoCompat* info = x11.GetVisualInfo(dpy, kVisualIDMask, &tmpl, &count);
|
|
if (info && count >= 1 && info->depth >= 24 && info->c_class == kTrueColor) {
|
|
return info;
|
|
}
|
|
if (info) {
|
|
x11.Free(info);
|
|
}
|
|
}
|
|
XVisualInfoCompat tmpl{};
|
|
tmpl.screen = screen;
|
|
tmpl.depth = 24;
|
|
tmpl.c_class = kTrueColor;
|
|
int count = 0;
|
|
XVisualInfoCompat* info = x11.GetVisualInfo(
|
|
dpy, kVisualScreenMask | kVisualDepthMask | kVisualClassMask, &tmpl, &count);
|
|
if (info && count < 1) {
|
|
x11.Free(info);
|
|
return nullptr;
|
|
}
|
|
return info;
|
|
}
|
|
} // namespace
|
|
|
|
int QueryExtension(Display*, int* errorBase, int* eventBase) {
|
|
if (errorBase) {
|
|
*errorBase = 0;
|
|
}
|
|
if (eventBase) {
|
|
*eventBase = 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int QueryVersion(Display*, int* major, int* minor) {
|
|
if (major) {
|
|
*major = 1;
|
|
}
|
|
if (minor) {
|
|
*minor = 4;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
const char* QueryExtensionsString(Display*, int) {
|
|
return kGLXExtensions;
|
|
}
|
|
|
|
const char* GetClientString(Display*, int name) {
|
|
switch (name) {
|
|
case GLX_VENDOR:
|
|
return "MobileGL";
|
|
case GLX_VERSION:
|
|
return "1.4";
|
|
case GLX_EXTENSIONS:
|
|
return kGLXExtensions;
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
const char* QueryServerString(Display* dpy, int, int name) {
|
|
return GetClientString(dpy, name);
|
|
}
|
|
|
|
GLXFBConfigHandle* GetFBConfigs(Display*, int, int* nelements) {
|
|
auto** configs = static_cast<GLXFBConfigHandle*>(
|
|
std::malloc(sizeof(GLXFBConfigHandle) * kFBConfigCount));
|
|
if (!configs) {
|
|
if (nelements) {
|
|
*nelements = 0;
|
|
}
|
|
return nullptr;
|
|
}
|
|
for (int i = 0; i < kFBConfigCount; ++i) {
|
|
configs[i] = const_cast<FBConfigInfo*>(&kFBConfigs[i]);
|
|
}
|
|
if (nelements) {
|
|
*nelements = kFBConfigCount;
|
|
}
|
|
return configs;
|
|
}
|
|
|
|
GLXFBConfigHandle* ChooseFBConfig(Display* dpy, int screen, const int* attribList, int* nelements) {
|
|
// Every config satisfies >= size matches for our fixed RGBA8888/24 caps;
|
|
// the only distinguishing filters are stencil size and hard mismatches.
|
|
Bool acceptConfig[kFBConfigCount] = {true, true};
|
|
if (attribList) {
|
|
for (SizeT i = 0; attribList[i] != 0; i += 2) {
|
|
const int attrib = attribList[i];
|
|
const int value = attribList[i + 1];
|
|
if (value == GLX_DONT_CARE) {
|
|
continue;
|
|
}
|
|
switch (attrib) {
|
|
case GLX_STENCIL_SIZE:
|
|
for (int c = 0; c < kFBConfigCount; ++c) {
|
|
if (kFBConfigs[c].StencilBits < value) {
|
|
acceptConfig[c] = false;
|
|
}
|
|
}
|
|
break;
|
|
case GLX_FBCONFIG_ID:
|
|
for (int c = 0; c < kFBConfigCount; ++c) {
|
|
if (kFBConfigs[c].FBConfigId != value) {
|
|
acceptConfig[c] = false;
|
|
}
|
|
}
|
|
break;
|
|
case GLX_RED_SIZE:
|
|
case GLX_GREEN_SIZE:
|
|
case GLX_BLUE_SIZE:
|
|
case GLX_ALPHA_SIZE:
|
|
if (value > 8) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
case GLX_BUFFER_SIZE:
|
|
if (value > 32) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
case GLX_DEPTH_SIZE:
|
|
if (value > 24) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
case GLX_DOUBLEBUFFER:
|
|
if (value != 1) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
case GLX_STEREO:
|
|
case GLX_AUX_BUFFERS:
|
|
case GLX_SAMPLE_BUFFERS:
|
|
case GLX_SAMPLES:
|
|
case GLX_LEVEL:
|
|
case GLX_ACCUM_RED_SIZE:
|
|
case GLX_ACCUM_GREEN_SIZE:
|
|
case GLX_ACCUM_BLUE_SIZE:
|
|
case GLX_ACCUM_ALPHA_SIZE:
|
|
if (value > 0) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
case GLX_RENDER_TYPE:
|
|
if ((value & GLX_RGBA_BIT) == 0) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
case GLX_DRAWABLE_TYPE:
|
|
if ((value & ~GLX_WINDOW_BIT) != 0) {
|
|
acceptConfig[0] = acceptConfig[1] = false;
|
|
}
|
|
break;
|
|
default:
|
|
// Lenient like EGLState's ChooseConfig: unknown attributes
|
|
// are ignored rather than failing the whole request.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
int count = 0;
|
|
for (int c = 0; c < kFBConfigCount; ++c) {
|
|
if (acceptConfig[c]) {
|
|
++count;
|
|
}
|
|
}
|
|
if (count == 0) {
|
|
if (nelements) {
|
|
*nelements = 0;
|
|
}
|
|
return nullptr;
|
|
}
|
|
auto** configs =
|
|
static_cast<GLXFBConfigHandle*>(std::malloc(sizeof(GLXFBConfigHandle) * count));
|
|
if (!configs) {
|
|
if (nelements) {
|
|
*nelements = 0;
|
|
}
|
|
return nullptr;
|
|
}
|
|
int out = 0;
|
|
for (int c = 0; c < kFBConfigCount; ++c) {
|
|
if (acceptConfig[c]) {
|
|
configs[out++] = const_cast<FBConfigInfo*>(&kFBConfigs[c]);
|
|
}
|
|
}
|
|
if (nelements) {
|
|
*nelements = count;
|
|
}
|
|
(void)dpy;
|
|
(void)screen;
|
|
return configs;
|
|
}
|
|
|
|
int GetFBConfigAttrib(Display* dpy, GLXFBConfigHandle config, int attribute, int* value) {
|
|
const auto* info = TryGetFBConfig(config);
|
|
if (!info || !value) {
|
|
return GLX_BAD_ATTRIBUTE;
|
|
}
|
|
return FBConfigAttribValue(dpy, *info, attribute, value);
|
|
}
|
|
|
|
void* GetVisualFromFBConfig(Display* dpy, GLXFBConfigHandle config) {
|
|
if (!TryGetFBConfig(config)) {
|
|
return nullptr;
|
|
}
|
|
return ResolveVisual(dpy, -1);
|
|
}
|
|
|
|
void* ChooseVisual(Display* dpy, int screen, int* attribList) {
|
|
// Legacy visual chooser: our single RGBA8888/24/8 double-buffered visual
|
|
// satisfies any RGBA request; color-index and stereo are unsupported.
|
|
Bool wantsRGBA = false;
|
|
if (attribList) {
|
|
for (SizeT i = 0; attribList[i] != 0;) {
|
|
const int attrib = attribList[i];
|
|
if (attrib == GLX_RGBA || attrib == GLX_USE_GL || attrib == GLX_DOUBLEBUFFER) {
|
|
wantsRGBA = wantsRGBA || attrib == GLX_RGBA;
|
|
++i;
|
|
continue;
|
|
}
|
|
if (attrib == GLX_STEREO) {
|
|
return nullptr;
|
|
}
|
|
i += 2;
|
|
}
|
|
}
|
|
if (!wantsRGBA) {
|
|
return nullptr;
|
|
}
|
|
return ResolveVisual(dpy, screen);
|
|
}
|
|
|
|
int GetConfig(Display*, void* visualInfo, int attribute, int* value) {
|
|
if (!visualInfo || !value) {
|
|
return GLX_BAD_ATTRIBUTE;
|
|
}
|
|
switch (attribute) {
|
|
case GLX_USE_GL:
|
|
case GLX_RGBA:
|
|
case GLX_DOUBLEBUFFER:
|
|
*value = 1;
|
|
return 0;
|
|
case GLX_STEREO:
|
|
case GLX_LEVEL:
|
|
case GLX_AUX_BUFFERS:
|
|
case GLX_ACCUM_RED_SIZE:
|
|
case GLX_ACCUM_GREEN_SIZE:
|
|
case GLX_ACCUM_BLUE_SIZE:
|
|
case GLX_ACCUM_ALPHA_SIZE:
|
|
*value = 0;
|
|
return 0;
|
|
case GLX_BUFFER_SIZE:
|
|
*value = 32;
|
|
return 0;
|
|
case GLX_RED_SIZE:
|
|
case GLX_GREEN_SIZE:
|
|
case GLX_BLUE_SIZE:
|
|
case GLX_ALPHA_SIZE:
|
|
*value = 8;
|
|
return 0;
|
|
case GLX_DEPTH_SIZE:
|
|
*value = 24;
|
|
return 0;
|
|
case GLX_STENCIL_SIZE:
|
|
*value = 8;
|
|
return 0;
|
|
default:
|
|
*value = 0;
|
|
return GLX_BAD_ATTRIBUTE;
|
|
}
|
|
}
|
|
|
|
GLXContextHandle CreateContext(Display* dpy, void* visualInfo, GLXContextHandle share, int) {
|
|
EnsureInitialized();
|
|
MGLOG_I("glx: glXCreateContext(visual=%p)", visualInfo);
|
|
if (!visualInfo) {
|
|
return nullptr;
|
|
}
|
|
// A legacy GLX context is a compatibility-profile context; MobileGL keys
|
|
// its relaxed-semantics mode off the explicit compatibility bit.
|
|
const EGLint attribs[] = {
|
|
EGL_CONTEXT_MAJOR_VERSION, 3,
|
|
EGL_CONTEXT_MINOR_VERSION, 3,
|
|
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
|
|
EGL_NONE,
|
|
};
|
|
return CreateContextFromEGLAttribs(dpy, &kFBConfigs[0], share, attribs);
|
|
}
|
|
|
|
GLXContextHandle CreateNewContext(Display* dpy, GLXFBConfigHandle config, int renderType,
|
|
GLXContextHandle share, int) {
|
|
EnsureInitialized();
|
|
const auto* info = TryGetFBConfig(config);
|
|
if (!info || renderType != GLX_RGBA_TYPE) {
|
|
return nullptr;
|
|
}
|
|
const EGLint attribs[] = {
|
|
EGL_CONTEXT_MAJOR_VERSION, 3,
|
|
EGL_CONTEXT_MINOR_VERSION, 3,
|
|
EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT,
|
|
EGL_NONE,
|
|
};
|
|
return CreateContextFromEGLAttribs(dpy, info, share, attribs);
|
|
}
|
|
|
|
GLXContextHandle CreateContextAttribsARB(Display* dpy, GLXFBConfigHandle config,
|
|
GLXContextHandle share, int, const int* attribList) {
|
|
EnsureInitialized();
|
|
const auto* info = TryGetFBConfig(config);
|
|
if (!info) {
|
|
return nullptr;
|
|
}
|
|
|
|
int major = 1;
|
|
int minor = 0;
|
|
int profileMask = 0;
|
|
int flags = 0;
|
|
if (attribList) {
|
|
for (SizeT i = 0; attribList[i] != 0; i += 2) {
|
|
const int attrib = attribList[i];
|
|
const int value = attribList[i + 1];
|
|
switch (attrib) {
|
|
case GLX_CONTEXT_MAJOR_VERSION_ARB:
|
|
major = value;
|
|
break;
|
|
case GLX_CONTEXT_MINOR_VERSION_ARB:
|
|
minor = value;
|
|
break;
|
|
case GLX_CONTEXT_PROFILE_MASK_ARB:
|
|
profileMask = value;
|
|
break;
|
|
case GLX_CONTEXT_FLAGS_ARB:
|
|
flags = value;
|
|
break;
|
|
case GLX_CONTEXT_OPENGL_NO_ERROR_ARB:
|
|
// Accepted and ignored: MobileGL always validates.
|
|
break;
|
|
default:
|
|
MGLOG_D("glXCreateContextAttribsARB: ignoring attrib 0x%04x = 0x%x", attrib,
|
|
value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (major < 1 || (profileMask & ~(GLX_CONTEXT_CORE_PROFILE_BIT_ARB |
|
|
GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB))) {
|
|
return nullptr;
|
|
}
|
|
|
|
Vector<EGLint> attribs = {
|
|
EGL_CONTEXT_MAJOR_VERSION, major,
|
|
EGL_CONTEXT_MINOR_VERSION, minor,
|
|
};
|
|
const Bool wantsCompat = (profileMask & GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB) != 0;
|
|
if (major > 3 || (major == 3 && minor >= 2) || profileMask != 0) {
|
|
attribs.push_back(EGL_CONTEXT_OPENGL_PROFILE_MASK);
|
|
attribs.push_back(wantsCompat ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT
|
|
: EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT);
|
|
}
|
|
if (flags & GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB) {
|
|
attribs.push_back(EGL_CONTEXT_OPENGL_FORWARD_COMPATIBLE);
|
|
attribs.push_back(EGL_TRUE);
|
|
}
|
|
if (flags & GLX_CONTEXT_DEBUG_BIT_ARB) {
|
|
attribs.push_back(EGL_CONTEXT_OPENGL_DEBUG);
|
|
attribs.push_back(EGL_TRUE);
|
|
}
|
|
attribs.push_back(EGL_NONE);
|
|
|
|
return CreateContextFromEGLAttribs(dpy, info, share, attribs.data());
|
|
}
|
|
|
|
void DestroyContext(Display*, GLXContextHandle context) {
|
|
EnsureInitialized();
|
|
MGLOG_I("glx: glXDestroyContext(%p)", context);
|
|
if (t_current.Context == context) {
|
|
MakeCurrent(t_current.XDisplay, 0, nullptr);
|
|
}
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
auto* object = TryGetContext(context);
|
|
if (!object) {
|
|
return;
|
|
}
|
|
if (object->Context != EGL_NO_CONTEXT) {
|
|
EGLImpl::DestroyContext(object->Display, object->Context);
|
|
}
|
|
Contexts().erase(context);
|
|
}
|
|
|
|
int MakeCurrent(Display* dpy, GLXDrawableHandle drawable, GLXContextHandle context) {
|
|
EnsureInitialized();
|
|
MGLOG_D("glx: glXMakeCurrent(drawable=0x%lx, ctx=%p)", drawable, context);
|
|
if (!context) {
|
|
if (!t_current.Context) {
|
|
t_current = {};
|
|
return 1;
|
|
}
|
|
const EGLBoolean released =
|
|
EGLImpl::MakeCurrent(EGL_NO_DISPLAY, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
t_current = {};
|
|
return released == EGL_TRUE ? 1 : 0;
|
|
}
|
|
|
|
if (drawable == 0) {
|
|
return 0;
|
|
}
|
|
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
auto* object = TryGetContext(context);
|
|
if (!object) {
|
|
return 0;
|
|
}
|
|
|
|
DrawableSurface* surface = EnsureDrawableSurface(dpy, drawable, *object);
|
|
if (!surface) {
|
|
return 0;
|
|
}
|
|
|
|
if (!EGLImpl::MakeCurrent(object->Display, surface->Surface, surface->Surface,
|
|
object->Context)) {
|
|
MGLOG_E_ONCE("glx: eglMakeCurrent failed (drawable=0x%lx, ctx=%p)", drawable, context);
|
|
return 0;
|
|
}
|
|
t_current = {dpy, drawable, drawable, context};
|
|
return 1;
|
|
}
|
|
|
|
int MakeContextCurrent(Display* dpy, GLXDrawableHandle draw, GLXDrawableHandle read,
|
|
GLXContextHandle context) {
|
|
if (context && draw != read) {
|
|
// MobileGL's backends reject split draw/read surfaces; bind the draw
|
|
// drawable for both, which is what every real caller here needs.
|
|
MGLOG_W_ONCE("glx: glXMakeContextCurrent draw 0x%lx != read 0x%lx, using draw for both", draw,
|
|
read);
|
|
}
|
|
const int result = MakeCurrent(dpy, draw, context);
|
|
if (result && context) {
|
|
t_current.Read = read;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void SwapBuffers(Display* dpy, GLXDrawableHandle drawable) {
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
auto& surfaces = DrawableSurfaces();
|
|
auto it = surfaces.find(drawable);
|
|
if (it == surfaces.end()) {
|
|
MGLOG_W_ONCE("glx: glXSwapBuffers with no surface for drawable 0x%lx", drawable);
|
|
return;
|
|
}
|
|
SyncSurfaceSize(dpy, drawable, it->second);
|
|
EGLImpl::SwapBuffers(it->second.Display, it->second.Surface);
|
|
}
|
|
|
|
GLXDrawableHandle CreateWindow(Display*, GLXFBConfigHandle config, GLXDrawableHandle window,
|
|
const int*) {
|
|
EnsureInitialized();
|
|
const auto* info = TryGetFBConfig(config);
|
|
if (!info || window == 0) {
|
|
return 0;
|
|
}
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
WindowFBConfigs()[window] = info;
|
|
// The GLXWindow is the X window itself; the EGL surface is created
|
|
// lazily on the first MakeCurrent (same pattern as WGL's HWND cache).
|
|
return window;
|
|
}
|
|
|
|
void DestroyWindow(Display*, GLXDrawableHandle window) {
|
|
EnsureInitialized();
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
WindowFBConfigs().erase(window);
|
|
auto& surfaces = DrawableSurfaces();
|
|
auto it = surfaces.find(window);
|
|
if (it == surfaces.end()) {
|
|
return;
|
|
}
|
|
EGLImpl::DestroySurface(it->second.Display, it->second.Surface);
|
|
surfaces.erase(it);
|
|
}
|
|
|
|
GLXContextHandle GetCurrentContext() {
|
|
return t_current.Context;
|
|
}
|
|
|
|
GLXDrawableHandle GetCurrentDrawable() {
|
|
return t_current.Draw;
|
|
}
|
|
|
|
GLXDrawableHandle GetCurrentReadDrawable() {
|
|
return t_current.Read;
|
|
}
|
|
|
|
Display* GetCurrentDisplay() {
|
|
return t_current.XDisplay;
|
|
}
|
|
|
|
int IsDirect(Display*, GLXContextHandle) {
|
|
return 1;
|
|
}
|
|
|
|
void WaitGL() {
|
|
EGLImpl::WaitGL();
|
|
}
|
|
|
|
void WaitX() {
|
|
const auto& x11 = X11();
|
|
if (x11.Valid() && x11.Sync && t_current.XDisplay) {
|
|
x11.Sync(t_current.XDisplay, 0);
|
|
}
|
|
}
|
|
|
|
int QueryContext(Display*, GLXContextHandle context, int attribute, int* value) {
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
auto* object = TryGetContext(context);
|
|
if (!object || !value) {
|
|
return GLX_BAD_ATTRIBUTE;
|
|
}
|
|
switch (attribute) {
|
|
case GLX_FBCONFIG_ID:
|
|
*value = object->FBConfig ? object->FBConfig->FBConfigId : 0;
|
|
return 0;
|
|
case GLX_RENDER_TYPE:
|
|
*value = GLX_RGBA_TYPE;
|
|
return 0;
|
|
case GLX_SCREEN:
|
|
*value = 0;
|
|
return 0;
|
|
default:
|
|
*value = 0;
|
|
return GLX_BAD_ATTRIBUTE;
|
|
}
|
|
}
|
|
|
|
void QueryDrawable(Display* dpy, GLXDrawableHandle drawable, int attribute, unsigned int* value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
switch (attribute) {
|
|
case GLX_WIDTH:
|
|
case GLX_HEIGHT: {
|
|
Uint32 width = 0;
|
|
Uint32 height = 0;
|
|
if (QueryDrawableSize(dpy, drawable, width, height)) {
|
|
*value = attribute == GLX_WIDTH ? width : height;
|
|
}
|
|
return;
|
|
}
|
|
case GLX_SWAP_INTERVAL_EXT:
|
|
*value = static_cast<unsigned int>(SwapIntervalShadow());
|
|
return;
|
|
case GLX_MAX_SWAP_INTERVAL_EXT:
|
|
*value = 4;
|
|
return;
|
|
case GLX_FBCONFIG_ID: {
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
auto& configs = WindowFBConfigs();
|
|
auto it = configs.find(drawable);
|
|
*value = it == configs.end() ? 0 : it->second->FBConfigId;
|
|
return;
|
|
}
|
|
default:
|
|
*value = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void SwapIntervalEXT(Display*, GLXDrawableHandle, int interval) {
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
EGLDisplay display = EnsureDisplay();
|
|
if (display == EGL_NO_DISPLAY) {
|
|
return;
|
|
}
|
|
// MobileGL validates 0..4; adaptive vsync (negative) clamps to regular.
|
|
interval = std::clamp(interval, 0, 4);
|
|
EGLImpl::SwapInterval(display, interval);
|
|
SwapIntervalShadow() = interval;
|
|
}
|
|
|
|
int SwapIntervalMESA(unsigned int interval) {
|
|
SwapIntervalEXT(nullptr, 0, static_cast<int>(interval));
|
|
return 0;
|
|
}
|
|
|
|
int GetSwapIntervalMESA() {
|
|
const std::lock_guard<std::recursive_mutex> lock(RegistryMutex());
|
|
return SwapIntervalShadow();
|
|
}
|
|
|
|
int SwapIntervalSGI(int interval) {
|
|
if (interval < 1) {
|
|
interval = 1;
|
|
}
|
|
SwapIntervalEXT(nullptr, 0, interval);
|
|
return 0;
|
|
}
|
|
} // namespace MobileGL::MG_Impl::GLXImpl
|
|
|
|
#endif // __linux__ && !__ANDROID__
|