mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
514 lines
22 KiB
C++
514 lines
22 KiB
C++
// MobileGL - MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.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 "NSOpenGLImpl.h"
|
|
|
|
#if defined(__APPLE__)
|
|
#ifndef GL_SILENCE_DEPRECATION
|
|
#define GL_SILENCE_DEPRECATION
|
|
#endif
|
|
#include "../CGLImpl/CGLImpl.h"
|
|
#include <CoreGraphics/CoreGraphics.h>
|
|
#include <objc/message.h>
|
|
#include <objc/objc.h>
|
|
#include <objc/runtime.h>
|
|
|
|
namespace MobileGL::MG_Impl::NSOpenGLImpl {
|
|
namespace {
|
|
constexpr intptr_t kAssociationRetain = 1;
|
|
constexpr intptr_t kAssociationAssign = 0;
|
|
|
|
char kPixelFormatHandleKey;
|
|
char kContextHandleKey;
|
|
char kContextPixelFormatObjectKey;
|
|
char kContextViewKey;
|
|
char kContextLayerKey;
|
|
|
|
IMP g_pixelFormatDealloc = nullptr;
|
|
IMP g_contextDealloc = nullptr;
|
|
|
|
std::mutex& HookInstallMutex() {
|
|
static auto* mutex = new std::mutex();
|
|
return *mutex;
|
|
}
|
|
|
|
Bool& HooksInstalled() {
|
|
static auto* installed = new Bool(false);
|
|
return *installed;
|
|
}
|
|
|
|
template <typename Fn>
|
|
Fn ObjcMsgSend() {
|
|
return reinterpret_cast<Fn>(objc_msgSend);
|
|
}
|
|
|
|
id SendId(id receiver, const char* selector) {
|
|
return receiver ? ObjcMsgSend<id (*)(id, SEL)>()(receiver, sel_registerName(selector)) : nil;
|
|
}
|
|
|
|
void SendVoid(id receiver, const char* selector) {
|
|
if (receiver) {
|
|
ObjcMsgSend<void (*)(id, SEL)>()(receiver, sel_registerName(selector));
|
|
}
|
|
}
|
|
|
|
void SendVoidBool(id receiver, const char* selector, bool value) {
|
|
if (receiver) {
|
|
ObjcMsgSend<void (*)(id, SEL, bool)>()(receiver, sel_registerName(selector), value);
|
|
}
|
|
}
|
|
|
|
void SendVoidId(id receiver, const char* selector, id value) {
|
|
if (receiver) {
|
|
ObjcMsgSend<void (*)(id, SEL, id)>()(receiver, sel_registerName(selector), value);
|
|
}
|
|
}
|
|
|
|
id SendIdCGLPixelFormat(id receiver, const char* selector, CGLPixelFormatObj value) {
|
|
return receiver ? ObjcMsgSend<id (*)(id, SEL, CGLPixelFormatObj)>()(receiver, sel_registerName(selector), value)
|
|
: nil;
|
|
}
|
|
|
|
void SendVoidCGRect(id receiver, const char* selector, CGRect value) {
|
|
if (receiver) {
|
|
ObjcMsgSend<void (*)(id, SEL, CGRect)>()(receiver, sel_registerName(selector), value);
|
|
}
|
|
}
|
|
|
|
void SendVoidCGSize(id receiver, const char* selector, CGSize value) {
|
|
if (receiver) {
|
|
ObjcMsgSend<void (*)(id, SEL, CGSize)>()(receiver, sel_registerName(selector), value);
|
|
}
|
|
}
|
|
|
|
void SendVoidCGFloat(id receiver, const char* selector, CGFloat value) {
|
|
if (receiver) {
|
|
ObjcMsgSend<void (*)(id, SEL, CGFloat)>()(receiver, sel_registerName(selector), value);
|
|
}
|
|
}
|
|
|
|
bool SendBool(id receiver, const char* selector) {
|
|
return receiver ? ObjcMsgSend<bool (*)(id, SEL)>()(receiver, sel_registerName(selector)) : false;
|
|
}
|
|
|
|
CGRect SendCGRect(id receiver, const char* selector) {
|
|
if (!receiver) {
|
|
return {};
|
|
}
|
|
return ObjcMsgSend<CGRect (*)(id, SEL)>()(receiver, sel_registerName(selector));
|
|
}
|
|
|
|
CGRect SendCGRectCGRect(id receiver, const char* selector, CGRect value) {
|
|
if (!receiver) {
|
|
return {};
|
|
}
|
|
return ObjcMsgSend<CGRect (*)(id, SEL, CGRect)>()(receiver, sel_registerName(selector), value);
|
|
}
|
|
|
|
CGFloat SendCGFloat(id receiver, const char* selector) {
|
|
return receiver ? ObjcMsgSend<CGFloat (*)(id, SEL)>()(receiver, sel_registerName(selector)) : 1.0;
|
|
}
|
|
|
|
void SetAssoc(id object, const void* key, id value, intptr_t policy) {
|
|
objc_setAssociatedObject(object, key, value, static_cast<objc_AssociationPolicy>(policy));
|
|
}
|
|
|
|
id GetAssoc(id object, const void* key) {
|
|
return object ? objc_getAssociatedObject(object, key) : nil;
|
|
}
|
|
|
|
CGLPixelFormatObj GetPixelFormatHandle(id object) {
|
|
return reinterpret_cast<CGLPixelFormatObj>(GetAssoc(object, &kPixelFormatHandleKey));
|
|
}
|
|
|
|
void SetPixelFormatHandle(id object, CGLPixelFormatObj handle) {
|
|
SetAssoc(object, &kPixelFormatHandleKey, reinterpret_cast<id>(handle), kAssociationAssign);
|
|
}
|
|
|
|
CGLContextObj GetContextHandle(id object) {
|
|
return reinterpret_cast<CGLContextObj>(GetAssoc(object, &kContextHandleKey));
|
|
}
|
|
|
|
void SetContextHandle(id object, CGLContextObj handle) {
|
|
SetAssoc(object, &kContextHandleKey, reinterpret_cast<id>(handle), kAssociationAssign);
|
|
}
|
|
|
|
id PixelFormatInitWithAttributes(id self, SEL, const CGLPixelFormatAttribute* attribs) {
|
|
CGLPixelFormatObj pixelFormat = nullptr;
|
|
GLint count = 0;
|
|
if (CGLImpl::ChoosePixelFormat(attribs, &pixelFormat, &count) != kCGLNoError) {
|
|
return nil;
|
|
}
|
|
SetPixelFormatHandle(self, pixelFormat);
|
|
return self;
|
|
}
|
|
|
|
id PixelFormatInitWithCGLPixelFormatObj(id self, SEL, CGLPixelFormatObj format) {
|
|
if (!format) {
|
|
return nil;
|
|
}
|
|
SetPixelFormatHandle(self, CGLImpl::RetainPixelFormat(format));
|
|
return self;
|
|
}
|
|
|
|
void PixelFormatGetValues(id self, SEL, GLint* vals, CGLPixelFormatAttribute attrib, GLint screen) {
|
|
(void)screen;
|
|
if (!vals) {
|
|
return;
|
|
}
|
|
GLint value = 0;
|
|
if (CGLImpl::DescribePixelFormat(GetPixelFormatHandle(self), 0, attrib, &value) == kCGLNoError) {
|
|
*vals = value;
|
|
}
|
|
}
|
|
|
|
GLint PixelFormatNumberOfVirtualScreens(id, SEL) {
|
|
return 1;
|
|
}
|
|
|
|
CGLPixelFormatObj PixelFormatCGLPixelFormatObj(id self, SEL) {
|
|
return GetPixelFormatHandle(self);
|
|
}
|
|
|
|
void PixelFormatDealloc(id self, SEL selector) {
|
|
if (auto format = GetPixelFormatHandle(self)) {
|
|
CGLImpl::ReleasePixelFormat(format);
|
|
SetPixelFormatHandle(self, nullptr);
|
|
}
|
|
if (g_pixelFormatDealloc) {
|
|
reinterpret_cast<void (*)(id, SEL)>(g_pixelFormatDealloc)(self, selector);
|
|
}
|
|
}
|
|
|
|
id ContextInitWithFormatShare(id self, SEL, id format, id share) {
|
|
auto pixelFormat = GetPixelFormatHandle(format);
|
|
if (!pixelFormat) {
|
|
return nil;
|
|
}
|
|
auto shareContext = GetContextHandle(share);
|
|
CGLContextObj context = nullptr;
|
|
if (CGLImpl::CreateContext(pixelFormat, shareContext, &context) != kCGLNoError) {
|
|
return nil;
|
|
}
|
|
CGLImpl::SetContextNSObject(context, self);
|
|
SetContextHandle(self, context);
|
|
SetAssoc(self, &kContextPixelFormatObjectKey, format, kAssociationRetain);
|
|
SetAssoc(self, &kContextViewKey, nil, kAssociationAssign);
|
|
SetAssoc(self, &kContextLayerKey, nil, kAssociationRetain);
|
|
return self;
|
|
}
|
|
|
|
id ContextInitWithCGLContextObj(id self, SEL, CGLContextObj context) {
|
|
if (!context) {
|
|
return nil;
|
|
}
|
|
CGLImpl::RetainContext(context);
|
|
CGLImpl::SetContextNSObject(context, self);
|
|
SetContextHandle(self, context);
|
|
if (auto pixelFormat = CGLImpl::GetPixelFormat(context)) {
|
|
id pixelFormatClass = reinterpret_cast<id>(objc_getClass("NSOpenGLPixelFormat"));
|
|
id allocated = SendId(pixelFormatClass, "alloc");
|
|
id pixelFormatObject = SendIdCGLPixelFormat(allocated, "initWithCGLPixelFormatObj:", pixelFormat);
|
|
SetAssoc(self, &kContextPixelFormatObjectKey, pixelFormatObject, kAssociationRetain);
|
|
SendVoid(pixelFormatObject, "release");
|
|
}
|
|
SetAssoc(self, &kContextViewKey, nil, kAssociationAssign);
|
|
SetAssoc(self, &kContextLayerKey, nil, kAssociationRetain);
|
|
return self;
|
|
}
|
|
|
|
struct DrawableGeometry {
|
|
CGRect Bounds = {};
|
|
CGSize DrawableSize = {1.0, 1.0};
|
|
CGFloat Scale = 1.0;
|
|
};
|
|
|
|
DrawableGeometry GetDrawableGeometry(id view) {
|
|
DrawableGeometry geometry;
|
|
geometry.Bounds = SendCGRect(view, "bounds");
|
|
if (geometry.Bounds.size.width <= 0.0 || geometry.Bounds.size.height <= 0.0) {
|
|
geometry.Bounds.size.width = 1.0;
|
|
geometry.Bounds.size.height = 1.0;
|
|
}
|
|
|
|
id window = SendId(view, "window");
|
|
if (window) {
|
|
geometry.Scale = SendCGFloat(window, "backingScaleFactor");
|
|
}
|
|
if (geometry.Scale <= 0.0) {
|
|
geometry.Scale = 1.0;
|
|
}
|
|
|
|
CGRect backingBounds = SendCGRectCGRect(view, "convertRectToBacking:", geometry.Bounds);
|
|
if (backingBounds.size.width > 0.0 && backingBounds.size.height > 0.0) {
|
|
geometry.DrawableSize = backingBounds.size;
|
|
} else {
|
|
geometry.DrawableSize = {geometry.Bounds.size.width * geometry.Scale,
|
|
geometry.Bounds.size.height * geometry.Scale};
|
|
}
|
|
geometry.DrawableSize.width = std::max<CGFloat>(std::round(geometry.DrawableSize.width), 1.0);
|
|
geometry.DrawableSize.height = std::max<CGFloat>(std::round(geometry.DrawableSize.height), 1.0);
|
|
return geometry;
|
|
}
|
|
|
|
void ConfigureMetalLayerForView(id layer, id view, const DrawableGeometry& geometry) {
|
|
SendVoidCGRect(layer, "setFrame:", geometry.Bounds);
|
|
SendVoidCGFloat(layer, "setContentsScale:", geometry.Scale);
|
|
SendVoidCGSize(layer, "setDrawableSize:", geometry.DrawableSize);
|
|
SendVoidBool(layer, "setNeedsDisplayOnBoundsChange:", true);
|
|
}
|
|
|
|
id CreateMetalLayerForView(id view, DrawableGeometry* outGeometry) {
|
|
if (!view) {
|
|
return nil;
|
|
}
|
|
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
|
|
if (!metalLayerClass) {
|
|
MGLOG_E_ONCE("NSOpenGLImpl: CAMetalLayer class not found");
|
|
return nil;
|
|
}
|
|
|
|
SendVoidBool(view, "setWantsLayer:", true);
|
|
id layer = SendId(metalLayerClass, "layer");
|
|
if (!layer) {
|
|
return nil;
|
|
}
|
|
|
|
const auto geometry = GetDrawableGeometry(view);
|
|
ConfigureMetalLayerForView(layer, view, geometry);
|
|
SendVoidId(view, "setLayer:", layer);
|
|
if (outGeometry) {
|
|
*outGeometry = geometry;
|
|
}
|
|
return layer;
|
|
}
|
|
|
|
void ContextSetView(id self, SEL, id view) {
|
|
auto context = GetContextHandle(self);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
if (!view) {
|
|
CGLImpl::ClearDrawable(context);
|
|
SetAssoc(self, &kContextViewKey, nil, kAssociationAssign);
|
|
SetAssoc(self, &kContextLayerKey, nil, kAssociationRetain);
|
|
return;
|
|
}
|
|
DrawableGeometry geometry;
|
|
id layer = CreateMetalLayerForView(view, &geometry);
|
|
if (!layer) {
|
|
return;
|
|
}
|
|
SetAssoc(self, &kContextViewKey, view, kAssociationAssign);
|
|
SetAssoc(self, &kContextLayerKey, layer, kAssociationRetain);
|
|
const auto error = CGLImpl::AttachDrawable(context, view, layer,
|
|
static_cast<GLint>(geometry.DrawableSize.width),
|
|
static_cast<GLint>(geometry.DrawableSize.height));
|
|
if (error != kCGLNoError) {
|
|
MGLOG_E_ONCE("NSOpenGLImpl: failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
|
}
|
|
}
|
|
|
|
id ContextView(id self, SEL) {
|
|
return GetAssoc(self, &kContextViewKey);
|
|
}
|
|
|
|
void ContextMakeCurrent(id self, SEL) {
|
|
auto context = GetContextHandle(self);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
const auto error = CGLImpl::SetCurrentContext(context);
|
|
if (error != kCGLNoError) {
|
|
MGLOG_E_ONCE("NSOpenGLImpl: makeCurrentContext failed: %s", CGLImpl::ErrorString(error));
|
|
}
|
|
}
|
|
|
|
void ContextClearCurrent(id, SEL) {
|
|
CGLImpl::SetCurrentContext(nullptr);
|
|
}
|
|
|
|
id ContextCurrentContext(id, SEL) {
|
|
auto context = CGLImpl::GetCurrentContext();
|
|
return reinterpret_cast<id>(CGLImpl::GetContextNSObject(context));
|
|
}
|
|
|
|
void ContextFlushBuffer(id self, SEL) {
|
|
auto context = GetContextHandle(self);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
const auto error = CGLImpl::FlushDrawable(context);
|
|
if (error != kCGLNoError) {
|
|
MGLOG_E_ONCE("NSOpenGLImpl: flushBuffer failed: %s", CGLImpl::ErrorString(error));
|
|
}
|
|
}
|
|
|
|
void ContextClearDrawable(id self, SEL) {
|
|
auto context = GetContextHandle(self);
|
|
if (context) {
|
|
CGLImpl::ClearDrawable(context);
|
|
}
|
|
}
|
|
|
|
void ContextUpdate(id self, SEL) {
|
|
auto context = GetContextHandle(self);
|
|
if (!context) {
|
|
return;
|
|
}
|
|
id view = GetAssoc(self, &kContextViewKey);
|
|
if (!view) {
|
|
CGLImpl::UpdateContext(context);
|
|
return;
|
|
}
|
|
id layer = GetAssoc(self, &kContextLayerKey);
|
|
if (!layer) {
|
|
ContextSetView(self, nullptr, view);
|
|
return;
|
|
}
|
|
const auto geometry = GetDrawableGeometry(view);
|
|
ConfigureMetalLayerForView(layer, view, geometry);
|
|
const auto error = CGLImpl::AttachDrawable(context, view, layer,
|
|
static_cast<GLint>(geometry.DrawableSize.width),
|
|
static_cast<GLint>(geometry.DrawableSize.height));
|
|
if (error != kCGLNoError) {
|
|
MGLOG_E_ONCE("NSOpenGLImpl: update failed to attach drawable: %s", CGLImpl::ErrorString(error));
|
|
return;
|
|
}
|
|
CGLImpl::UpdateContext(context);
|
|
}
|
|
|
|
void ContextSetValues(id self, SEL, const GLint* values, long parameter) {
|
|
auto context = GetContextHandle(self);
|
|
if (context) {
|
|
CGLImpl::SetParameter(context, static_cast<CGLContextParameter>(parameter), values);
|
|
}
|
|
}
|
|
|
|
void ContextGetValues(id self, SEL, GLint* values, long parameter) {
|
|
auto context = GetContextHandle(self);
|
|
if (context) {
|
|
CGLImpl::GetParameter(context, static_cast<CGLContextParameter>(parameter), values);
|
|
}
|
|
}
|
|
|
|
CGLContextObj ContextCGLContextObj(id self, SEL) {
|
|
return GetContextHandle(self);
|
|
}
|
|
|
|
id ContextPixelFormat(id self, SEL) {
|
|
return GetAssoc(self, &kContextPixelFormatObjectKey);
|
|
}
|
|
|
|
void ContextDealloc(id self, SEL selector) {
|
|
SetAssoc(self, &kContextPixelFormatObjectKey, nil, kAssociationRetain);
|
|
if (auto context = GetContextHandle(self)) {
|
|
CGLImpl::SetContextNSObject(context, nullptr);
|
|
CGLImpl::ReleaseContext(context);
|
|
SetContextHandle(self, nullptr);
|
|
}
|
|
if (g_contextDealloc) {
|
|
reinterpret_cast<void (*)(id, SEL)>(g_contextDealloc)(self, selector);
|
|
}
|
|
}
|
|
|
|
void ReplaceInstanceMethod(Class cls, const char* selectorName, IMP replacement, IMP* original = nullptr) {
|
|
SEL selector = sel_registerName(selectorName);
|
|
Method method = class_getInstanceMethod(cls, selector);
|
|
if (!method) {
|
|
MGLOG_W_ONCE("NSOpenGLImpl: missing instance method %s", selectorName);
|
|
return;
|
|
}
|
|
if (original) {
|
|
*original = method_getImplementation(method);
|
|
}
|
|
method_setImplementation(method, replacement);
|
|
}
|
|
|
|
void ReplaceClassMethod(Class cls, const char* selectorName, IMP replacement) {
|
|
SEL selector = sel_registerName(selectorName);
|
|
Method method = class_getClassMethod(cls, selector);
|
|
if (!method) {
|
|
MGLOG_W_ONCE("NSOpenGLImpl: missing class method %s", selectorName);
|
|
return;
|
|
}
|
|
method_setImplementation(method, replacement);
|
|
}
|
|
|
|
Bool InstallHooksOnce() {
|
|
Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat");
|
|
Class contextClass = objc_getClass("NSOpenGLContext");
|
|
if (!pixelFormatClass || !contextClass) {
|
|
MGLOG_W_ONCE("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed");
|
|
return false;
|
|
}
|
|
|
|
ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:",
|
|
reinterpret_cast<IMP>(PixelFormatInitWithAttributes));
|
|
ReplaceInstanceMethod(pixelFormatClass, "initWithCGLPixelFormatObj:",
|
|
reinterpret_cast<IMP>(PixelFormatInitWithCGLPixelFormatObj));
|
|
ReplaceInstanceMethod(pixelFormatClass, "getValues:forAttribute:forVirtualScreen:",
|
|
reinterpret_cast<IMP>(PixelFormatGetValues));
|
|
ReplaceInstanceMethod(pixelFormatClass, "numberOfVirtualScreens",
|
|
reinterpret_cast<IMP>(PixelFormatNumberOfVirtualScreens));
|
|
ReplaceInstanceMethod(pixelFormatClass, "CGLPixelFormatObj",
|
|
reinterpret_cast<IMP>(PixelFormatCGLPixelFormatObj));
|
|
ReplaceInstanceMethod(pixelFormatClass, "dealloc", reinterpret_cast<IMP>(PixelFormatDealloc),
|
|
&g_pixelFormatDealloc);
|
|
|
|
ReplaceInstanceMethod(contextClass, "initWithFormat:shareContext:",
|
|
reinterpret_cast<IMP>(ContextInitWithFormatShare));
|
|
ReplaceInstanceMethod(contextClass, "initWithCGLContextObj:",
|
|
reinterpret_cast<IMP>(ContextInitWithCGLContextObj));
|
|
ReplaceInstanceMethod(contextClass, "setView:", reinterpret_cast<IMP>(ContextSetView));
|
|
ReplaceInstanceMethod(contextClass, "view", reinterpret_cast<IMP>(ContextView));
|
|
ReplaceInstanceMethod(contextClass, "makeCurrentContext", reinterpret_cast<IMP>(ContextMakeCurrent));
|
|
ReplaceClassMethod(contextClass, "clearCurrentContext", reinterpret_cast<IMP>(ContextClearCurrent));
|
|
ReplaceClassMethod(contextClass, "currentContext", reinterpret_cast<IMP>(ContextCurrentContext));
|
|
ReplaceInstanceMethod(contextClass, "flushBuffer", reinterpret_cast<IMP>(ContextFlushBuffer));
|
|
ReplaceInstanceMethod(contextClass, "clearDrawable", reinterpret_cast<IMP>(ContextClearDrawable));
|
|
ReplaceInstanceMethod(contextClass, "update", reinterpret_cast<IMP>(ContextUpdate));
|
|
ReplaceInstanceMethod(contextClass, "setValues:forParameter:", reinterpret_cast<IMP>(ContextSetValues));
|
|
ReplaceInstanceMethod(contextClass, "getValues:forParameter:", reinterpret_cast<IMP>(ContextGetValues));
|
|
ReplaceInstanceMethod(contextClass, "CGLContextObj", reinterpret_cast<IMP>(ContextCGLContextObj));
|
|
ReplaceInstanceMethod(contextClass, "pixelFormat", reinterpret_cast<IMP>(ContextPixelFormat));
|
|
ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast<IMP>(ContextDealloc), &g_contextDealloc);
|
|
|
|
MGLOG_I("NSOpenGLImpl hooks installed");
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
void InstallHooks() {
|
|
const std::lock_guard<std::mutex> lock(HookInstallMutex());
|
|
if (!HooksInstalled()) {
|
|
// Do not permanently consume the install attempt when the OpenGL
|
|
// framework has not registered its Objective-C classes yet. The
|
|
// dyld bootstrap normally runs after framework dependencies, but
|
|
// an explicitly loaded/static-linked MobileGL can arrive earlier.
|
|
HooksInstalled() = InstallHooksOnce();
|
|
}
|
|
}
|
|
} // namespace MobileGL::MG_Impl::NSOpenGLImpl
|
|
|
|
namespace {
|
|
// SDL's Cocoa backend creates NSOpenGLPixelFormat/NSOpenGLContext before
|
|
// its first dlsym("glGetString") or other MobileGL host-API call. Install
|
|
// only the lightweight Objective-C dispatch hooks while the injected dylib
|
|
// is loading so those first Cocoa objects are routed through CGLImpl. The
|
|
// hooked context constructor reaches EGLImpl::GetDisplay(), which performs
|
|
// the full, thread-safe MobileGL initialization outside this bootstrap.
|
|
//
|
|
// There is intentionally no matching destructor: backend teardown remains
|
|
// owned by the EGL lifecycle and process-exit globals remain leak-at-exit.
|
|
__attribute__((constructor)) void BootstrapNSOpenGLHooks() {
|
|
MobileGL::MG_Impl::NSOpenGLImpl::InstallHooks();
|
|
}
|
|
} // namespace
|
|
#endif
|