From 241ed377b4da8a5948656a2b7e971e734798a8d1 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 28 Jul 2026 11:54:50 -0400 Subject: [PATCH] [Fix] (macOS): harden Cocoa context setup and isolate embedded glslang --- CMakeLists.txt | 14 ++++++ MobileGL/Init.cpp | 10 ++-- MobileGL/Init.h | 7 +-- MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp | 33 +++++++++++++ MobileGL/MG_Impl/CGLImpl/CGLImpl.h | 2 + .../MG_Impl/CGLImpl/Exporting/Definitions.cpp | 8 ++++ .../MG_Impl/DyldInterpose/DyldInterpose.cpp | 46 +++++++++++++++++++ .../MG_Impl/DyldInterpose/ExportedSymbols.txt | 10 ++++ MobileGL/MG_Impl/GetProcAddress.cpp | 2 + .../MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp | 40 ++++++++++++++-- 10 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 8778865b..01457a4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -455,8 +455,21 @@ if (ANDROID) endif() if (APPLE AND NOT MOBILEGL_IOS) + # MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When + # this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++ + # symbols interposes incompatible copies embedded by host libraries such + # as shaderc. Keep only the public GL/EGL/CGL loader surface globally + # visible; GetProcAddress can still return pointers to hidden internals. + set(MOBILEGL_MACOS_EXPORTED_SYMBOLS + "${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt") + target_link_options(${CMAKE_PROJECT_NAME} PRIVATE + "LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}") + set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY + LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}") + target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC "-framework Cocoa" + "-framework CoreVideo" "-framework QuartzCore" "-framework Foundation" "-framework OpenGL" @@ -464,6 +477,7 @@ if (APPLE AND NOT MOBILEGL_IOS) if(TARGET ${CMAKE_PROJECT_NAME}_s) target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC "-framework Cocoa" + "-framework CoreVideo" "-framework QuartzCore" "-framework Foundation" "-framework OpenGL" diff --git a/MobileGL/Init.cpp b/MobileGL/Init.cpp index b277e170..80868a9f 100644 --- a/MobileGL/Init.cpp +++ b/MobileGL/Init.cpp @@ -107,9 +107,11 @@ namespace MobileGL { // (EGL/WGL/CGL): initialization happens lazily on the first entry point // via EnsureInitialized(), and full teardown happens deterministically // when the last EGL display is terminated with nothing current (EGLImpl - // calls Destroy()). There is intentionally no static constructor, no - // static destructor, and no DllMain: the global singletons use - // leak-at-exit storage (see GlobalObjects.cpp), so a process that exits + // calls Destroy()). There is intentionally no backend-initializing static + // constructor, no static destructor, and no DllMain: the global singletons + // use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits // without eglTerminate simply leaks them to the OS instead of running - // backend destructors during static teardown. + // backend destructors during static teardown. macOS has a lightweight + // dyld constructor that installs NSOpenGL dispatch hooks only; full backend + // initialization still enters here from the first hooked CGL context. } // namespace MobileGL diff --git a/MobileGL/Init.h b/MobileGL/Init.h index 724f1a8e..c2cf887c 100644 --- a/MobileGL/Init.h +++ b/MobileGL/Init.h @@ -13,9 +13,10 @@ namespace MobileGL { void Initialize(); // Thread-safe, idempotent, and re-entrant wrapper around Initialize(). // Host layers (EGL/WGL/CGL entry points) call this lazily on first use so - // MobileGL's lifecycle never depends on ELF/DLL static constructors, and - // so a fresh init can follow a full Destroy() (e.g. after the last - // eglTerminate). + // full backend initialization never depends on ELF/DLL static constructors, + // and so a fresh init can follow a full Destroy() (e.g. after the last + // eglTerminate). The macOS dyld bootstrap installs only lightweight + // NSOpenGL method hooks. void EnsureInitialized(); void Destroy(); diff --git a/MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp b/MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp index e1e6e9af..f8410b98 100644 --- a/MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp +++ b/MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp @@ -24,6 +24,7 @@ namespace MobileGL::MG_Impl::CGLImpl { GLint Samples = 0; GLint Profile = kCGLOGLPVersion_3_2_Core; GLint RendererId = 0x4d474c; + GLint DisplayMask = 0; }; struct ContextObject { @@ -134,6 +135,9 @@ namespace MobileGL::MG_Impl::CGLImpl { case kCGLPFARendererID: pixelFormat.RendererId = value; break; + case kCGLPFADisplayMask: + pixelFormat.DisplayMask = value; + break; default: break; } @@ -343,6 +347,9 @@ namespace MobileGL::MG_Impl::CGLImpl { case kCGLPFARendererID: *value = pixelFormat->RendererId; return kCGLNoError; + case kCGLPFADisplayMask: + *value = pixelFormat->DisplayMask; + return kCGLNoError; case kCGLPFAOpenGLProfile: *value = pixelFormat->Profile; return kCGLNoError; @@ -481,6 +488,32 @@ namespace MobileGL::MG_Impl::CGLImpl { return it == currentContexts.end() ? nullptr : it->second; } + CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen) { + const std::lock_guard lock(RegistryMutex()); + auto* object = TryGetContext(ctx); + if (!object) { + return kCGLBadContext; + } + if (screen != 0) { + return kCGLBadValue; + } + object->VirtualScreen = screen; + return kCGLNoError; + } + + CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen) { + const std::lock_guard lock(RegistryMutex()); + auto* object = TryGetContext(ctx); + if (!object) { + return kCGLBadContext; + } + if (!screen) { + return kCGLBadAddress; + } + *screen = object->VirtualScreen; + return kCGLNoError; + } + CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) { const std::lock_guard lock(RegistryMutex()); auto* object = TryGetContext(ctx); diff --git a/MobileGL/MG_Impl/CGLImpl/CGLImpl.h b/MobileGL/MG_Impl/CGLImpl/CGLImpl.h index 7f6bf28b..cc482ba4 100644 --- a/MobileGL/MG_Impl/CGLImpl/CGLImpl.h +++ b/MobileGL/MG_Impl/CGLImpl/CGLImpl.h @@ -32,6 +32,8 @@ namespace MobileGL::MG_Impl::CGLImpl { CGLError SetCurrentContext(CGLContextObj ctx); CGLContextObj GetCurrentContext(); + CGLError SetVirtualScreen(CGLContextObj ctx, GLint screen); + CGLError GetVirtualScreen(CGLContextObj ctx, GLint* screen); CGLError SetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params); CGLError GetParameter(CGLContextObj ctx, CGLContextParameter pname, GLint* params); CGLError UpdateContext(CGLContextObj ctx); diff --git a/MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.cpp b/MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.cpp index fa066450..32d4b65d 100644 --- a/MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.cpp +++ b/MobileGL/MG_Impl/CGLImpl/Exporting/Definitions.cpp @@ -71,6 +71,14 @@ MOBILEGL_CGL_API CGLContextObj CGLGetCurrentContext(void) { return MobileGL::MG_Impl::CGLImpl::GetCurrentContext(); } +MOBILEGL_CGL_API CGLError CGLSetVirtualScreen(CGLContextObj ctx, GLint screen) { + return MobileGL::MG_Impl::CGLImpl::SetVirtualScreen(ctx, screen); +} + +MOBILEGL_CGL_API CGLError CGLGetVirtualScreen(CGLContextObj ctx, GLint* screen) { + return MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(ctx, screen); +} + MOBILEGL_CGL_API CGLError CGLSetParameter(CGLContextObj ctx, CGLContextParameter pname, const GLint* params) { return MobileGL::MG_Impl::CGLImpl::SetParameter(ctx, pname, params); } diff --git a/MobileGL/MG_Impl/DyldInterpose/DyldInterpose.cpp b/MobileGL/MG_Impl/DyldInterpose/DyldInterpose.cpp index 3ee561f6..e73ce516 100644 --- a/MobileGL/MG_Impl/DyldInterpose/DyldInterpose.cpp +++ b/MobileGL/MG_Impl/DyldInterpose/DyldInterpose.cpp @@ -10,8 +10,12 @@ #if defined(__APPLE__) +#include "MG_Impl/CGLImpl/CGLImpl.h" #include "MG_Impl/GetProcAddress.h" +#include +#include +#include #include namespace { @@ -47,10 +51,52 @@ namespace { return dlsym(handle, symbol); } + CGDirectDisplayID DisplayForMask(GLint displayMask) { + constexpr std::uint32_t MaxDisplays = sizeof(CGOpenGLDisplayMask) * 8; + CGDirectDisplayID displays[MaxDisplays] = {}; + std::uint32_t displayCount = 0; + if (displayMask != 0 && + CGGetActiveDisplayList(MaxDisplays, displays, &displayCount) == kCGErrorSuccess) { + const auto mask = static_cast(displayMask); + for (std::uint32_t i = 0; i < displayCount; ++i) { + if ((CGDisplayIDToOpenGLDisplayMask(displays[i]) & mask) != 0) { + return displays[i]; + } + } + } + return CGMainDisplayID(); + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + CVReturn MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext( + CVDisplayLinkRef displayLink, + CGLContextObj context, + CGLPixelFormatObj pixelFormat) { + GLint virtualScreen = 0; + if (MobileGL::MG_Impl::CGLImpl::GetVirtualScreen(context, &virtualScreen) == kCGLNoError) { + GLint displayMask = 0; + if (!displayLink || + MobileGL::MG_Impl::CGLImpl::DescribePixelFormat( + pixelFormat, virtualScreen, kCGLPFADisplayMask, &displayMask) != kCGLNoError) { + return kCVReturnInvalidArgument; + } + return CVDisplayLinkSetCurrentCGDisplay(displayLink, DisplayForMask(displayMask)); + } + + using OriginalFunction = CVReturn (*)(CVDisplayLinkRef, CGLContextObj, CGLPixelFormatObj); + static const auto original = reinterpret_cast( + dlsym(RTLD_NEXT, "CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext")); + return original ? original(displayLink, context, pixelFormat) : kCVReturnError; + } + __attribute__((used)) static const DyldInterposeEntry kMobileGLDyldInterpose[] __attribute__((section("__DATA,__interpose"))) = { {reinterpret_cast(MobileGLDlsym), reinterpret_cast(dlsym)}, + {reinterpret_cast(MobileGLCVDisplayLinkSetCurrentCGDisplayFromOpenGLContext), + reinterpret_cast(CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext)}, }; +#pragma clang diagnostic pop } // namespace #endif diff --git a/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt b/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt new file mode 100644 index 00000000..2653e3ef --- /dev/null +++ b/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt @@ -0,0 +1,10 @@ +# Public CGL entry points. +_CGL* + +# Public EGL entry points. +_egl* + +# Public OpenGL and GLX entry points. OpenGL function names always use an +# uppercase letter or digit after the "gl" prefix; excluding lowercase here +# deliberately prevents glslang_* from matching this pattern. +_gl[A-Z0-9]* diff --git a/MobileGL/MG_Impl/GetProcAddress.cpp b/MobileGL/MG_Impl/GetProcAddress.cpp index 811e8986..a38458ee 100644 --- a/MobileGL/MG_Impl/GetProcAddress.cpp +++ b/MobileGL/MG_Impl/GetProcAddress.cpp @@ -85,6 +85,8 @@ namespace MobileGL::MG_Impl { GETPROC(CGLGetPixelFormat, name); GETPROC(CGLSetCurrentContext, name); GETPROC(CGLGetCurrentContext, name); + GETPROC(CGLSetVirtualScreen, name); + GETPROC(CGLGetVirtualScreen, name); GETPROC(CGLSetParameter, name); GETPROC(CGLGetParameter, name); GETPROC(CGLUpdateContext, name); diff --git a/MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp b/MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp index b23b848b..5907ff31 100644 --- a/MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp +++ b/MobileGL/MG_Impl/NSOpenGLImpl/NSOpenGLImpl.cpp @@ -29,10 +29,19 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl { char kContextViewKey; char kContextLayerKey; - std::once_flag g_installOnce; 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 Fn ObjcMsgSend() { return reinterpret_cast(objc_msgSend); @@ -431,12 +440,12 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl { method_setImplementation(method, replacement); } - void InstallHooksOnce() { + Bool InstallHooksOnce() { Class pixelFormatClass = objc_getClass("NSOpenGLPixelFormat"); Class contextClass = objc_getClass("NSOpenGLContext"); if (!pixelFormatClass || !contextClass) { MGLOG_W("NSOpenGLImpl: NSOpenGL classes are not loaded; hooks not installed"); - return; + return false; } ReplaceInstanceMethod(pixelFormatClass, "initWithAttributes:", @@ -471,11 +480,34 @@ namespace MobileGL::MG_Impl::NSOpenGLImpl { ReplaceInstanceMethod(contextClass, "dealloc", reinterpret_cast(ContextDealloc), &g_contextDealloc); MGLOG_I("NSOpenGLImpl hooks installed"); + return true; } } // namespace void InstallHooks() { - std::call_once(g_installOnce, InstallHooksOnce); + const std::lock_guard 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