[Feat] (MG_Backend/DirectVulkan): support macOS Metal surfaces

This commit is contained in:
2026-06-28 14:19:51 +08:00
parent 46f3192c67
commit 0ef9c76224
12 changed files with 410 additions and 15 deletions
+29 -7
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.22.1)
if(NOT UNIX OR APPLE OR ANDROID)
message(STATUS "MobileGL trace replay runner is only enabled on Linux")
if(NOT UNIX OR ANDROID)
message(STATUS "MobileGL trace replay runner is only enabled on desktop Unix")
return()
endif()
@@ -140,12 +140,16 @@ target_link_libraries(mobilegl_trace_common PUBLIC
add_library(mobilegl_trace_glproc STATIC
"${APITRACE_GENERATED_DIR}/glproc.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_glproc_mobilegl.cpp")
if(APPLE)
target_compile_definitions(mobilegl_trace_glproc PRIVATE GL_SILENCE_DEPRECATION)
endif()
target_include_directories(mobilegl_trace_glproc PUBLIC
"${CMAKE_SOURCE_DIR}/include"
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch"
"${APITRACE_ROOT}/lib/os"
"${APITRACE_ROOT}/thirdparty/khronos")
target_link_libraries(mobilegl_trace_glproc PUBLIC mobilegl_trace_apitrace_os dl)
target_link_libraries(mobilegl_trace_glproc PUBLIC mobilegl_trace_apitrace_os ${CMAKE_DL_LIBS})
execute_process(
COMMAND ${Python3_EXECUTABLE} "${APITRACE_ROOT}/retrace/glretrace.py"
@@ -221,6 +225,15 @@ add_library(mobilegl_trace_glretrace_common STATIC
"${APITRACE_ROOT}/retrace/metric_helper.cpp"
"${APITRACE_ROOT}/retrace/metric_writer.cpp"
"${MOBILEGL_TRACE_ROOT}/apitrace_glws_egl.cpp")
if(APPLE)
set(MOBILEGL_TRACE_APPLE_FRAMEWORKS
"-framework Cocoa"
"-framework QuartzCore"
"-framework Foundation")
target_sources(mobilegl_trace_glretrace_common PRIVATE
"${MOBILEGL_TRACE_ROOT}/macos_cgs_surface_bounds.cpp")
target_compile_definitions(mobilegl_trace_glretrace_common PRIVATE GL_SILENCE_DEPRECATION)
endif()
target_include_directories(mobilegl_trace_glretrace_common PUBLIC
"${APITRACE_GENERATED_DIR}"
"${APITRACE_ROOT}/dispatch"
@@ -244,8 +257,7 @@ target_compile_definitions(mobilegl_trace_replay PRIVATE
target_include_directories(mobilegl_trace_replay PRIVATE
"${MOBILEGL_TRACE_SHARED_CPP_DIR}"
"${APITRACE_ROOT}/lib/image")
target_link_libraries(mobilegl_trace_replay PRIVATE
"-Wl,--start-group"
set(MOBILEGL_TRACE_REPLAY_LINK_LIBRARIES
mobilegl_trace_glretrace_common
mobilegl_trace_retrace_common
mobilegl_trace_image
@@ -253,8 +265,18 @@ target_link_libraries(mobilegl_trace_replay PRIVATE
mobilegl_trace_glhelpers
mobilegl_trace_glproc
mobilegl_trace_apitrace_os
"-Wl,--end-group"
dl)
${CMAKE_DL_LIBS})
if(APPLE)
target_link_libraries(mobilegl_trace_replay PRIVATE
${MOBILEGL_TRACE_REPLAY_LINK_LIBRARIES}
${MOBILEGL_TRACE_APPLE_FRAMEWORKS})
else()
target_link_libraries(mobilegl_trace_replay PRIVATE
"-Wl,--start-group"
${MOBILEGL_TRACE_REPLAY_LINK_LIBRARIES}
"-Wl,--end-group")
endif()
if(MOBILEGL_TRACE_REPLAY_MOBILEGL_LIBRARY)
set(mobilegl_trace_replay_mobilegl_library "${MOBILEGL_TRACE_REPLAY_MOBILEGL_LIBRARY}")
@@ -1,6 +1,16 @@
#include "glproc.hpp"
#if defined(__has_include)
#if __has_include(<GLES3/gl3.h>)
#include <GLES3/gl3.h>
#elif __has_include(<GLES3/gl32.h>)
#include <GLES3/gl32.h>
#else
#error "OpenGL ES 3 headers are required"
#endif
#else
#include <GLES3/gl3.h>
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
+213 -3
View File
@@ -10,12 +10,20 @@
#include <dlfcn.h>
#include <iostream>
#if defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
#include <objc/message.h>
#include <objc/objc.h>
#include <objc/runtime.h>
#endif
namespace {
using PfnEglBindApi = EGLBoolean (*)(EGLenum);
using PfnEglChooseConfig = EGLBoolean (*)(EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *);
using PfnEglCreateContext = EGLContext (*)(EGLDisplay, EGLConfig, EGLContext, const EGLint *);
using PfnEglCreatePbufferSurface = EGLSurface (*)(EGLDisplay, EGLConfig, const EGLint *);
using PfnEglCreateWindowSurface = EGLSurface (*)(EGLDisplay, EGLConfig, EGLNativeWindowType, const EGLint *);
using PfnEglDestroyContext = EGLBoolean (*)(EGLDisplay, EGLContext);
using PfnEglDestroySurface = EGLBoolean (*)(EGLDisplay, EGLSurface);
using PfnEglGetConfigAttrib = EGLBoolean (*)(EGLDisplay, EGLConfig, EGLint, EGLint *);
@@ -33,6 +41,7 @@ struct EglFns {
PfnEglChooseConfig chooseConfig = nullptr;
PfnEglCreateContext createContext = nullptr;
PfnEglCreatePbufferSurface createPbufferSurface = nullptr;
PfnEglCreateWindowSurface createWindowSurface = nullptr;
PfnEglDestroyContext destroyContext = nullptr;
PfnEglDestroySurface destroySurface = nullptr;
PfnEglGetConfigAttrib getConfigAttrib = nullptr;
@@ -54,6 +63,136 @@ int gRequestedWidth = 0;
int gRequestedHeight = 0;
bool gPrintedGlIdentity = false;
#if defined(__APPLE__)
constexpr unsigned long kNSWindowStyleMaskTitled = 1ul << 0;
constexpr unsigned long kNSWindowStyleMaskClosable = 1ul << 1;
constexpr unsigned long kNSWindowStyleMaskMiniaturizable = 1ul << 2;
constexpr unsigned long kNSWindowStyleMaskResizable = 1ul << 3;
constexpr unsigned long kNSBackingStoreBuffered = 2;
constexpr long kNSApplicationActivationPolicyRegular = 0;
constexpr unsigned long long kNSEventMaskAny = ~0ull;
template <typename Fn>
Fn ObjcMsgSend() {
return reinterpret_cast<Fn>(objc_msgSend);
}
id SendId(id receiver, const char *selector) {
return ObjcMsgSend<id (*)(id, SEL)>()(receiver, sel_registerName(selector));
}
void SendVoid(id receiver, const char *selector) {
ObjcMsgSend<void (*)(id, SEL)>()(receiver, sel_registerName(selector));
}
void SendVoidId(id receiver, const char *selector, id value) {
ObjcMsgSend<void (*)(id, SEL, id)>()(receiver, sel_registerName(selector), value);
}
void SendVoidBool(id receiver, const char *selector, bool value) {
ObjcMsgSend<void (*)(id, SEL, bool)>()(receiver, sel_registerName(selector), value);
}
void SendVoidLong(id receiver, const char *selector, long value) {
ObjcMsgSend<void (*)(id, SEL, long)>()(receiver, sel_registerName(selector), value);
}
void SendVoidCGRect(id receiver, const char *selector, CGRect value) {
ObjcMsgSend<void (*)(id, SEL, CGRect)>()(receiver, sel_registerName(selector), value);
}
void SendVoidCGSize(id receiver, const char *selector, CGSize value) {
ObjcMsgSend<void (*)(id, SEL, CGSize)>()(receiver, sel_registerName(selector), value);
}
id Retain(id object) {
return object ? SendId(object, "retain") : nil;
}
void Release(id object) {
if (object) {
SendVoid(object, "release");
}
}
id SharedApplication() {
id appClass = reinterpret_cast<id>(objc_getClass("NSApplication"));
return appClass ? SendId(appClass, "sharedApplication") : nil;
}
void PumpMacOSEvents() {
id app = SharedApplication();
if (!app) {
return;
}
id distantPast = SendId(reinterpret_cast<id>(objc_getClass("NSDate")), "distantPast");
id stringClass = reinterpret_cast<id>(objc_getClass("NSString"));
id defaultRunLoopMode = ObjcMsgSend<id (*)(id, SEL, const char *)>()(
stringClass, sel_registerName("stringWithUTF8String:"), "kCFRunLoopDefaultMode");
while (true) {
id event = ObjcMsgSend<id (*)(id, SEL, unsigned long long, id, id, bool)>()(
app, sel_registerName("nextEventMatchingMask:untilDate:inMode:dequeue:"),
kNSEventMaskAny, distantPast, defaultRunLoopMode, true);
if (!event) {
break;
}
SendVoidId(app, "sendEvent:", event);
}
SendVoid(app, "updateWindows");
}
void EnsureApplicationActive() {
id app = SharedApplication();
if (!app) {
return;
}
SendVoidLong(app, "setActivationPolicy:", kNSApplicationActivationPolicyRegular);
SendVoidBool(app, "activateIgnoringOtherApps:", true);
}
id CreateMetalWindow(int width, int height, id *outLayer) {
EnsureApplicationActive();
id windowClass = reinterpret_cast<id>(objc_getClass("NSWindow"));
id metalLayerClass = reinterpret_cast<id>(objc_getClass("CAMetalLayer"));
if (!windowClass || !metalLayerClass) {
std::cerr << "error: failed to resolve NSWindow/CAMetalLayer\n";
return nil;
}
const auto surfaceWidth = static_cast<CGFloat>(std::max(width, 1));
const auto surfaceHeight = static_cast<CGFloat>(std::max(height, 1));
const CGRect frame = {{80.0, 80.0}, {surfaceWidth, surfaceHeight}};
constexpr unsigned long style = kNSWindowStyleMaskTitled | kNSWindowStyleMaskClosable |
kNSWindowStyleMaskMiniaturizable | kNSWindowStyleMaskResizable;
id window = SendId(windowClass, "alloc");
window = ObjcMsgSend<id (*)(id, SEL, CGRect, unsigned long, unsigned long, bool)>()(
window, sel_registerName("initWithContentRect:styleMask:backing:defer:"),
frame, style, kNSBackingStoreBuffered, false);
if (!window) {
std::cerr << "error: failed to create NSWindow\n";
return nil;
}
id contentView = SendId(window, "contentView");
id layer = SendId(metalLayerClass, "layer");
if (!contentView || !layer) {
Release(window);
std::cerr << "error: failed to create CAMetalLayer\n";
return nil;
}
Retain(layer);
SendVoidBool(contentView, "setWantsLayer:", true);
SendVoidCGRect(layer, "setFrame:", {{0.0, 0.0}, {surfaceWidth, surfaceHeight}});
SendVoidCGSize(layer, "setDrawableSize:", {surfaceWidth, surfaceHeight});
SendVoidId(contentView, "setLayer:", layer);
ObjcMsgSend<void (*)(id, SEL, id)>()(window, sel_registerName("makeKeyAndOrderFront:"), nil);
PumpMacOSEvents();
*outLayer = layer;
return window;
}
#endif
int ResolveWidth(int width) {
if (gRequestedWidth > 0) {
return gRequestedWidth;
@@ -108,6 +247,7 @@ bool LoadEgl() {
Load(gEgl.chooseConfig, "eglChooseConfig") &&
Load(gEgl.createContext, "eglCreateContext") &&
Load(gEgl.createPbufferSurface, "eglCreatePbufferSurface") &&
Load(gEgl.createWindowSurface, "eglCreateWindowSurface") &&
Load(gEgl.destroyContext, "eglDestroyContext") &&
Load(gEgl.destroySurface, "eglDestroySurface") &&
Load(gEgl.getConfigAttrib, "eglGetConfigAttrib") &&
@@ -120,6 +260,11 @@ bool LoadEgl() {
Load(gEgl.terminate, "eglTerminate");
}
bool TraceReplayWantsWindowSurface() {
const char *mode = std::getenv("MOBILEGL_TRACE_SURFACE");
return mode != nullptr && std::strcmp(mode, "window") == 0;
}
void PrintGlIdentityOnce() {
if (gPrintedGlIdentity) {
return;
@@ -160,6 +305,10 @@ public:
class EglDrawable final : public glws::Drawable {
public:
EGLSurface surface = EGL_NO_SURFACE;
#if defined(__APPLE__)
id window = nil;
id metalLayer = nil;
#endif
EglDrawable(const EglVisual *visual, int width, int height, bool pbuffer)
: Drawable(visual, width, height, pbuffer) {
@@ -168,6 +317,12 @@ public:
~EglDrawable() override {
destroySurface();
#if defined(__APPLE__)
Release(metalLayer);
metalLayer = nil;
Release(window);
window = nil;
#endif
}
void resize(int w, int h) override {
@@ -187,6 +342,12 @@ public:
void show() override {
visible = true;
#if defined(__APPLE__)
if (window) {
ObjcMsgSend<void (*)(id, SEL, id)>()(window, sel_registerName("makeKeyAndOrderFront:"), nil);
PumpMacOSEvents();
}
#endif
}
void swapBuffers() override {
@@ -203,13 +364,50 @@ public:
setenv("MOBILEGL_PRESENT_CURRENT_CALL", callNo, 1);
gEgl.swapBuffers(gDisplay, surface);
unsetenv("MOBILEGL_PRESENT_CURRENT_CALL");
#if defined(__APPLE__)
PumpMacOSEvents();
#endif
}
private:
bool shouldCreateWindowSurface() const {
if (pbuffer) {
return false;
}
const char *mode = std::getenv("MOBILEGL_TRACE_SURFACE");
return mode != nullptr && std::strcmp(mode, "window") == 0;
}
void createSurface() {
const int surfaceWidth = ResolveWidth(width);
const int surfaceHeight = ResolveHeight(height);
#if defined(__APPLE__)
if (shouldCreateWindowSurface()) {
if (!window) {
window = CreateMetalWindow(surfaceWidth, surfaceHeight, &metalLayer);
} else if (metalLayer) {
const auto w = static_cast<CGFloat>(std::max(surfaceWidth, 1));
const auto h = static_cast<CGFloat>(std::max(surfaceHeight, 1));
SendVoidCGSize(metalLayer, "setDrawableSize:", {w, h});
SendVoidCGRect(metalLayer, "setFrame:", {{0.0, 0.0}, {w, h}});
}
if (metalLayer) {
surface = gEgl.createWindowSurface(
gDisplay,
static_cast<const EglVisual *>(visual)->config,
reinterpret_cast<EGLNativeWindowType>(metalLayer),
nullptr);
}
if (surface == EGL_NO_SURFACE) {
std::cerr << "error: EGL window surface creation failed: 0x" << std::hex
<< gEgl.getError() << std::dec << "\n";
}
return;
}
#endif
const EGLint attribs[] = {
EGL_WIDTH, ResolveWidth(width),
EGL_HEIGHT, ResolveHeight(height),
EGL_WIDTH, surfaceWidth,
EGL_HEIGHT, surfaceHeight,
EGL_NONE,
};
surface = gEgl.createPbufferSurface(gDisplay, static_cast<const EglVisual *>(visual)->config, attribs);
@@ -224,6 +422,14 @@ private:
gEgl.destroySurface(gDisplay, surface);
surface = EGL_NO_SURFACE;
}
#if defined(__APPLE__)
if (!shouldCreateWindowSurface()) {
Release(metalLayer);
metalLayer = nil;
Release(window);
window = nil;
}
#endif
}
};
@@ -323,8 +529,9 @@ Visual *createVisual(bool doubleBuffer, unsigned samples, Profile profile) {
return nullptr;
}
const EGLint surfaceType = TraceReplayWantsWindowSurface() ? EGL_WINDOW_BIT : EGL_PBUFFER_BIT;
const EGLint attribs[] = {
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_SURFACE_TYPE, surfaceType,
EGL_RENDERABLE_TYPE, visual->api == EGL_OPENGL_ES_API ? EGL_OPENGL_ES3_BIT : EGL_OPENGL_BIT,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
@@ -373,6 +580,9 @@ bool makeCurrentInternal(Drawable *drawable, Drawable *readable, Context *contex
}
bool processEvents() {
#if defined(__APPLE__)
PumpMacOSEvents();
#endif
return false;
}
@@ -0,0 +1,24 @@
#include <Carbon/Carbon.h>
using CGSConnectionID = void*;
using CGSSurfaceID = int;
extern "C" bool mobilegl_trace_get_drawable_bounds(int* width, int* height);
extern "C" OSStatus CGSGetSurfaceBounds(CGSConnectionID, CGWindowID, CGSSurfaceID, CGRect* rect) {
if (rect == nullptr) {
return -1;
}
int width = 0;
int height = 0;
if (!mobilegl_trace_get_drawable_bounds(&width, &height) || width <= 0 || height <= 0) {
return -1;
}
rect->origin.x = 0.0;
rect->origin.y = 0.0;
rect->size.width = static_cast<CGFloat>(width);
rect->size.height = static_cast<CGFloat>(height);
return 0;
}
+6
View File
@@ -20,6 +20,8 @@ void PrintUsage(const char *argv0) {
<< " --mobilegl-library PATH libMobileGL.so path (default: libMobileGL.so)\n"
<< " --width N Replay surface width override\n"
<< " --height N Replay surface height override\n"
<< " --window-surface Replay to a native window surface\n"
<< " --pbuffer-surface Replay to an EGL pbuffer surface (default)\n"
<< " --ssim-threshold N Minimum SSIM required to pass (default: 0.99)\n"
<< " --crop-x N Compare crop x\n"
<< " --crop-y N Compare crop y\n"
@@ -94,6 +96,10 @@ bool ParseArgs(int argc, char **argv, mobilegl_trace::Request &request) {
if (!ReadInt(argc, argv, i, request.width)) return false;
} else if (arg == "--height") {
if (!ReadInt(argc, argv, i, request.height)) return false;
} else if (arg == "--window-surface") {
request.usePbuffer = false;
} else if (arg == "--pbuffer-surface") {
request.usePbuffer = true;
} else if (arg == "--ssim-threshold") {
if (!ReadDouble(argc, argv, i, request.ssimThreshold)) return false;
} else if (arg == "--crop-x") {