diff --git a/AI/Wrappers/CUtils/Util.c b/AI/Wrappers/CUtils/Util.c index c33d2e418e8..4764a619900 100644 --- a/AI/Wrappers/CUtils/Util.c +++ b/AI/Wrappers/CUtils/Util.c @@ -487,11 +487,7 @@ static void util_initFileSelector(const char* suffix) { fileSelectorSuffix = suffix; } -#if defined(__APPLE__) -static int util_fileSelector(struct dirent* fileDesc) { -#else static int util_fileSelector(const struct dirent* fileDesc) { -#endif return util_endsWith(fileDesc->d_name, fileSelectorSuffix); } diff --git a/include/Mac/AL/alext.h b/include/Mac/AL/alext.h new file mode 100644 index 00000000000..fe6d6c6378a --- /dev/null +++ b/include/Mac/AL/alext.h @@ -0,0 +1,51 @@ +/* OpenAL extensions header — vendored for macOS compatibility. + * + * Apple's OpenAL.framework does not include alext.h. + * These constants and typedefs are from OpenAL-Soft's alext.h + * for the ALC_SOFT_loopback extension used by the sound system. + * + * Functions are loaded at runtime via alcGetProcAddress(). + */ + +#ifndef AL_ALEXT_H +#define AL_ALEXT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ALC_SOFT_loopback */ +#define ALC_SOFT_loopback 1 +#define ALC_FORMAT_CHANNELS_SOFT 0x1990 +#define ALC_FORMAT_TYPE_SOFT 0x1991 + +/* Sample types */ +#define ALC_BYTE_SOFT 0x1400 +#define ALC_UNSIGNED_BYTE_SOFT 0x1401 +#define ALC_SHORT_SOFT 0x1402 +#define ALC_UNSIGNED_SHORT_SOFT 0x1403 +#define ALC_INT_SOFT 0x1404 +#define ALC_UNSIGNED_INT_SOFT 0x1405 +#define ALC_FLOAT_SOFT 0x1406 + +/* Channel configurations */ +#define ALC_MONO_SOFT 0x1500 +#define ALC_STEREO_SOFT 0x1501 +#define ALC_QUAD_SOFT 0x1503 +#define ALC_5POINT1_SOFT 0x1504 +#define ALC_6POINT1_SOFT 0x1505 +#define ALC_7POINT1_SOFT 0x1506 + +/* Loopback function pointer types */ +typedef ALCdevice* (ALC_APIENTRY *LPALCLOOPBACKOPENDEVICESOFT)(const ALCchar*); +typedef ALCboolean (ALC_APIENTRY *LPALCISRENDERFORMATSUPPORTEDSOFT)(ALCdevice*, ALCsizei, ALCenum, ALCenum); +typedef void (ALC_APIENTRY *LPALCRENDERSAMPLESSOFT)(ALCdevice*, ALCvoid*, ALCsizei); + +#ifdef __cplusplus +} +#endif + +#endif /* AL_ALEXT_H */ diff --git a/include/Mac/AL/efx.h b/include/Mac/AL/efx.h new file mode 100644 index 00000000000..3fa9a9bc70a --- /dev/null +++ b/include/Mac/AL/efx.h @@ -0,0 +1,149 @@ +/* OpenAL EFX extension header — vendored for macOS compatibility. + * + * Apple's OpenAL.framework does not include EFX headers. + * These constants and typedefs are from the OpenAL 1.1 EFX specification + * (originally distributed with OpenAL-Soft). + * + * Functions are loaded at runtime via alGetProcAddress() in EFXfuncs.cpp, + * so only compile-time constants and typedefs are needed here. + */ + +#ifndef AL_EFX_H +#define AL_EFX_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Effect types */ +#define AL_EFFECT_TYPE 0x8001 +#define AL_EFFECT_NULL 0x0000 +#define AL_EFFECT_REVERB 0x0001 +#define AL_EFFECT_CHORUS 0x0002 +#define AL_EFFECT_DISTORTION 0x0003 +#define AL_EFFECT_ECHO 0x0004 +#define AL_EFFECT_FLANGER 0x0005 +#define AL_EFFECT_FREQUENCY_SHIFTER 0x0006 +#define AL_EFFECT_VOCAL_MORPHER 0x0007 +#define AL_EFFECT_PITCH_SHIFTER 0x0008 +#define AL_EFFECT_RING_MODULATOR 0x0009 +#define AL_EFFECT_AUTOWAH 0x000A +#define AL_EFFECT_COMPRESSOR 0x000B +#define AL_EFFECT_EQUALIZER 0x000C +#define AL_EFFECT_EAXREVERB 0x8000 + +/* EAX Reverb effect parameters */ +#define AL_EAXREVERB_DENSITY 0x0001 +#define AL_EAXREVERB_DIFFUSION 0x0002 +#define AL_EAXREVERB_GAIN 0x0003 +#define AL_EAXREVERB_GAINHF 0x0004 +#define AL_EAXREVERB_GAINLF 0x0005 +#define AL_EAXREVERB_DECAY_TIME 0x0006 +#define AL_EAXREVERB_DECAY_HFRATIO 0x0007 +#define AL_EAXREVERB_DECAY_LFRATIO 0x0008 +#define AL_EAXREVERB_REFLECTIONS_GAIN 0x0009 +#define AL_EAXREVERB_REFLECTIONS_DELAY 0x000A +#define AL_EAXREVERB_REFLECTIONS_PAN 0x000B +#define AL_EAXREVERB_LATE_REVERB_GAIN 0x000C +#define AL_EAXREVERB_LATE_REVERB_DELAY 0x000D +#define AL_EAXREVERB_LATE_REVERB_PAN 0x000E +#define AL_EAXREVERB_ECHO_TIME 0x000F +#define AL_EAXREVERB_ECHO_DEPTH 0x0010 +#define AL_EAXREVERB_MODULATION_TIME 0x0011 +#define AL_EAXREVERB_MODULATION_DEPTH 0x0012 +#define AL_EAXREVERB_AIR_ABSORPTION_GAINHF 0x0013 +#define AL_EAXREVERB_HFREFERENCE 0x0014 +#define AL_EAXREVERB_LFREFERENCE 0x0015 +#define AL_EAXREVERB_ROOM_ROLLOFF_FACTOR 0x0016 +#define AL_EAXREVERB_DECAY_HFLIMIT 0x0017 + +/* Filter types */ +#define AL_FILTER_TYPE 0x8001 +#define AL_FILTER_NULL 0x0000 +#define AL_FILTER_LOWPASS 0x0001 +#define AL_FILTER_HIGHPASS 0x0002 +#define AL_FILTER_BANDPASS 0x0003 + +/* Lowpass filter parameters */ +#define AL_LOWPASS_GAIN 0x0001 +#define AL_LOWPASS_GAINHF 0x0002 + +/* Highpass filter parameters */ +#define AL_HIGHPASS_GAIN 0x0001 +#define AL_HIGHPASS_GAINLF 0x0002 + +/* Bandpass filter parameters */ +#define AL_BANDPASS_GAIN 0x0001 +#define AL_BANDPASS_GAINLF 0x0002 +#define AL_BANDPASS_GAINHF 0x0003 + +/* Auxiliary effect slot properties */ +#define AL_EFFECTSLOT_EFFECT 0x0001 +#define AL_EFFECTSLOT_GAIN 0x0002 +#define AL_EFFECTSLOT_AUXILIARY_SEND_AUTO 0x0003 +#define AL_EFFECTSLOT_NULL 0x0000 + +/* Context attribute for max auxiliary sends */ +#define ALC_MAX_AUXILIARY_SENDS 0x20003 + +/* Air absorption factor range */ +#define AL_MIN_AIR_ABSORPTION_FACTOR 0.0f +#define AL_MAX_AIR_ABSORPTION_FACTOR 10.0f + +/* Source properties for auxiliary send filter */ +#define AL_DIRECT_FILTER 0x20005 +#define AL_AUXILIARY_SEND_FILTER 0x20006 +#define AL_AIR_ABSORPTION_FACTOR 0x20007 +#define AL_DIRECT_FILTER_GAINHF_AUTO 0x2000B +#define AL_AUXILIARY_SEND_FILTER_GAIN_AUTO 0x2000C +#define AL_AUXILIARY_SEND_FILTER_GAINHF_AUTO 0x2000D + +/* EFX function pointer types — loaded at runtime via alGetProcAddress() */ + +/* Effect functions */ +typedef void (AL_APIENTRY *LPALGENEFFECTS)(ALsizei, ALuint*); +typedef void (AL_APIENTRY *LPALDELETEEFFECTS)(ALsizei, const ALuint*); +typedef ALboolean (AL_APIENTRY *LPALISEFFECT)(ALuint); +typedef void (AL_APIENTRY *LPALEFFECTI)(ALuint, ALenum, ALint); +typedef void (AL_APIENTRY *LPALEFFECTIV)(ALuint, ALenum, const ALint*); +typedef void (AL_APIENTRY *LPALEFFECTF)(ALuint, ALenum, ALfloat); +typedef void (AL_APIENTRY *LPALEFFECTFV)(ALuint, ALenum, const ALfloat*); +typedef void (AL_APIENTRY *LPALGETEFFECTI)(ALuint, ALenum, ALint*); +typedef void (AL_APIENTRY *LPALGETEFFECTIV)(ALuint, ALenum, ALint*); +typedef void (AL_APIENTRY *LPALGETEFFECTF)(ALuint, ALenum, ALfloat*); +typedef void (AL_APIENTRY *LPALGETEFFECTFV)(ALuint, ALenum, ALfloat*); + +/* Filter functions */ +typedef void (AL_APIENTRY *LPALGENFILTERS)(ALsizei, ALuint*); +typedef void (AL_APIENTRY *LPALDELETEFILTERS)(ALsizei, const ALuint*); +typedef ALboolean (AL_APIENTRY *LPALISFILTER)(ALuint); +typedef void (AL_APIENTRY *LPALFILTERI)(ALuint, ALenum, ALint); +typedef void (AL_APIENTRY *LPALFILTERIV)(ALuint, ALenum, const ALint*); +typedef void (AL_APIENTRY *LPALFILTERF)(ALuint, ALenum, ALfloat); +typedef void (AL_APIENTRY *LPALFILTERFV)(ALuint, ALenum, const ALfloat*); +typedef void (AL_APIENTRY *LPALGETFILTERI)(ALuint, ALenum, ALint*); +typedef void (AL_APIENTRY *LPALGETFILTERIV)(ALuint, ALenum, ALint*); +typedef void (AL_APIENTRY *LPALGETFILTERF)(ALuint, ALenum, ALfloat*); +typedef void (AL_APIENTRY *LPALGETFILTERFV)(ALuint, ALenum, ALfloat*); + +/* Auxiliary effect slot functions */ +typedef void (AL_APIENTRY *LPALGENAUXILIARYEFFECTSLOTS)(ALsizei, ALuint*); +typedef void (AL_APIENTRY *LPALDELETEAUXILIARYEFFECTSLOTS)(ALsizei, const ALuint*); +typedef ALboolean (AL_APIENTRY *LPALISAUXILIARYEFFECTSLOT)(ALuint); +typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint); +typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, const ALint*); +typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat); +typedef void (AL_APIENTRY *LPALAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, const ALfloat*); +typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTI)(ALuint, ALenum, ALint*); +typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTIV)(ALuint, ALenum, ALint*); +typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTF)(ALuint, ALenum, ALfloat*); +typedef void (AL_APIENTRY *LPALGETAUXILIARYEFFECTSLOTFV)(ALuint, ALenum, ALfloat*); + +#ifdef __cplusplus +} +#endif + +#endif /* AL_EFX_H */ diff --git a/rts/CMakeLists.txt b/rts/CMakeLists.txt index 001ea7cb6ba..dda8db290e5 100644 --- a/rts/CMakeLists.txt +++ b/rts/CMakeLists.txt @@ -36,6 +36,10 @@ endif() ### give error when not found find_package_static(DevIL REQUIRED) +# DevIL's CMake module sets IL_INCLUDE_DIR to the directory containing il.h (e.g. /include/IL) +# but code uses #include , so we need the parent directory +get_filename_component(_IL_PARENT_DIR "${IL_INCLUDE_DIR}" DIRECTORY) +include_directories(${_IL_PARENT_DIR}) ### Assemble common include dirs include_directories(BEFORE lib) @@ -106,7 +110,7 @@ if (USE_MIMALLOC) endif (USE_MIMALLOC) -if(UNIX AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD")) +if(UNIX AND NOT APPLE AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD")) find_package_static(Libunwind 1.4.0 REQUIRED) prefer_static_libs() find_library(LZMA_LIBRARY lzma) diff --git a/rts/Game/LoadScreen.cpp b/rts/Game/LoadScreen.cpp index e2a9b7e6b99..02a687d3e07 100644 --- a/rts/Game/LoadScreen.cpp +++ b/rts/Game/LoadScreen.cpp @@ -136,7 +136,21 @@ bool CLoadScreen::Init() // the global font), the latter will cause problems in GL4 { auto lock = CLoadLock::GetUniqueLock(); +#if defined(__APPLE__) + // The Lua intro was disabled on macOS as an EGL-path workaround (it uses + // the global font / gl.*Text which broke under the old core-profile + // shaders). With the compatibility-profile context + fixed font/render- + // buffer shaders it renders correctly (full BAR splash + progress), so + // it is enabled by default. Set SPRING_MAC_DISABLE_LUAINTRO=1 to skip it + // (falls back to a black load screen) if it ever misbehaves. + if (getenv("SPRING_MAC_DISABLE_LUAINTRO") != nullptr) { + LOG("[LoadScreen::%s] skipping CLuaIntro (SPRING_MAC_DISABLE_LUAINTRO)", __func__); + } else { + CLuaIntro::LoadFreeHandler(); + } +#else CLuaIntro::LoadFreeHandler(); +#endif } if (mtLoading) diff --git a/rts/Lua/LuaShaders.cpp b/rts/Lua/LuaShaders.cpp index 5109e5d64c3..3d473a576ea 100644 --- a/rts/Lua/LuaShaders.cpp +++ b/rts/Lua/LuaShaders.cpp @@ -657,6 +657,27 @@ int LuaShaders::CreateShader(lua_State* L) if (!ParseShaderTable(L, 1, "fragment", fragSrcs)) return 0; + if (!geomSrcs.empty()) { +#if defined(__APPLE__) + // macOS via Mesa Zink / KosmicKrisp on Apple Silicon: Vulkan reports + // geometryShader = false (Metal has no GS stage). Mesa advertises + // GL_MAX_GEOMETRY_OUTPUT_VERTICES > 0 anyway, so the GL query cannot + // detect the missing capability. Strip GS so the program at least + // links; widgets that need GS-style point expansion have a Lua-layer + // NoGS fallback (see Beyond-All-Reason PR for the dual-path widgets). + GLint maxGeomOutputVerts = 0; + glGetIntegerv(GL_MAX_GEOMETRY_OUTPUT_VERTICES, &maxGeomOutputVerts); + const GLenum err = glGetError(); + LOG_L(L_WARNING, "[LuaShaders::%s] GS check: GL_MAX_GEOMETRY_OUTPUT_VERTICES=%d, glErr=0x%x, geomSrcs=%d", + __func__, maxGeomOutputVerts, err, (int)geomSrcs.size()); + LOG_L(L_WARNING, + "[LuaShaders::%s] GS unconditionally stripped on macOS " + "(Metal has no GS stage). maxGeomVerts=%d", + __func__, maxGeomOutputVerts); + geomSrcs.clear(); +#endif // __APPLE__ + } + if (!ParseShaderTable(L, 1, "compute", compSrcs)) return 0; diff --git a/rts/Lua/LuaTextures.cpp b/rts/Lua/LuaTextures.cpp index 8d3a083dbef..0ae691d63c0 100644 --- a/rts/Lua/LuaTextures.cpp +++ b/rts/Lua/LuaTextures.cpp @@ -90,7 +90,9 @@ std::string LuaTextures::Create(const Texture& tex) } break; } - if (glGetError() != GL_NO_ERROR) { + if (const GLenum texErr = glGetError(); texErr != GL_NO_ERROR) { + LOG_L(L_ERROR, "[LuaTextures::%s] glTexImage failed: target=0x%x size=%dx%d fmt=0x%x dataFmt=0x%x dataType=0x%x border=%d glError=0x%x", + __func__, tex.target, tex.xsize, tex.ysize, tex.format, dataFormat, dataType, tex.border, texErr); glDeleteTextures(1, &texID); glBindTexture(tex.target, currentBinding); return ""; diff --git a/rts/Rendering/GL/glxHandler.cpp b/rts/Rendering/GL/glxHandler.cpp index ab7e687caa0..beb181799e5 100644 --- a/rts/Rendering/GL/glxHandler.cpp +++ b/rts/Rendering/GL/glxHandler.cpp @@ -1,3 +1,4 @@ +#ifndef __APPLE__ #include "glxHandler.h" #if !defined(HEADLESS) && !defined(_WIN32) && !defined(__APPLE__) @@ -60,4 +61,5 @@ bool GLX::GetVideoMemInfoMESA(int* memInfo) void GLX::Load(SDL_Window* window) {} void GLX::Unload() {} bool GLX::GetVideoMemInfoMESA(int* memInfo) { return false; } -#endif \ No newline at end of file +#endif +#endif // __APPLE__ diff --git a/rts/Rendering/GL/glxHandler.h b/rts/Rendering/GL/glxHandler.h index 2665fecece1..096f647be81 100644 --- a/rts/Rendering/GL/glxHandler.h +++ b/rts/Rendering/GL/glxHandler.h @@ -1,3 +1,4 @@ +#ifndef __APPLE__ #pragma once struct SDL_Window; @@ -10,4 +11,5 @@ struct GLX { static bool GetVideoMemInfoMESA(int* memInfo); private: static inline bool supported = false; -}; \ No newline at end of file +}; +#endif // __APPLE__ diff --git a/rts/Rendering/GL/myGL.cpp b/rts/Rendering/GL/myGL.cpp index 6e4396554b7..3ce6ad84f02 100644 --- a/rts/Rendering/GL/myGL.cpp +++ b/rts/Rendering/GL/myGL.cpp @@ -151,7 +151,11 @@ static bool GetVideoMemInfoATI(GLint* memInfo) static bool GetVideoMemInfoMESA(GLint* memInfo) { RECOIL_DETAILED_TRACY_ZONE; + #ifndef __APPLE__ return GLX::GetVideoMemInfoMESA(memInfo); +#else + return false; +#endif } #endif diff --git a/rts/Rendering/GlobalRendering.cpp b/rts/Rendering/GlobalRendering.cpp index be3c80e3a93..4c9677fa0c4 100644 --- a/rts/Rendering/GlobalRendering.cpp +++ b/rts/Rendering/GlobalRendering.cpp @@ -7,6 +7,315 @@ #include #include "GlobalRendering.h" + +#if defined(__APPLE__) && !defined(HEADLESS) +#include +#include +#include +#include +#include "System/Platform/Mac/MetalPresent.h" + +static EGLDisplay g_eglDisplay = EGL_NO_DISPLAY; +static EGLContext g_eglContext = EGL_NO_CONTEXT; +static EGLSurface g_eglSurface = EGL_NO_SURFACE; +static void* g_metalLayer = nullptr; // CAMetalLayer attached to the window's NSView +static int g_pbufW = 1280; // pbuffer (default framebuffer) dimensions +static int g_pbufH = 720; +static std::vector g_presentBuf; // reused glReadPixels staging buffer + +// Downsample-before-readback FBO. Engine renders at full +// pbuffer (Retina) resolution into the default FBO so UI layout is correct; +// before glReadPixels we glBlitFramebuffer (GL_LINEAR) into this smaller FBO, +// so the readback only pays for the smaller buffer. Enabled with +// SPRING_DOWNSAMPLE_READBACK=<2..8>. +// GL headers aren't included this early in the TU; use the underlying integer +// type. On every platform we target, GLuint is uint32_t == unsigned int, so +// taking &g_dsFBO and passing it to glGenFramebuffers is ABI-safe. +static unsigned int g_dsFBO = 0; +static unsigned int g_dsTex = 0; +static int g_dsW = 0; +static int g_dsH = 0; + +// Double-buffered Pixel Buffer Objects for async readback. +// Frame N submits glReadPixels into g_pbos[curIdx] (non-blocking GPU command); +// the previous frame's PBO is mapped and memcpy'd into the IOSurface. The +// glReadPixels stall (15-44ms while the GPU drains its pipeline) is hidden +// behind a 1-frame latency. Disable with SPRING_NO_PBO=1. +// +// The truly zero-copy path (render the engine FBO directly into the +// IOSurface that Metal samples — no readback at all) is blocked on Mesa +// upstream work: KosmicKrisp's VK_EXT_external_memory_metal needs to grow +// IOSurface/MTLTexture handle support (today it only handles MTLHeap), and +// Zink needs a new MESA_memory_object_metal GL extension to consume the +// resulting VkImage as a GL FBO color attachment. The KosmicKrisp lead +// (Aitor Camacho at LunarG) authored the underlying VK spec, so the +// Vulkan half is unusually low-risk; the Zink consumer is the LOC sink. +// Estimated ~1 month of focused upstream work — worth doing if the port +// commits long-term, not in scope for shipping today. +static unsigned int g_pbos[2] = { 0, 0 }; +static int g_pboW = 0; +static int g_pboH = 0; +static int g_pboCurIdx = 0; +static bool g_pboHasPrev = false; + +static void* GetNSViewFromSDLWindow(SDL_Window* window) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + if (!SDL_GetWindowWMInfo(window, &info)) + return nullptr; + id nswindow = (id)info.info.cocoa.window; + id view = ((id(*)(id, SEL))objc_msgSend)(nswindow, sel_registerName("contentView")); + + // Set up CAMetalLayer on the view BEFORE passing to EGL + // This prevents crashes in wsi_metal_layer_size + Class CAMetalLayerClass = objc_getClass("CAMetalLayer"); + if (CAMetalLayerClass && view) { + // [view setWantsLayer:YES] + ((void(*)(id, SEL, BOOL))objc_msgSend)(view, sel_registerName("setWantsLayer:"), YES); + + // Create CAMetalLayer + id metalLayer = ((id(*)(id, SEL))objc_msgSend)((id)CAMetalLayerClass, sel_registerName("layer")); + if (metalLayer) { + // Get view bounds + typedef struct { double x, y, w, h; } CGRectD; + CGRectD (*getBounds)(id, SEL) = (CGRectD(*)(id, SEL))objc_msgSend; + CGRectD bounds = getBounds(view, sel_registerName("bounds")); + + // [metalLayer setFrame:bounds] + ((void(*)(id, SEL, CGRectD))objc_msgSend)(metalLayer, sel_registerName("setFrame:"), bounds); + + // [metalLayer setOpaque:YES] + ((void(*)(id, SEL, BOOL))objc_msgSend)(metalLayer, sel_registerName("setOpaque:"), YES); + + // Get backing scale factor: [view window] -> [window backingScaleFactor] + id win = ((id(*)(id, SEL))objc_msgSend)(view, sel_registerName("window")); + if (win) { + double (*getScale)(id, SEL) = (double(*)(id, SEL))objc_msgSend; + double scale = getScale(win, sel_registerName("backingScaleFactor")); + // [metalLayer setContentsScale:scale] + ((void(*)(id, SEL, double))objc_msgSend)(metalLayer, sel_registerName("setContentsScale:"), scale); + } + + // [view setLayer:metalLayer] + ((void(*)(id, SEL, id))objc_msgSend)(view, sel_registerName("setLayer:"), metalLayer); + + return (void*)metalLayer; + } + } + return (void*)view; +} + +// Returns the SDL window's NSWindow.backingScaleFactor (1.0 on non-Retina, +// 2.0 on standard Retina). Used to size the pbuffer (= GL default framebuffer) +// in physical pixels so full-resolution rendering isn't clipped. +static double GetBackingScaleFactor(SDL_Window* window) { + SDL_SysWMinfo wmInfo; + SDL_VERSION(&wmInfo.version); + if (!SDL_GetWindowWMInfo(window, &wmInfo)) + return 1.0; + + id nswindow = (id)wmInfo.info.cocoa.window; + if (!nswindow) + return 1.0; + + double (*getScale)(id, SEL) = (double(*)(id, SEL))objc_msgSend; + double scale = getScale(nswindow, sel_registerName("backingScaleFactor")); + return (scale > 0.0) ? scale : 1.0; +} + +static bool InitEGLContext(SDL_Window* window, int major, int minor) { + fprintf(stderr, "[EGL] eglGetDisplay(EGL_DEFAULT_DISPLAY)...\n"); + g_eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); + fprintf(stderr, "[EGL] eglGetDisplay -> %p (lastError=0x%x)\n", (void*)g_eglDisplay, eglGetError()); + if (g_eglDisplay == EGL_NO_DISPLAY) return false; + + EGLint eglMajor = 0, eglMinor = 0; + EGLBoolean initOk = eglInitialize(g_eglDisplay, &eglMajor, &eglMinor); + fprintf(stderr, "[EGL] eglInitialize -> %d (version %d.%d, lastError=0x%x)\n", + (int)initOk, eglMajor, eglMinor, eglGetError()); + if (!initOk) return false; + + const char* vendor = eglQueryString(g_eglDisplay, EGL_VENDOR); + const char* version = eglQueryString(g_eglDisplay, EGL_VERSION); + const char* clientApis = eglQueryString(g_eglDisplay, EGL_CLIENT_APIS); + fprintf(stderr, "[EGL] vendor=%s version=%s clientApis=%s\n", + vendor ? vendor : "?", version ? version : "?", clientApis ? clientApis : "?"); + + EGLBoolean bindOk = eglBindAPI(EGL_OPENGL_API); + fprintf(stderr, "[EGL] eglBindAPI(EGL_OPENGL_API) -> %d (lastError=0x%x)\n", + (int)bindOk, eglGetError()); + + // On macOS with Mesa-surfaceless EGL, EGL_WINDOW_BIT is unsupported. + // The actual presentation happens via the CAMetalLayer + KosmicKrisp WSI + // (Vulkan -> Metal), so we only need a pbuffer-capable config here. + 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, 8, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_NONE + }; + EGLConfig eglConfig; + EGLint numConfigs = 0; + EGLBoolean cfgOk = eglChooseConfig(g_eglDisplay, configAttribs, &eglConfig, 1, &numConfigs); + fprintf(stderr, "[EGL] eglChooseConfig -> %d (numConfigs=%d, lastError=0x%x)\n", + (int)cfgOk, numConfigs, eglGetError()); + if (!cfgOk || numConfigs == 0) + return false; + + void* nativeView = GetNSViewFromSDLWindow(window); + g_metalLayer = nativeView; // CAMetalLayer for the manual present path + + // Size the pbuffer (= the GL default framebuffer) to the window's *backing* + // pixel size, so the engine's full-resolution (Retina) rendering isn't + // clipped. SDL_GetWindowSizeInPixels returns logical points on this + // surfaceless/borderless setup, so compute backing pixels explicitly via + // the window size in points * backingScaleFactor. The manual Metal present + // reads this whole buffer back each SwapBuffers. + // + // glReadPixels of the full Retina pbuffer is the + // dominant per-frame cost (measured 40-55ms at 2944x1908). The Metal + // present pass linear-samples the IOSurface into the drawable, so we can + // render at logical (1x) resolution and let CoreAnimation upscale to + // Retina with negligible cost — at ~4x less readback data. Opt in with + // SPRING_MAC_NO_RETINA=1. + int winW = 0, winH = 0; + SDL_GetWindowSize(window, &winW, &winH); + const double bsfTrue = GetBackingScaleFactor(window); + const bool noRetina = (getenv("SPRING_MAC_NO_RETINA") != nullptr); + const double bsf = noRetina ? 1.0 : bsfTrue; + int pxW = (int)(winW * bsf + 0.5); + int pxH = (int)(winH * bsf + 0.5); + if (pxW <= 0 || pxH <= 0) { pxW = 1280; pxH = 720; } + g_pbufW = pxW; + g_pbufH = pxH; + fprintf(stderr, "[EGL] window %dx%d pts * %.2f scale -> pbuffer %dx%d px%s\n", + winW, winH, bsf, pxW, pxH, + noRetina ? " (SPRING_MAC_NO_RETINA=1; native scale was " : ""); + if (noRetina) + fprintf(stderr, " (true backing scale=%.2f; CoreAnimation will upscale)\n", bsfTrue); + + if (nativeView) { + g_eglSurface = eglCreateWindowSurface(g_eglDisplay, eglConfig, (EGLNativeWindowType)nativeView, NULL); + } + if (g_eglSurface == EGL_NO_SURFACE) { + EGLint pbAttribs[] = { EGL_WIDTH, g_pbufW, EGL_HEIGHT, g_pbufH, EGL_NONE }; + g_eglSurface = eglCreatePbufferSurface(g_eglDisplay, eglConfig, pbAttribs); + if (g_eglSurface == EGL_NO_SURFACE) return false; + fprintf(stderr, "[EGL] FALLBACK to PbufferSurface %dx%d surface=%p\n", g_pbufW, g_pbufH, (void*)g_eglSurface); + } + + // Dump what the chosen config actually supports. + { + EGLint cfgRenderable = 0, cfgSurface = 0, cfgConformant = 0; + eglGetConfigAttrib(g_eglDisplay, eglConfig, EGL_RENDERABLE_TYPE, &cfgRenderable); + eglGetConfigAttrib(g_eglDisplay, eglConfig, EGL_SURFACE_TYPE, &cfgSurface); + eglGetConfigAttrib(g_eglDisplay, eglConfig, EGL_CONFORMANT, &cfgConformant); + fprintf(stderr, "[EGL] Config: renderable=0x%x surfaceType=0x%x conformant=0x%x (OPENGL_BIT=0x%x)\n", + cfgRenderable, cfgSurface, cfgConformant, EGL_OPENGL_BIT); + } + // Prefer a COMPATIBILITY-profile context. Mesa 26.2 Zink grants GL 4.6 + // compat on KosmicKrisp (verified: "4.6 (Compatibility Profile)"), which is + // a strict superset of core: it keeps all modern GL4 features AND the legacy + // paths BAR pervasively relies on (immediate mode / glBegin, display lists, + // the fixed-function matrix stack, and '#version ... compatibility' GLSL). + // This eliminates an otherwise huge per-shader core-profile port. We fall + // back to a CORE context if compat is refused, or if SPRING_MAC_GL_CORE is + // set to force the old behavior. + // + // Note: geometry shaders are unavailable regardless of profile because + // KosmicKrisp's Vulkan reports geometryShader=false (Metal has no GS stage); + // that is a separate problem from the GL profile. + const int2 tryVersions[] = { + {4,6},{4,5},{4,4},{4,3},{4,2},{4,1},{4,0},{3,3},{3,2} + }; + const bool forceCore = (getenv("SPRING_MAC_GL_CORE") != nullptr); + + if (!forceCore) { + EGLint compatAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, 0, + EGL_CONTEXT_MINOR_VERSION, 0, + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT, + EGL_NONE + }; + for (const auto& v : tryVersions) { + compatAttribs[1] = v.x; + compatAttribs[3] = v.y; + g_eglContext = eglCreateContext(g_eglDisplay, eglConfig, EGL_NO_CONTEXT, compatAttribs); + EGLint err = eglGetError(); + fprintf(stderr, "[EGL] eglCreateContext(%d.%d COMPAT) -> %p (lastError=0x%x)\n", + v.x, v.y, (void*)g_eglContext, err); + if (g_eglContext != EGL_NO_CONTEXT) + break; + } + fprintf(stderr, g_eglContext != EGL_NO_CONTEXT + ? "[EGL] COMPAT profile context obtained.\n" + : "[EGL] COMPAT profile unavailable; falling back to CORE.\n"); + } + + EGLint contextAttribs[] = { + EGL_CONTEXT_MAJOR_VERSION, 0, + EGL_CONTEXT_MINOR_VERSION, 0, + EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE + }; + for (const auto& v : tryVersions) { + if (g_eglContext != EGL_NO_CONTEXT) + break; + contextAttribs[1] = v.x; + contextAttribs[3] = v.y; + g_eglContext = eglCreateContext(g_eglDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs); + EGLint err = eglGetError(); + fprintf(stderr, "[EGL] eglCreateContext(%d.%d core) -> %p (lastError=0x%x)\n", + v.x, v.y, (void*)g_eglContext, err); + if (g_eglContext != EGL_NO_CONTEXT) + break; + } + (void)major; (void)minor; + if (g_eglContext == EGL_NO_CONTEXT) return false; + + EGLBoolean mcOk = eglMakeCurrent(g_eglDisplay, g_eglSurface, g_eglSurface, g_eglContext); + fprintf(stderr, "[EGL] eglMakeCurrent -> %d (lastError=0x%x)\n", (int)mcOk, eglGetError()); + if (!mcOk) + return false; + + // Probe GL via eglGetProcAddress before the engine's glad pass runs. + // Using raw types because GL headers aren't included this high in the file. + typedef const unsigned char* (*PFN_glGetString)(unsigned int); + auto p_glGetString = (PFN_glGetString)eglGetProcAddress("glGetString"); + if (p_glGetString) { + const char* glVer = (const char*)p_glGetString(0x1F02u); // GL_VERSION + const char* glVendor = (const char*)p_glGetString(0x1F00u); // GL_VENDOR + const char* glRend = (const char*)p_glGetString(0x1F01u); // GL_RENDERER + const char* glsl = (const char*)p_glGetString(0x8B8Cu); // GL_SHADING_LANGUAGE_VERSION + fprintf(stderr, "[EGL/GL] vendor='%s'\n", glVendor ? glVendor : "(null)"); + fprintf(stderr, "[EGL/GL] renderer='%s'\n", glRend ? glRend : "(null)"); + fprintf(stderr, "[EGL/GL] version='%s'\n", glVer ? glVer : "(null)"); + fprintf(stderr, "[EGL/GL] glsl='%s'\n", glsl ? glsl : "(null)"); + } else { + fprintf(stderr, "[EGL/GL] eglGetProcAddress(glGetString) returned NULL\n"); + } + + // Set up the manual present path now that the layer + context exist. + if (!MacMetalPresent_Init(g_metalLayer)) + fprintf(stderr, "[EGL] MacMetalPresent_Init failed; window will not show frames\n"); + + return true; +} + +static void DestroyEGLContext() { + if (g_eglDisplay != EGL_NO_DISPLAY) { + eglMakeCurrent(g_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + if (g_eglContext != EGL_NO_CONTEXT) eglDestroyContext(g_eglDisplay, g_eglContext); + if (g_eglSurface != EGL_NO_SURFACE) eglDestroySurface(g_eglDisplay, g_eglSurface); + eglTerminate(g_eglDisplay); + } + g_eglDisplay = EGL_NO_DISPLAY; + g_eglContext = EGL_NO_CONTEXT; + g_eglSurface = EGL_NO_SURFACE; +} +#endif // __APPLE__ && !HEADLESS + #include "GlobalRenderingInfo.h" #include "Rendering/VerticalSync.h" #include "Rendering/GL/StreamBuffer.h" @@ -425,7 +734,11 @@ SDL_Window* CGlobalRendering::CreateSDLWindow(const char* title) const // SDL_WINDOW_FULLSCREEN_DESKTOP for "fake" fullscreen that takes the size of the desktop; // and 0 for windowed mode. +#if defined(__APPLE__) && !defined(HEADLESS) + uint32_t sdlFlags = (SDL_WINDOW_RESIZABLE); +#else uint32_t sdlFlags = (SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); +#endif sdlFlags |= (borderless_ ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN) * fullScreen_; sdlFlags |= (SDL_WINDOW_BORDERLESS * borderless_); @@ -584,11 +897,58 @@ bool CGlobalRendering::CreateWindowAndContext(const char* title) WindowManagerHelper::BlockCompositing(sdlWindow); #endif +#if defined(__APPLE__) && !defined(HEADLESS) + if (!InitEGLContext(sdlWindow, minCtx.x, minCtx.y)) { + handleerror(nullptr, "Failed to create EGL context on macOS", "ERROR", MBF_OK | MBF_EXCL); + return false; + } + glContext = (SDL_GLContext)g_eglContext; +#else if ((glContext = CreateGLContext(minCtx)) == nullptr) return false; +#endif +#if defined(__APPLE__) && !defined(HEADLESS) + gladLoadGLLoader((GLADloadproc)eglGetProcAddress); +#else gladLoadGL(); +#endif +#ifndef __APPLE__ +#ifndef __APPLE__ GLX::Load(sdlWindow); +#endif +#endif + +#if defined(__APPLE__) && !defined(HEADLESS) + // Tracer bullet: when SPRING_MAC_PRESENT_TEST is set, flash the window + // red/blue for ~8s by rendering a clear and presenting it to the window's + // CAMetalLayer via the manual Metal present path (glReadPixels -> MTLTexture + // -> blit to drawable). If the window now flashes, the manual present works + // and can be wired into SwapBuffers for real frames. + if (const char* e = getenv("SPRING_MAC_PRESENT_TEST"); e != nullptr && e[0] == '1') { + const bool metalOk = MacMetalPresent_Init(g_metalLayer); + GLint vp[4] = {0, 0, 1280, 720}; + glGetIntegerv(GL_VIEWPORT, vp); + const int rw = (vp[2] > 0) ? vp[2] : 1280; + const int rh = (vp[3] > 0) ? vp[3] : 720; + std::vector buf(static_cast(rw) * rh * 4); + fprintf(stderr, "[PRESENT_TEST] metalInit=%d flashing ~8s via Metal (%dx%d)\n", (int)metalOk, rw, rh); + for (int i = 0; i < 16; i++) { + const bool odd = (i % 2) != 0; + glClearColor(odd ? 1.0f : 0.0f, 0.15f, odd ? 0.0f : 1.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glReadPixels(0, 0, rw, rh, GL_BGRA, GL_UNSIGNED_BYTE, buf.data()); + MacMetalPresent_PresentBGRA(rw, rh, buf.data(), false); // solid color: no flip needed + fprintf(stderr, "[PRESENT_TEST] frame %d color=%s\n", i, odd ? "RED" : "BLUE"); + // Pump the Cocoa run loop so CoreAnimation actually composites each + // presented frame (otherwise only the final frame shows on screen). + SDL_PumpEvents(); + SDL_Event ev; while (SDL_PollEvent(&ev)) {} + SDL_Delay(500); + } + fprintf(stderr, "[PRESENT_TEST] done\n"); + } +#endif if (!CheckGLContextVersion(minCtx)) { int ctxProfile = 0; @@ -610,7 +970,14 @@ bool CGlobalRendering::CreateWindowAndContext(const char* title) void CGlobalRendering::MakeCurrentContext(bool clear) const { +#if defined(__APPLE__) && !defined(HEADLESS) + if (clear) + eglMakeCurrent(g_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + else + eglMakeCurrent(g_eglDisplay, g_eglSurface, g_eglSurface, g_eglContext); +#else SDL_GL_MakeCurrent(sdlWindow, clear ? nullptr : glContext); +#endif } @@ -632,7 +999,11 @@ void CGlobalRendering::DestroyWindowAndContext() { sdlWindow = nullptr; glContext = nullptr; +#ifndef __APPLE__ +#ifndef __APPLE__ GLX::Unload(); +#endif +#endif } void CGlobalRendering::KillSDL() const { @@ -668,6 +1039,71 @@ void CGlobalRendering::PostInit() { UpdateTimer(); } +#if defined(__APPLE__) && !defined(HEADLESS) +// Lazily allocate (or resize) the downsample-readback FBO. +// Returns false on allocation/completeness failure; caller should fall back +// to a full-resolution readback. +static bool EnsureDownscaleReadbackFBO(int w, int h) +{ + if (g_dsFBO != 0 && g_dsW == w && g_dsH == h) + return true; + if (g_dsFBO == 0) { + glGenFramebuffers(1, &g_dsFBO); + glGenTextures(1, &g_dsTex); + } + glBindTexture(GL_TEXTURE_2D, g_dsTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + GLint prevDrawFBO = 0; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &prevDrawFBO); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, g_dsFBO); + glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_dsTex, 0); + const GLenum st = glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, (GLuint)prevDrawFBO); + glBindTexture(GL_TEXTURE_2D, 0); + + if (st != GL_FRAMEBUFFER_COMPLETE) { + fprintf(stderr, "[spring-mac/downsample] FBO incomplete: 0x%04x\n", (unsigned)st); + return false; + } + g_dsW = w; + g_dsH = h; + return true; +} + +// Allocate or resize the two readback PBOs. Tightly packed +// BGRA (no row padding); the IOSurface row stride is handled at memcpy time. +// Returns false on allocation failure; caller falls back to sync readback. +static bool EnsureReadbackPBOs(int w, int h) +{ + if (g_pbos[0] != 0 && g_pboW == w && g_pboH == h) + return true; + if (g_pbos[0] == 0) + glGenBuffers(2, g_pbos); + const GLsizeiptr nBytes = (GLsizeiptr)w * (GLsizeiptr)h * 4; + for (int i = 0; i < 2; ++i) { + glBindBuffer(GL_PIXEL_PACK_BUFFER, g_pbos[i]); + glBufferData(GL_PIXEL_PACK_BUFFER, nBytes, nullptr, GL_STREAM_READ); + } + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + g_pboW = w; + g_pboH = h; + g_pboHasPrev = false; + g_pboCurIdx = 0; + + static bool s_logged = false; + if (!s_logged) { + fprintf(stderr, "[spring-mac/pbo] async readback active (%dx%d, 1-frame latency)\n", w, h); + s_logged = true; + } + return true; +} +#endif + void CGlobalRendering::SwapBuffers(bool allowSwapBuffers, bool clearErrors) { spring_time pre; @@ -680,6 +1116,35 @@ void CGlobalRendering::SwapBuffers(bool allowSwapBuffers, bool clearErrors) if (clearErrors || glDebugErrors) glClearErrors("GR", __func__, glDebugErrors); +#if defined(__APPLE__) && !defined(HEADLESS) + // Headless verification capture. Draw() has already rendered into + // the default FBO by the time SwapBuffers() runs, so we can read it + // back here even when the actual present/swap is suppressed + // (allowSwapBuffers==false, e.g. an unfocused/background launch). + // Writes .NNNN.raw (uint32 w,h header + w*h*4 BGRA, bottom-up). + if (const char* cp = getenv("SPRING_FRAME_CAPTURE")) { + if (g_eglDisplay != EGL_NO_DISPLAY && g_eglSurface != EGL_NO_SURFACE && g_pbufW > 0 && g_pbufH > 0) { + static int s_cap = 0; + if ((s_cap % 30) == 0 && s_cap < 1800) { + const size_t need = static_cast(g_pbufW) * g_pbufH * 4; + if (g_presentBuf.size() < need) + g_presentBuf.resize(need); + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glReadPixels(0, 0, g_pbufW, g_pbufH, GL_BGRA, GL_UNSIGNED_BYTE, g_presentBuf.data()); + char path[1024]; + snprintf(path, sizeof(path), "%s.%04d.raw", cp, s_cap); + if (FILE* f = fopen(path, "wb")) { + const uint32_t hdr[2] = { (uint32_t)g_pbufW, (uint32_t)g_pbufH }; + fwrite(hdr, sizeof(hdr), 1, f); + fwrite(g_presentBuf.data(), 1, need, f); + fclose(f); + } + } + s_cap++; + } + } +#endif + if (!allowSwapBuffers && !forceSwapBuffers) return; @@ -700,6 +1165,204 @@ void CGlobalRendering::SwapBuffers(bool allowSwapBuffers, bool clearErrors) } #endif +#if defined(__APPLE__) && !defined(HEADLESS) + if (g_eglDisplay != EGL_NO_DISPLAY && g_eglSurface != EGL_NO_SURFACE) { + // eglSwapBuffers on the (surfaceless) pbuffer presents nowhere, so + // read the rendered default framebuffer back and blit it onto the + // window's CAMetalLayer via Metal. + // + // Fast path: glReadPixels writes directly into an IOSurface-backed + // MTLTexture (no CPU intermediate buffer, no CPU Y-flip, no + // replaceRegion upload — Metal samples the same backing store and + // flips during a one-triangle render pass to the drawable). + // + // Fallback path: SPRING_MAC_LEGACY_PRESENT=1 or AcquireIOSurfaceBuffer + // failure routes through the original CPU staging path. + const bool wantLegacy = (getenv("SPRING_MAC_LEGACY_PRESENT") != nullptr); + // Per-frame timing instrumentation. Set SPRING_TIME_PRESENT=1 + // to log a breakdown of (lock-wait | glReadPixels | Metal present | other) + // averaged over every 60 frames. Use to attribute the per-frame cost. + static const bool s_timePresent = (getenv("SPRING_TIME_PRESENT") != nullptr); + + // Optional downsample-before-readback. The engine + // renders at full pbuffer res (UI layout uses that viewport, so it + // looks right), but we blit-downsample to (rdW x rdH) before + // glReadPixels — readback data drops by dsFactor^2. + static const int s_dsFactor = []() -> int { + if (const char* s = getenv("SPRING_DOWNSAMPLE_READBACK")) { + const int v = atoi(s); + if (v >= 2 && v <= 8) return v; + } + return 1; + }(); + const int rdW = std::max(1, g_pbufW / s_dsFactor); + const int rdH = std::max(1, g_pbufH / s_dsFactor); + const bool doDownsample = (s_dsFactor > 1 && EnsureDownscaleReadbackFBO(rdW, rdH)); + + const spring_time tEntry = s_timePresent ? spring_now() : spring_notime; + size_t ioRowBytes = 0; + void* ioBase = wantLegacy ? nullptr + : MacMetalPresent_AcquireIOSurfaceBuffer(rdW, rdH, &ioRowBytes); + const spring_time tAfterAcquire = s_timePresent ? spring_now() : spring_notime; + + if (ioBase != nullptr) { + if (doDownsample) { + // Blit default FBO (full Retina render target) → smaller FBO with linear filter. + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, g_dsFBO); + glBlitFramebuffer(0, 0, g_pbufW, g_pbufH, + 0, 0, rdW, rdH, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + glBindFramebuffer(GL_READ_FRAMEBUFFER, g_dsFBO); + } + // Async PBO readback is the default — measured + // 3x FPS win in busy scenes vs the sync path at the same + // resolution (NO_RETINA=1: sync drops 60->22 fps as scene grows; + // PBO holds steady at 65-75 fps). The 1-frame present latency + // is imperceptible. Opt out with SPRING_NO_PBO=1 to restore + // the direct-into-IOSurface sync glReadPixels — useful for + // debugging perf regressions or when running at 4x pixel counts + // where the per-frame GPU budget is tight enough that + // glMapBufferRange can still block. + static const bool s_noPBO = (getenv("SPRING_NO_PBO") != nullptr); + const size_t srcRowBytes = (size_t)rdW * 4; + + if (s_noPBO) { + // Sync path: glReadPixels stalls until the GPU pipeline drains. + const int rowPixels = static_cast(ioRowBytes / 4); + if (rowPixels != rdW) + glPixelStorei(GL_PACK_ROW_LENGTH, rowPixels); + glReadPixels(0, 0, rdW, rdH, GL_BGRA, GL_UNSIGNED_BYTE, ioBase); + if (rowPixels != rdW) + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + } else { + // Async path: glReadPixels into a PBO is non-blocking; we + // then map the *previous* frame's PBO (one frame of present + // latency) and memcpy into the IOSurface. The pipeline stall + // is hidden because the GPU has had a full frame to finish + // writing PBO[prev] before we ask to map it. + EnsureReadbackPBOs(rdW, rdH); + glBindBuffer(GL_PIXEL_PACK_BUFFER, g_pbos[g_pboCurIdx]); + // Tightly packed into the PBO; pack alignment 4 (BGRA) is fine. + glReadPixels(0, 0, rdW, rdH, GL_BGRA, GL_UNSIGNED_BYTE, nullptr); + + if (g_pboHasPrev) { + const int prevIdx = 1 - g_pboCurIdx; + glBindBuffer(GL_PIXEL_PACK_BUFFER, g_pbos[prevIdx]); + const GLsizeiptr nBytes = (GLsizeiptr)rdW * (GLsizeiptr)rdH * 4; + void* mapped = glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, nBytes, GL_MAP_READ_BIT); + if (mapped != nullptr) { + if (ioRowBytes == srcRowBytes) { + std::memcpy(ioBase, mapped, srcRowBytes * (size_t)rdH); + } else { + const uint8_t* src = static_cast(mapped); + uint8_t* dst = static_cast(ioBase); + for (int y = 0; y < rdH; ++y) { + std::memcpy(dst + (size_t)y * ioRowBytes, + src + (size_t)y * srcRowBytes, + srcRowBytes); + } + } + glUnmapBuffer(GL_PIXEL_PACK_BUFFER); + } + } + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + g_pboHasPrev = true; + g_pboCurIdx = 1 - g_pboCurIdx; + } + + if (doDownsample) { + // Restore default read FBO so downstream code finds the state it expects. + glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + } + + // Debug: dump rendered frames to raw files for inspection. + // SPRING_MAC_DUMP_FRAME=; writes .NNN.raw + // (8-byte header: uint32 w,h; then row-major BGRA, bottom-up). + if (const char* dp = getenv("SPRING_MAC_DUMP_FRAME")) { + static int s_df = 0; + if (s_df < 80 && (s_df % 6) == 0) { + char path[1024]; + snprintf(path, sizeof(path), "%s.%03d.raw", dp, s_df); + if (FILE* f = fopen(path, "wb")) { + const uint32_t hdr[2] = { (uint32_t)rdW, (uint32_t)rdH }; + fwrite(hdr, sizeof(hdr), 1, f); + const uint8_t* row = static_cast(ioBase); + for (int y = 0; y < rdH; ++y) { + fwrite(row + (size_t)y * ioRowBytes, 1, (size_t)rdW * 4, f); + } + fclose(f); + } + } + s_df++; + } + + const spring_time tAfterRead = s_timePresent ? spring_now() : spring_notime; + MacMetalPresent_PresentIOSurface(true); + if (s_timePresent) { + const spring_time tAfterPresent = spring_now(); + static int s_frameCnt = 0; + static double s_sumAcquireMs = 0.0; + static double s_sumReadMs = 0.0; + static double s_sumPresentMs = 0.0; + static double s_sumTotalMs = 0.0; + s_sumAcquireMs += (tAfterAcquire - tEntry).toMilliSecsf(); + s_sumReadMs += (tAfterRead - tAfterAcquire).toMilliSecsf(); + s_sumPresentMs += (tAfterPresent - tAfterRead).toMilliSecsf(); + s_sumTotalMs += (tAfterPresent - tEntry).toMilliSecsf(); + if (++s_frameCnt >= 60) { + const double n = (double)s_frameCnt; + fprintf(stderr, + "[spring-mac/present] avg over %d frames: acquire(=lock-wait) %.2fms | " + "glReadPixels %.2fms | metal-submit %.2fms | total %.2fms (=> %.1f fps if loop-bound)\n", + s_frameCnt, + s_sumAcquireMs / n, + s_sumReadMs / n, + s_sumPresentMs / n, + s_sumTotalMs / n, + 1000.0 / std::max(0.001, s_sumTotalMs / n)); + s_frameCnt = 0; + s_sumAcquireMs = 0.0; + s_sumReadMs = 0.0; + s_sumPresentMs = 0.0; + s_sumTotalMs = 0.0; + } + } + } else { + static bool s_loggedLegacy = false; + if (!s_loggedLegacy) { + fprintf(stderr, "[MetalPresent] LEGACY CPU-staging path active (%s)\n", + wantLegacy ? "forced via SPRING_MAC_LEGACY_PRESENT" : "IOSurface acquire failed"); + s_loggedLegacy = true; + } + // Legacy CPU-staging fallback. + const size_t need = static_cast(g_pbufW) * g_pbufH * 4; + if (g_presentBuf.size() < need) + g_presentBuf.resize(need); + glReadPixels(0, 0, g_pbufW, g_pbufH, GL_BGRA, GL_UNSIGNED_BYTE, g_presentBuf.data()); + if (const char* dp = getenv("SPRING_MAC_DUMP_FRAME")) { + static int s_df = 0; + if (s_df < 80 && (s_df % 6) == 0) { + char path[1024]; + snprintf(path, sizeof(path), "%s.%03d.raw", dp, s_df); + if (FILE* f = fopen(path, "wb")) { + const uint32_t hdr[2] = { (uint32_t)g_pbufW, (uint32_t)g_pbufH }; + fwrite(hdr, sizeof(hdr), 1, f); + fwrite(g_presentBuf.data(), 1, need, f); + fclose(f); + } + } + s_df++; + } + MacMetalPresent_PresentBGRA(g_pbufW, g_pbufH, g_presentBuf.data(), true); + } + // We replaced SDL_GL_SwapWindow (which serviced the Cocoa run loop), + // so pump events here to let CoreAnimation actually composite the + // presented drawable — including during the single-threaded load. + SDL_PumpEvents(); + } else +#endif SDL_GL_SwapWindow(sdlWindow); #ifdef _WIN32 @@ -782,6 +1445,12 @@ void CGlobalRendering::CheckGLExtensions() if (underExternalDebug) return; + // In an OpenGL CORE profile context these ARB extensions are not advertised + // by name (they were folded into GL 1.3/2.0/3.0 long ago) but their + // functionality is guaranteed by the spec. Skip the legacy-extension check. + if (globalRenderingInfo.glContextIsCore) + return; + char extMsg[ 128] = {0}; char errMsg[2048] = {0}; char* ptr = &extMsg[0]; @@ -1569,6 +2238,33 @@ void CGlobalRendering::ReadWindowPosAndSize() UpdateWindowBorders(sdlWindow); SDL_GetWindowSize(sdlWindow, &winSizeX, &winSizeY); +#if defined(__APPLE__) && !defined(HEADLESS) + { + // Engine contract: winSize/viewSize must match the pbuffer FBO the + // engine actually renders into — *not* the CAMetalLayer drawableSize. + // They are equal by accident at full Retina (drawable == backing == + // pbuffer size), but diverge under SPRING_MAC_NO_RETINA=1 (FBO at + // logical 1x; drawable kept at full Retina backing so CoreAnimation + // upscales for free). Reading the drawable here used to silently + // align them; with the upscale optimization it breaks viewport = + // 4x FBO area, so geometry ends up in the bottom-left quadrant of + // the FBO and the rest of the UI is clipped. Bind to the FBO size + // instead. + const int sdlW_saved = winSizeX, sdlH_saved = winSizeY; + if (g_pbufW > 1 && g_pbufH > 1) { + winSizeX = g_pbufW; + winSizeY = g_pbufH; + } else { + // Pre-EGL-init fallback: mirror InitEGLContext's pbuffer sizing + // so loading UI starts at the right resolution. + const bool noRetina = (getenv("SPRING_MAC_NO_RETINA") != nullptr); + const double scale = noRetina ? 1.0 : GetBackingScaleFactor(sdlWindow); + winSizeX = std::max(1, int(std::lround(double(sdlW_saved) * scale))); + winSizeY = std::max(1, int(std::lround(double(sdlH_saved) * scale))); + } + SDL_GetWindowPosition(sdlWindow, &winPosX, &winPosY); + } +#endif SDL_GetWindowPosition(sdlWindow, &winPosX, &winPosY); //enforce >=0 https://github.com/beyond-all-reason/spring/issues/23 @@ -1599,8 +2295,19 @@ void CGlobalRendering::SaveWindowPosAndSize() // do not notify about changes to block update loop configHandler->Set("WindowPosX", winPosX, false, false); configHandler->Set("WindowPosY", winPosY, false, false); - configHandler->Set("XResolutionWindowed", winSizeX, false, false); - configHandler->Set("YResolutionWindowed", winSizeY, false, false); + + // Persist the logical (point) window size, not backing pixels. + // GetCfgWinRes feeds the saved value straight to SDL_CreateWindow, which + // expects logical points. On HiDPI displays (macOS Retina, Wayland/X11 + // HiDPI) winSizeX/Y can hold the backing-pixel size; persisting that + // round-tripped to a 2x-too-big request each launch, then clamped to the + // screen as a portrait sliver. Querying SDL directly here pins the saved + // value to logical points regardless of how winSize is wired upstream. + int saveW = winSizeX, saveH = winSizeY; + if (sdlWindow != nullptr) + SDL_GetWindowSize(sdlWindow, &saveW, &saveH); + configHandler->Set("XResolutionWindowed", saveW, false, false); + configHandler->Set("YResolutionWindowed", saveH, false, false); } diff --git a/rts/Rendering/Models/GLTFParser.cpp b/rts/Rendering/Models/GLTFParser.cpp index f72b17dabe5..3f06d7f252e 100644 --- a/rts/Rendering/Models/GLTFParser.cpp +++ b/rts/Rendering/Models/GLTFParser.cpp @@ -22,6 +22,10 @@ #include #include #include +// smmalloc.h leaks `#define INLINE inline` which conflicts with simdjson's layout_mode::INLINE enum +#ifdef INLINE +#undef INLINE +#endif #include diff --git a/rts/Rendering/Textures/TextureAtlas.h b/rts/Rendering/Textures/TextureAtlas.h index 05397d797c8..57d14892f0d 100644 --- a/rts/Rendering/Textures/TextureAtlas.h +++ b/rts/Rendering/Textures/TextureAtlas.h @@ -119,7 +119,9 @@ class CTextureAtlas int2 GetSize() const; std::string GetName() const { return name; } - uint32_t GetTexID() const { return atlasTex->GetId(); } + // atlasTex stays null if Finalize()/Allocate() failed; guard so a failed + // atlas degrades (no texture) instead of crashing on a null deref. + uint32_t GetTexID() const { return atlasTex ? atlasTex->GetId() : 0; } uint32_t GetTexTarget() const; uint32_t GetNumPages() const; @@ -128,7 +130,7 @@ class CTextureAtlas void BindTexture(); void UnbindTexture(); - void DisOwnTexture() { atlasTex->DisOwn(); } + void DisOwnTexture() { if (atlasTex) atlasTex->DisOwn(); } void SetName(const std::string& s) { name = s; } static void SetDebug(bool b) { debug = b; } diff --git a/rts/Rml/SolLua/bind/Context.cpp b/rts/Rml/SolLua/bind/Context.cpp index db0dcb8960b..bd40fd4e535 100644 --- a/rts/Rml/SolLua/bind/Context.cpp +++ b/rts/Rml/SolLua/bind/Context.cpp @@ -131,7 +131,7 @@ struct lua_iterator_state sol::state_view l{s}; int index = 0; int count = keytable.size(); - while (keytable.get(++index).get_type() != sol::type::nil && index <= count) { + while (keytable.get(++index).get_type() != sol::type::lua_nil && index <= count) { this->keys.emplace_back(sol::object(l, sol::in_place, index)); } } else { @@ -199,7 +199,7 @@ createNewIndexFunction(std::shared_ptr data, const } if (value.is()) { auto value_raw = value.as().raw_get("__raw"); - if (value_raw != sol::nil && value_raw.is()) { + if (value_raw != sol::lua_nil && value_raw.is()) { // new value is a datamodel proxy, so get the underlying table to assign prop.as().raw_set(solkey, value_raw.as().call(value)); } else { @@ -290,7 +290,7 @@ sol::table openDataModel(Rml::Context& self, const Rml::String& name, sol::objec } if (value.is()) { auto value_raw = value.as().raw_get("__raw"); - if (value_raw != sol::nil && value_raw.is()) { + if (value_raw != sol::lua_nil && value_raw.is()) { // new value is a datamodel proxy, so get the underlying table to assign data->Table.raw_set(key, value_raw.as().call(value)); } else { diff --git a/rts/Rml/SolLua/bind/Element.cpp b/rts/Rml/SolLua/bind/Element.cpp index e2edf19159a..3bf3ecc7fea 100644 --- a/rts/Rml/SolLua/bind/Element.cpp +++ b/rts/Rml/SolLua/bind/Element.cpp @@ -158,7 +158,7 @@ namespace Rml::SolLua void Set(const sol::this_state L, const std::string& name, const sol::object& value) { - if (value.get_type() == sol::type::nil) { + if (value.get_type() == sol::type::lua_nil) { m_element->RemoveProperty(name); return; } diff --git a/rts/Rml/SolLua/bind/bind.cpp b/rts/Rml/SolLua/bind/bind.cpp index 21eabe9d46c..98ce9fec37c 100644 --- a/rts/Rml/SolLua/bind/bind.cpp +++ b/rts/Rml/SolLua/bind/bind.cpp @@ -39,7 +39,7 @@ namespace Rml::SolLua sol::object makeObjectFromVariant(const Rml::Variant* variant, sol::state_view s) { - if (!variant) return sol::make_object(s, sol::nil); + if (!variant) return sol::make_object(s, sol::lua_nil); switch (variant->GetType()) { @@ -69,10 +69,10 @@ namespace Rml::SolLua case Rml::Variant::VOIDPTR: return sol::make_object(s, variant->Get()); default: - return sol::make_object(s, sol::nil); + return sol::make_object(s, sol::lua_nil); } - return sol::make_object(s, sol::nil); + return sol::make_object(s, sol::lua_nil); } } // end namespace Rml::SolLua diff --git a/rts/Sim/Units/Unit.cpp b/rts/Sim/Units/Unit.cpp index 9d2f32b304d..76771cf6a23 100644 --- a/rts/Sim/Units/Unit.cpp +++ b/rts/Sim/Units/Unit.cpp @@ -965,7 +965,14 @@ static auto SplitResourcePackIntoPositiveNegative (const SResourcePack &pack) { SResourcePack positive {0.0f}, negative {0.0f}; +#if defined(__APPLE__) + // libc++ on older Apple Clang lacks std::views::enumerate (C++23, P2164). + // Fall back to an index-based loop on macOS. + for (int resourceID = 0; resourceID < SResourcePack::MAX_RESOURCES; ++resourceID) { + const auto value = pack.res[resourceID]; +#else for (auto [resourceID, value] : std::views::enumerate (pack)) { +#endif if (value < 0.0f) negative[resourceID] = -value; else diff --git a/rts/System/CMakeLists.txt b/rts/System/CMakeLists.txt index c8fae324574..168f2cc4c5f 100644 --- a/rts/System/CMakeLists.txt +++ b/rts/System/CMakeLists.txt @@ -144,7 +144,7 @@ set(sources_engine_System_Platform_Mac "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/MessageBox.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/CrashHandler.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/WindowManagerHelper.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Linux/ThreadSupport.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/MetalPresent.mm" ) set(sources_engine_System_Platform_Windows "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Win/CrashHandler.cpp" @@ -157,6 +157,8 @@ set(sources_engine_System_Platform_Windows set(sources_engine_System_Threading_Mac "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/Signal.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/CpuTopology.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Mac/ThreadSupport.cpp" ) set(sources_engine_System_Threading_Linux "${CMAKE_CURRENT_SOURCE_DIR}/Platform/Linux/CpuTopology.cpp" diff --git a/rts/System/Cpp23Compat.hpp b/rts/System/Cpp23Compat.hpp new file mode 100644 index 00000000000..7292550b8cd --- /dev/null +++ b/rts/System/Cpp23Compat.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +// Polyfill for C++23 features not yet available in all toolchains (e.g. Apple libc++) + +namespace Recoil { + +#if defined(__cpp_lib_ranges_enumerate) && __cpp_lib_ranges_enumerate >= 202302L + using std::views::enumerate; +#else + // Minimal std::views::enumerate polyfill + template + class enumerate_view { + Range range_; + public: + explicit enumerate_view(Range&& r) : range_(std::forward(r)) {} + explicit enumerate_view(const Range& r) : range_(r) {} + + struct iterator { + using inner_iter = decltype(std::begin(std::declval())); + using value_type = std::tuple())>; + + std::ptrdiff_t index; + inner_iter it; + + auto operator*() const { return std::tie(index, *it); } + iterator& operator++() { ++index; ++it; return *this; } + bool operator!=(const iterator& other) const { return it != other.it; } + bool operator==(const iterator& other) const { return it == other.it; } + }; + + auto begin() { return iterator{0, std::begin(range_)}; } + auto end() { return iterator{0, std::end(range_)}; } + auto begin() const { return iterator{0, std::begin(range_)}; } + auto end() const { return iterator{0, std::end(range_)}; } + }; + + struct enumerate_fn { + template + auto operator()(Range&& r) const { + return enumerate_view(std::forward(r)); + } + }; + + inline constexpr enumerate_fn enumerate{}; +#endif + +} // namespace Recoil diff --git a/rts/System/FastMath.h b/rts/System/FastMath.h index 312f1df9744..c89a5d879d1 100644 --- a/rts/System/FastMath.h +++ b/rts/System/FastMath.h @@ -8,6 +8,13 @@ // Tell streflop_cond.h not to define math::sqrt(float) - we'll provide a faster one #define MATH_SQRT_OVERRIDE 1 +// On macOS, streflop_cond.h provides a std::hypot fallback that uses math::sqrtf, +// which would cause a circular dependency since math::sqrtf is defined later in this file. +// Provide a temporary declaration so the template can resolve; real definition follows below. +#ifdef __APPLE__ +#include +namespace math { inline float sqrtf(float x) { return std::sqrt(x); } } +#endif #include "lib/streflop/streflop_cond.h" #include "System/MainDefines.h" #include "System/MathConstants.h" diff --git a/rts/System/Input/MouseInput.cpp b/rts/System/Input/MouseInput.cpp index c7fbed6708c..0c1ddee1ab7 100644 --- a/rts/System/Input/MouseInput.cpp +++ b/rts/System/Input/MouseInput.cpp @@ -30,6 +30,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif IMouseInput* mouseInput = nullptr; @@ -62,25 +65,72 @@ IMouseInput::~IMouseInput() } +#ifdef __APPLE__ +// SDL emits mouse events in logical (point) coordinates. On the macOS +// surfaceless+pbuffer path the engine viewport is in backing-pixel +// coordinates (viewSize/winSize are tied to the pbuffer FBO; see +// GlobalRendering::ReadWindowPosAndSize). Without rescaling, windowed- +// mode clicks land at half the cursor position on Retina displays. +static int2 ScaleMouseCoords(int x, int y) +{ + int sdlW = 1, sdlH = 1; + if (globalRendering->sdlWindow != nullptr) + SDL_GetWindowSize(globalRendering->sdlWindow, &sdlW, &sdlH); + if (sdlW < 1) sdlW = 1; + if (sdlH < 1) sdlH = 1; + const int scaledX = (int)((float)x * (float)globalRendering->viewSizeX / (float)sdlW); + const int scaledY = (int)((float)y * (float)globalRendering->viewSizeY / (float)sdlH); + return int2(scaledX, scaledY); +} + +static float ScaleMouseDelta(float d, int sdlSize, int viewSize) +{ + if (sdlSize < 1) sdlSize = 1; + return d * (float)viewSize / (float)sdlSize; +} +#endif + + bool IMouseInput::HandleSDLMouseEvent(const SDL_Event& event) { switch (event.type) { case SDL_MOUSEMOTION: { +#ifdef __APPLE__ + mousepos = ScaleMouseCoords(event.motion.x, event.motion.y); + + if (mouse != nullptr) { + int sdlW = 1, sdlH = 1; + if (globalRendering->sdlWindow != nullptr) + SDL_GetWindowSize(globalRendering->sdlWindow, &sdlW, &sdlH); + mouse->MouseMove(mousepos.x, mousepos.y, + ScaleMouseDelta(event.motion.xrel, sdlW, globalRendering->viewSizeX), + ScaleMouseDelta(event.motion.yrel, sdlH, globalRendering->viewSizeY)); + } +#else mousepos = int2(event.motion.x, event.motion.y); if (mouse != nullptr) mouse->MouseMove(mousepos.x, mousepos.y, event.motion.xrel, event.motion.yrel); +#endif } break; case SDL_MOUSEBUTTONDOWN: { +#ifdef __APPLE__ + mousepos = ScaleMouseCoords(event.button.x, event.button.y); +#else mousepos = int2(event.button.x, event.button.y); +#endif if (mouse != nullptr) mouse->MousePress(mousepos.x, mousepos.y, event.button.button); } break; case SDL_MOUSEBUTTONUP: { +#ifdef __APPLE__ + mousepos = ScaleMouseCoords(event.button.x, event.button.y); +#else mousepos = int2(event.button.x, event.button.y); +#endif if (mouse != nullptr) mouse->MouseRelease(mousepos.x, mousepos.y, event.button.button); diff --git a/rts/System/MemPoolTypes.h b/rts/System/MemPoolTypes.h index dcd5730e040..08991339658 100644 --- a/rts/System/MemPoolTypes.h +++ b/rts/System/MemPoolTypes.h @@ -21,6 +21,21 @@ #include "System/Platform/Threading.h" #include "System/Log/ILog.h" +namespace { + // Helper to get a numeric thread ID for logging (handles pthread_t being a pointer on macOS) + inline uint64_t GetThreadIdForLog() { +#ifdef __APPLE__ + // On macOS, pthread_t is a pointer - cast to integer for logging + return reinterpret_cast(Threading::GetCurrentThreadId()); +#elif defined(_WIN32) + return static_cast(Threading::GetCurrentThreadId()); +#else + // Linux pthread_t is typically unsigned long + return static_cast(Threading::GetCurrentThreadId()); +#endif + } +} + template struct PassThroughPool { public: PassThroughPool() { @@ -431,7 +446,7 @@ inline size_t StablePosAllocator::Allocate(size_t numElems) if (positionToSize.empty()) { size_t returnPos = data.size(); data.resize(data.size() + numElems); - myLog("StablePosAllocator::Allocate(%u) = %u [thread_id = %u]", uint32_t(numElems), uint32_t(returnPos), static_cast(Threading::GetCurrentThreadId())); + myLog("StablePosAllocator::Allocate(%u) = %u [thread_id = 0x%llx]", uint32_t(numElems), uint32_t(returnPos), static_cast(GetThreadIdForLog())); return returnPos; } diff --git a/rts/System/Platform/Mac/CpuTopology.cpp b/rts/System/Platform/Mac/CpuTopology.cpp new file mode 100644 index 00000000000..8b027fee25d --- /dev/null +++ b/rts/System/Platform/Mac/CpuTopology.cpp @@ -0,0 +1,91 @@ +#include "System/Platform/CpuTopology.h" +#include +#include + +namespace cpu_topology { + +namespace { + +// Read an unsigned sysctl value by name. Returns 0 if the key is unavailable +// (e.g. perflevel keys on Intel Macs or pre-Apple-Silicon kernels). +unsigned int ReadSysctlUInt(const char* name) { + int value = 0; + size_t valueSize = sizeof(value); + if (sysctlbyname(name, &value, &valueSize, nullptr, 0) != 0) + return 0; + return (value > 0) ? static_cast(value) : 0; +} + +unsigned int BitsForCount(unsigned int n) { + if (n == 0) return 0; + if (n >= 32) return 0xFFFFFFFFu; + return (1u << n) - 1u; +} + +} // namespace + +ThreadPinPolicy GetThreadPinPolicy() { + // macOS has no pthread_setaffinity_np equivalent. Scheduling locality is + // instead expressed via QOS classes; see Platform/Mac/ThreadSupport.cpp. + return THREAD_PIN_POLICY_NONE; +} + +ProcessorMasks GetProcessorMasks() { + ProcessorMasks masks{}; + + // Apple Silicon exposes per-perflevel core counts. perflevel0 is the + // high-performance (P) cluster; perflevel1, when present, is the + // efficiency (E) cluster. Intel Macs and older kernels do not expose + // these keys; fall back to treating every core as a P-core there. + const unsigned int numPCores = ReadSysctlUInt("hw.perflevel0.physicalcpu"); + const unsigned int numECores = ReadSysctlUInt("hw.perflevel1.physicalcpu"); + + if (numPCores > 0) { + masks.performanceCoreMask = BitsForCount(numPCores); + // E-cores occupy the bits above the P-cores in the combined mask. + const unsigned int totalCores = numPCores + numECores; + const unsigned int allMask = BitsForCount(totalCores); + masks.efficiencyCoreMask = allMask & ~masks.performanceCoreMask; + } else { + // Intel Mac / unknown topology: treat the visible core count as + // homogeneous P-cores. Matches prior behavior on those targets. + unsigned int numCores = std::thread::hardware_concurrency(); + if (numCores == 0) numCores = 4; + masks.performanceCoreMask = BitsForCount(numCores); + masks.efficiencyCoreMask = 0; + } + + // macOS does not expose SMT/HT details; report all visible cores as + // hyper-thread-low so callers consuming those masks stay consistent. + masks.hyperThreadLowMask = masks.performanceCoreMask | masks.efficiencyCoreMask; + masks.hyperThreadHighMask = 0; + + return masks; +} + +ProcessorCaches GetProcessorCache() { + ProcessorCaches caches; + + ProcessorGroupCaches group; + unsigned int numCores = std::thread::hardware_concurrency(); + if (numCores == 0) numCores = 4; + group.groupMask = BitsForCount(numCores); + + // Try to get cache sizes via sysctl + size_t size = sizeof(uint64_t); + uint64_t cacheSize = 0; + + if (sysctlbyname("hw.l1dcachesize", &cacheSize, &size, nullptr, 0) == 0) + group.cacheSizes[0] = static_cast(cacheSize); + + if (sysctlbyname("hw.l2cachesize", &cacheSize, &size, nullptr, 0) == 0) + group.cacheSizes[1] = static_cast(cacheSize); + + if (sysctlbyname("hw.l3cachesize", &cacheSize, &size, nullptr, 0) == 0) + group.cacheSizes[2] = static_cast(cacheSize); + + caches.groupCaches.push_back(group); + return caches; +} + +} // namespace cpu_topology diff --git a/rts/System/Platform/Mac/MetalPresent.h b/rts/System/Platform/Mac/MetalPresent.h new file mode 100644 index 00000000000..80c0763d8c6 --- /dev/null +++ b/rts/System/Platform/Mac/MetalPresent.h @@ -0,0 +1,44 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#ifndef MAC_METAL_PRESENT_H +#define MAC_METAL_PRESENT_H + +// macOS manual Metal present path. +// +// Mesa's EGL on macOS is built with the "surfaceless" platform, so +// eglCreateWindowSurface() fails and the engine renders into an off-screen +// pbuffer that eglSwapBuffers() can never present. These helpers take the +// pixels the engine rendered (read back via glReadPixels) and blit them onto +// the window's CAMetalLayer drawable via Metal, so something actually appears. + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// caMetalLayer: the CAMetalLayer* already attached to the SDL window's NSView +// (the same pointer GetNSViewFromSDLWindow returns). Returns true on success. +bool MacMetalPresent_Init(void* caMetalLayer); + +// Present a CPU pixel buffer (BGRA8, w*h*4 bytes, top-down) to the layer. +// flipY: if true, the source is treated as OpenGL bottom-up and flipped. +// Used for early splash / solid colors before the main render path is up. +void MacMetalPresent_PresentBGRA(int w, int h, const void* pixels, bool flipY); + +// IOSurface zero-copy path. +// +// Acquire returns a CPU-writable pointer backed by an IOSurface that is also +// bound as an MTLTexture. The caller should glReadPixels (or otherwise fill) +// the buffer with BGRA8 pixel data (`*outRowBytes` may exceed `w*4` due to +// alignment, so honor it via glPixelStorei(GL_PACK_ROW_LENGTH, ...)). Then +// call Present to issue the Y-flipped blit to the drawable. Returns NULL on +// failure (caller should fall back to MacMetalPresent_PresentBGRA). +void* MacMetalPresent_AcquireIOSurfaceBuffer(int w, int h, size_t* outRowBytes); +void MacMetalPresent_PresentIOSurface(bool flipY); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/rts/System/Platform/Mac/MetalPresent.mm b/rts/System/Platform/Mac/MetalPresent.mm new file mode 100644 index 00000000000..3e26a6080a2 --- /dev/null +++ b/rts/System/Platform/Mac/MetalPresent.mm @@ -0,0 +1,338 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#import +#import +#import + +#include "MetalPresent.h" +#include +#include +#include + +// Written to be MRC-safe (no ARC assumptions): long-lived objects come from +// +new/Create (owned, +1) and are intentionally kept for process lifetime; +// per-frame objects are autoreleased. + +static id g_device = nil; +static id g_queue = nil; +static CAMetalLayer* g_layer = nil; + +// Path 1: CPU-staging texture for MacMetalPresent_PresentBGRA (early splash). +static id g_staging = nil; +static int g_w = 0; +static int g_h = 0; +static std::vector g_flipBuf; + +// Path 2: IOSurface zero-copy. Engine writes pixels directly into the +// IOSurface base address via glReadPixels (no CPU intermediate buffer, no +// CPU-side Y flip, no replaceRegion upload). A small render pipeline samples +// the IOSurface-backed texture and writes it Y-flipped to the drawable. +static IOSurfaceRef g_ioSurface = nullptr; +static id g_ioTexture = nil; +static int g_ioW = 0; +static int g_ioH = 0; +static bool g_ioLocked = false; +static id g_presentPSO = nil; +static id g_presentPSO_flip = nil; +static id g_linearSampler = nil; + +// Fullscreen-triangle vertex+fragment shader. Two PSOs (flipped + non-flipped) +// keep the per-frame Y-flip choice branch-free in the GPU shader. +static NSString* const kPresentShaderSrc = @R"( +#include +using namespace metal; + +struct VOut { + float4 position [[position]]; + float2 uv; +}; + +vertex VOut vs_present(uint vid [[vertex_id]]) { + // Fullscreen triangle covering NDC [-1,1]^2 with UVs [0,1]^2. + const float2 pos[3] = { float2(-1.0, 3.0), float2(-1.0, -1.0), float2( 3.0, -1.0) }; + const float2 uv [3] = { float2( 0.0, -1.0), float2( 0.0, 1.0), float2( 2.0, 1.0) }; + VOut o; + o.position = float4(pos[vid], 0.0, 1.0); + o.uv = uv[vid]; + return o; +} + +vertex VOut vs_present_flip(uint vid [[vertex_id]]) { + // Same triangle but with V flipped at the source side. + const float2 pos[3] = { float2(-1.0, 3.0), float2(-1.0, -1.0), float2( 3.0, -1.0) }; + const float2 uv [3] = { float2( 0.0, 2.0), float2( 0.0, 0.0), float2( 2.0, 0.0) }; + VOut o; + o.position = float4(pos[vid], 0.0, 1.0); + o.uv = uv[vid]; + return o; +} + +fragment float4 fs_present(VOut in [[stage_in]], + texture2d src [[texture(0)]], + sampler s [[sampler(0)]]) { + return src.sample(s, in.uv); +} +)"; + +static bool BuildPresentPipelines() +{ + if (g_presentPSO != nil && g_presentPSO_flip != nil) + return true; + + NSError* err = nil; + id lib = [g_device newLibraryWithSource:kPresentShaderSrc options:nil error:&err]; + if (lib == nil) { + fprintf(stderr, "[MetalPresent] shader compile failed: %s\n", + err ? [[err localizedDescription] UTF8String] : "(no error)"); + return false; + } + id vs = [lib newFunctionWithName:@"vs_present"]; + id vsFlip = [lib newFunctionWithName:@"vs_present_flip"]; + id fs = [lib newFunctionWithName:@"fs_present"]; + if (vs == nil || vsFlip == nil || fs == nil) { + fprintf(stderr, "[MetalPresent] shader function lookup failed\n"); + return false; + } + + auto makePSO = [&](id vertFn) -> id { + MTLRenderPipelineDescriptor* d = [[MTLRenderPipelineDescriptor alloc] init]; + d.vertexFunction = vertFn; + d.fragmentFunction = fs; + d.colorAttachments[0].pixelFormat = g_layer.pixelFormat; + d.colorAttachments[0].blendingEnabled = NO; + NSError* e = nil; + id p = [g_device newRenderPipelineStateWithDescriptor:d error:&e]; + if (p == nil) + fprintf(stderr, "[MetalPresent] PSO build failed: %s\n", + e ? [[e localizedDescription] UTF8String] : "(no error)"); + return p; + }; + + g_presentPSO = makePSO(vs); + g_presentPSO_flip = makePSO(vsFlip); + + MTLSamplerDescriptor* sd = [[MTLSamplerDescriptor alloc] init]; + sd.minFilter = MTLSamplerMinMagFilterNearest; + sd.magFilter = MTLSamplerMinMagFilterNearest; + sd.sAddressMode = MTLSamplerAddressModeClampToEdge; + sd.tAddressMode = MTLSamplerAddressModeClampToEdge; + g_linearSampler = [g_device newSamplerStateWithDescriptor:sd]; + + return (g_presentPSO != nil && g_presentPSO_flip != nil && g_linearSampler != nil); +} + +bool MacMetalPresent_Init(void* caMetalLayer) +{ + if (g_device != nil) + return true; + if (caMetalLayer == nullptr) + return false; + + g_layer = (CAMetalLayer*)caMetalLayer; + g_device = MTLCreateSystemDefaultDevice(); + if (g_device == nil) + return false; + + g_queue = [g_device newCommandQueue]; + if (g_queue == nil) + return false; + + g_layer.device = g_device; + g_layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + g_layer.framebufferOnly = NO; // allow the drawable to be a blit destination + return true; +} + +void MacMetalPresent_PresentBGRA(int w, int h, const void* pixels, bool flipY) +{ + if (g_device == nil || g_queue == nil || g_layer == nil) + return; + if (w <= 0 || h <= 0 || pixels == nullptr) + return; + + const uint8_t* src = (const uint8_t*)pixels; + const size_t rowBytes = (size_t)w * 4; + + // OpenGL readback is bottom-up; Metal/CoreAnimation is top-down. + if (flipY) { + g_flipBuf.resize(rowBytes * (size_t)h); + for (int y = 0; y < h; ++y) + std::memcpy(&g_flipBuf[(size_t)y * rowBytes], src + (size_t)(h - 1 - y) * rowBytes, rowBytes); + src = g_flipBuf.data(); + } + + if (g_staging == nil || g_w != w || g_h != h) { + MTLTextureDescriptor* d = + [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(NSUInteger)w + height:(NSUInteger)h + mipmapped:NO]; + d.usage = MTLTextureUsageShaderRead; + g_staging = [g_device newTextureWithDescriptor:d]; + g_w = w; + g_h = h; + g_layer.drawableSize = CGSizeMake((CGFloat)w, (CGFloat)h); + } + + [g_staging replaceRegion:MTLRegionMake2D(0, 0, w, h) + mipmapLevel:0 + withBytes:src + bytesPerRow:rowBytes]; + + id drawable = [g_layer nextDrawable]; + if (drawable == nil) { + fprintf(stderr, "[MetalPresent] nextDrawable returned nil — frame not presented\n"); + return; + } + + id cb = [g_queue commandBuffer]; + id blit = [cb blitCommandEncoder]; + [blit copyFromTexture:g_staging + sourceSlice:0 + sourceLevel:0 + sourceOrigin:MTLOriginMake(0, 0, 0) + sourceSize:MTLSizeMake(w, h, 1) + toTexture:drawable.texture + destinationSlice:0 + destinationLevel:0 + destinationOrigin:MTLOriginMake(0, 0, 0)]; + [blit endEncoding]; + [cb presentDrawable:drawable]; + [cb commit]; +} + +static void ReleaseIOSurfaceBacking() +{ + if (g_ioLocked && g_ioSurface) { + IOSurfaceUnlock(g_ioSurface, 0, nullptr); + g_ioLocked = false; + } + g_ioTexture = nil; // autoreleased + if (g_ioSurface) { + CFRelease(g_ioSurface); + g_ioSurface = nullptr; + } + g_ioW = 0; + g_ioH = 0; +} + +static bool EnsureIOSurfaceBacking(int w, int h) +{ + if (g_ioSurface && g_ioW == w && g_ioH == h) + return true; + + ReleaseIOSurfaceBacking(); + + NSDictionary* props = @{ + (id)kIOSurfaceWidth: @(w), + (id)kIOSurfaceHeight: @(h), + (id)kIOSurfaceBytesPerElement: @(4), + (id)kIOSurfacePixelFormat: @((uint32_t)'BGRA'), + }; + g_ioSurface = IOSurfaceCreate((__bridge CFDictionaryRef)props); + if (g_ioSurface == nullptr) { + fprintf(stderr, "[MetalPresent] IOSurfaceCreate failed (%dx%d)\n", w, h); + return false; + } + + MTLTextureDescriptor* d = + [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:(NSUInteger)w + height:(NSUInteger)h + mipmapped:NO]; + d.usage = MTLTextureUsageShaderRead; + d.storageMode = MTLStorageModeShared; + g_ioTexture = [g_device newTextureWithDescriptor:d iosurface:g_ioSurface plane:0]; + if (g_ioTexture == nil) { + fprintf(stderr, "[MetalPresent] newTextureWithDescriptor:iosurface: failed\n"); + ReleaseIOSurfaceBacking(); + return false; + } + + g_ioW = w; + g_ioH = h; + // Drawable size must match the layer's *natural* backing + // pixel count, not the IOSurface size. With SPRING_MAC_NO_RETINA the + // IOSurface is at logical (1x) res while the layer's backing is Retina + // (2x). Setting drawableSize to the smaller IOSurface size makes + // CoreAnimation place the drawable at 1:1 in a corner of the layer. + const CGSize lbSize = g_layer.bounds.size; + const CGFloat cs = g_layer.contentsScale > 0 ? g_layer.contentsScale : 1.0; + const CGFloat targetW = lbSize.width * cs; + const CGFloat targetH = lbSize.height * cs; + const CGFloat finalW = targetW > 0 ? targetW : (CGFloat)w; + const CGFloat finalH = targetH > 0 ? targetH : (CGFloat)h; + g_layer.drawableSize = CGSizeMake(finalW, finalH); + fprintf(stderr, "[MetalPresent/drawable] IOSurface=%dx%d layer.bounds=%.1fx%.1f pt " + "contentsScale=%.2f -> drawableSize=%.1fx%.1f px\n", + w, h, (double)lbSize.width, (double)lbSize.height, + (double)cs, (double)finalW, (double)finalH); + return true; +} + +void* MacMetalPresent_AcquireIOSurfaceBuffer(int w, int h, size_t* outRowBytes) +{ + if (outRowBytes) *outRowBytes = 0; + if (g_device == nil || g_queue == nil || g_layer == nil) + return nullptr; + if (w <= 0 || h <= 0) + return nullptr; + if (!EnsureIOSurfaceBacking(w, h)) + return nullptr; + if (!BuildPresentPipelines()) + return nullptr; + + static bool s_loggedOnce = false; + if (!s_loggedOnce) { + fprintf(stderr, "[MetalPresent] IOSurface zero-copy path active (%dx%d, rowBytes=%zu)\n", + w, h, IOSurfaceGetBytesPerRow(g_ioSurface)); + s_loggedOnce = true; + } + + // Acquire CPU access. AVERTING_PRECEDENT_DEADLOCK: the previous frame's + // Metal command buffer may still hold a GPU reference to the surface; + // IOSurfaceLock blocks until that read is retired. + if (IOSurfaceLock(g_ioSurface, 0, nullptr) != kIOReturnSuccess) { + fprintf(stderr, "[MetalPresent] IOSurfaceLock failed\n"); + return nullptr; + } + g_ioLocked = true; + + const size_t rb = IOSurfaceGetBytesPerRow(g_ioSurface); + if (outRowBytes) *outRowBytes = rb; + return IOSurfaceGetBaseAddress(g_ioSurface); +} + +void MacMetalPresent_PresentIOSurface(bool flipY) +{ + if (g_device == nil || g_queue == nil || g_layer == nil) + return; + if (g_ioSurface == nullptr || g_ioTexture == nil) + return; + + if (g_ioLocked) { + IOSurfaceUnlock(g_ioSurface, 0, nullptr); + g_ioLocked = false; + } + + id drawable = [g_layer nextDrawable]; + if (drawable == nil) { + fprintf(stderr, "[MetalPresent] nextDrawable returned nil — frame not presented\n"); + return; + } + + MTLRenderPassDescriptor* rpd = [MTLRenderPassDescriptor renderPassDescriptor]; + rpd.colorAttachments[0].texture = drawable.texture; + rpd.colorAttachments[0].loadAction = MTLLoadActionDontCare; + rpd.colorAttachments[0].storeAction = MTLStoreActionStore; + + id cb = [g_queue commandBuffer]; + id enc = [cb renderCommandEncoderWithDescriptor:rpd]; + [enc setRenderPipelineState:flipY ? g_presentPSO_flip : g_presentPSO]; + [enc setFragmentTexture:g_ioTexture atIndex:0]; + [enc setFragmentSamplerState:g_linearSampler atIndex:0]; + [enc drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:3]; + [enc endEncoding]; + [cb presentDrawable:drawable]; + [cb commit]; +} diff --git a/rts/System/Platform/Mac/ThreadSupport.cpp b/rts/System/Platform/Mac/ThreadSupport.cpp new file mode 100644 index 00000000000..7148bec906b --- /dev/null +++ b/rts/System/Platform/Mac/ThreadSupport.cpp @@ -0,0 +1,51 @@ +#include "System/Platform/Threading.h" +#include +#include + +namespace Threading { + +void SetupCurrentThreadControls(std::shared_ptr& threadCtls) +{ + threadCtls.reset(new Threading::ThreadControls()); + threadCtls->handle = pthread_self(); + + // macOS has no pthread_setaffinity_np equivalent, so we cannot pin sim + // worker threads to performance cores the way the Linux path does. Hint + // the scheduler instead: USER_INTERACTIVE is the highest QOS tier and + // strongly prefers the performance (P) cluster on Apple Silicon. We + // apply it only to threads that pass through ThreadStart with a + // ThreadControls handle (the sim workers), not every thread in the + // process, so background I/O / helper threads remain free to land on + // the efficiency cluster. + pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0); +} + +void ThreadStart( + std::function taskFunc, + std::shared_ptr* threadCtls, + ThreadControls* tempCtls) +{ + if (threadCtls != nullptr) { + SetupCurrentThreadControls(*threadCtls); + } + + // notify the caller that this thread is running + { + std::lock_guard lock(tempCtls->mutSuspend); + tempCtls->condInitialized.notify_one(); + } + + taskFunc(); +} + +SuspendResult ThreadControls::Suspend() +{ + return Threading::THREADERR_NOT_RUNNING; +} + +SuspendResult ThreadControls::Resume() +{ + return Threading::THREADERR_NONE; +} + +} // namespace Threading diff --git a/rts/System/Platform/ThreadAffinityGuard.cpp b/rts/System/Platform/ThreadAffinityGuard.cpp index 713b7b5a401..436fafeac16 100644 --- a/rts/System/Platform/ThreadAffinityGuard.cpp +++ b/rts/System/Platform/ThreadAffinityGuard.cpp @@ -3,23 +3,27 @@ #include "System/Log/ILog.h" #ifdef _WIN32 #include +#elif defined(__APPLE__) +// macOS has no portable CPU affinity API #else #include #include #include #endif -// Constructor: Saves the current thread's affinity ThreadAffinityGuard::ThreadAffinityGuard() : affinitySaved(false) { #ifdef _WIN32 - threadHandle = GetCurrentThread(); // Get the current thread handle + threadHandle = GetCurrentThread(); savedAffinity = SetThreadAffinityMask(threadHandle, ~0); affinitySaved = ( savedAffinity != 0 ); if (!affinitySaved) { LOG_L(L_WARNING, "GetThreadAffinityMask failed with error code: %lu", GetLastError()); } +#elif defined(__APPLE__) + // no-op on macOS + affinitySaved = false; #else - tid = syscall(SYS_gettid); // Get thread ID + tid = syscall(SYS_gettid); CPU_ZERO(&savedAffinity); if (sched_getaffinity(tid, sizeof(cpu_set_t), &savedAffinity) == 0) { affinitySaved = true; @@ -29,13 +33,14 @@ ThreadAffinityGuard::ThreadAffinityGuard() : affinitySaved(false) { #endif } -// Destructor: Restores the saved affinity if it was successfully stored ThreadAffinityGuard::~ThreadAffinityGuard() { if (affinitySaved) { #ifdef _WIN32 if (!SetThreadAffinityMask(threadHandle, savedAffinity)) { LOG_L(L_WARNING, "SetThreadAffinityMask failed with error code: %lu", GetLastError()); } +#elif defined(__APPLE__) + // no-op on macOS #else if (sched_setaffinity(tid, sizeof(cpu_set_t), &savedAffinity) != 0) { LOG_L(L_WARNING, "Failed to restore thread affinity."); diff --git a/rts/System/Platform/ThreadAffinityGuard.h b/rts/System/Platform/ThreadAffinityGuard.h index 2e456c56e48..c4af0e2cd30 100644 --- a/rts/System/Platform/ThreadAffinityGuard.h +++ b/rts/System/Platform/ThreadAffinityGuard.h @@ -3,6 +3,9 @@ #ifdef _WIN32 #include +#elif defined(__APPLE__) +#include +#include #else #include #endif @@ -12,6 +15,9 @@ class ThreadAffinityGuard { #ifdef _WIN32 DWORD_PTR savedAffinity; HANDLE threadHandle; +#elif defined(__APPLE__) + // macOS has no portable CPU affinity API; stub out + bool dummy; #else cpu_set_t savedAffinity; pid_t tid; @@ -19,16 +25,9 @@ class ThreadAffinityGuard { bool affinitySaved; public: - // Constructor: Saves the current thread's affinity ThreadAffinityGuard(); - - // Destructor: Restores the saved affinity if it was successfully stored ~ThreadAffinityGuard(); - - // Delete copy constructor to prevent copying ThreadAffinityGuard(const ThreadAffinityGuard&) = delete; - - // Delete copy assignment operator to prevent assignment ThreadAffinityGuard& operator=(const ThreadAffinityGuard&) = delete; }; diff --git a/rts/System/Platform/Threading.cpp b/rts/System/Platform/Threading.cpp index 0df646e9a4c..73c3c408941 100644 --- a/rts/System/Platform/Threading.cpp +++ b/rts/System/Platform/Threading.cpp @@ -197,7 +197,7 @@ namespace Threading { // The cache groups from GetProcessorCaches() are sorted in order of largest first. Find the first group that // has a logical processor that will be used to pin the main/worker threads. - auto preferredCache = std::ranges::find_if(pc.groupCaches + auto preferredCache = std::find_if(pc.groupCaches.begin(), pc.groupCaches.end() , [affinityMask](const auto& gc) -> bool { return !!(affinityMask & gc.groupMask); }); std::call_once(preferredMaskDetailsLogFlag, [&](){ diff --git a/rts/System/SafeUtil.h b/rts/System/SafeUtil.h index ce19b3f9738..cc16f88ad28 100644 --- a/rts/System/SafeUtil.h +++ b/rts/System/SafeUtil.h @@ -5,6 +5,8 @@ #include #include +#include +#include namespace spring { template inline void SafeDestruct(T*& p) @@ -112,7 +114,7 @@ namespace spring { static_assert(sizeof(TIn) == sizeof(TOut), "Types must match sizes"); static_assert(std::is_trivially_copyable::value , "Requires TriviallyCopyable input"); static_assert(std::is_trivially_copyable::value, "Requires TriviallyCopyable output"); - static_assert(std::is_trivially_constructible_v, + static_assert(std::is_trivially_default_constructible::value, "This implementation additionally requires destination type to be trivially constructible"); TOut t2; diff --git a/rts/System/Sound/CMakeLists.txt b/rts/System/Sound/CMakeLists.txt index 1e53bb9bd99..c14c5bef6b3 100644 --- a/rts/System/Sound/CMakeLists.txt +++ b/rts/System/Sound/CMakeLists.txt @@ -51,6 +51,13 @@ if (NOT NO_SOUND) include_directories(${CMAKE_SOURCE_DIR}/include/) include_directories(${CMAKE_SOURCE_DIR}/include/AL) + if(APPLE) + # macOS OpenAL.framework has no EFX support, so we vendor the EFX + # headers under include/Mac/AL/ and search that path only on Apple. + # Linux's system OpenAL-Soft devel package provides these headers via + # the OpenAL::OpenAL target, so we must not shadow them there. + include_directories(${CMAKE_SOURCE_DIR}/include/Mac/AL) + endif() add_library(sound STATIC EXCLUDE_FROM_ALL ${soundSources}) target_link_libraries(sound SDL2::SDL2) diff --git a/rts/System/float3.h b/rts/System/float3.h index f096c112c0d..b557556f166 100644 --- a/rts/System/float3.h +++ b/rts/System/float3.h @@ -9,9 +9,8 @@ #include #include "System/BranchPrediction.h" -#include "lib/streflop/streflop_cond.h" -#include "System/creg/creg_cond.h" #include "System/FastMath.h" +#include "System/creg/creg_cond.h" #include "System/type2.h" #ifdef _MSC_VER #include "System/Platform/Win/win32.h" diff --git a/rts/build/cmake/FindLibunwind.cmake b/rts/build/cmake/FindLibunwind.cmake index 2b166993ed9..2cc6c178277 100644 --- a/rts/build/cmake/FindLibunwind.cmake +++ b/rts/build/cmake/FindLibunwind.cmake @@ -48,14 +48,25 @@ if (LIBUNWIND_INCLUDE_DIR AND LIBUNWIND_LIBRARY) set(LIBUNWIND_DEFINITIONS "LIBUNWIND") set(LIBUNWIND_INCLUDE_DIRS ${LIBUNWIND_INCLUDE_DIR}) set(LIBUNWIND_LIBRARIES ${LIBUNWIND_LIBRARY}) - + if (NOT TARGET libunwind::libunwind) - add_library(libunwind::libunwind UNKNOWN IMPORTED) - set_target_properties(libunwind::libunwind PROPERTIES - INTERFACE_COMPILE_DEFINITIONS ${LIBUNWIND_DEFINITIONS} - INTERFACE_INCLUDE_DIRECTORIES ${LIBUNWIND_INCLUDE_DIR} - IMPORTED_LOCATION ${LIBUNWIND_LIBRARY} - ) + if (APPLE) + # On macOS, libunwind is a system library; use INTERFACE library to avoid + # IMPORTED_LOCATION issues with "-framework Cocoa" in Unix Makefiles + add_library(libunwind::libunwind INTERFACE IMPORTED) + set_target_properties(libunwind::libunwind PROPERTIES + INTERFACE_COMPILE_DEFINITIONS ${LIBUNWIND_DEFINITIONS} + INTERFACE_INCLUDE_DIRECTORIES ${LIBUNWIND_INCLUDE_DIR} + INTERFACE_LINK_LIBRARIES "${LIBUNWIND_LIBRARY}" + ) + else() + add_library(libunwind::libunwind UNKNOWN IMPORTED) + set_target_properties(libunwind::libunwind PROPERTIES + INTERFACE_COMPILE_DEFINITIONS ${LIBUNWIND_DEFINITIONS} + INTERFACE_INCLUDE_DIRECTORIES ${LIBUNWIND_INCLUDE_DIR} + IMPORTED_LOCATION ${LIBUNWIND_LIBRARY} + ) + endif() endif() endif() diff --git a/rts/build/cmake/FindSDL2.cmake b/rts/build/cmake/FindSDL2.cmake index 465e270a93d..559fbcc85a1 100644 --- a/rts/build/cmake/FindSDL2.cmake +++ b/rts/build/cmake/FindSDL2.cmake @@ -21,4 +21,28 @@ if (SDL2_FOUND AND NOT TARGET SDL2::SDL2) INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INCLUDE_DIRS}" IMPORTED_LOCATION ${SDL2_LIBRARY} ) +elseif(APPLE AND SDL2_FOUND AND TARGET SDL2::SDL2) + # macOS-only: Homebrew's SDL2 CMake config sets INTERFACE_INCLUDE_DIRECTORIES + # to SDL2_INCLUDE_DIR (e.g. /opt/homebrew/include/SDL2), but this project + # uses #include , so we need the parent directory too. + # Fix the include directories to include both the SDL2 subdirectory and its parent. + # Linux distros' sdl2-config already produces a usable include layout, so + # mutating INTERFACE_INCLUDE_DIRECTORIES there would leak /usr/include into + # every SDL2-consuming target. + get_target_property(_sdl2_includes SDL2::SDL2 INTERFACE_INCLUDE_DIRECTORIES) + if(_sdl2_includes) + set(_new_includes "") + foreach(_inc ${_sdl2_includes}) + list(APPEND _new_includes "${_inc}") + # If this path ends with /SDL2, also add the parent directory + if(_inc MATCHES "/SDL2$") + get_filename_component(_parent "${_inc}" DIRECTORY) + list(APPEND _new_includes "${_parent}") + endif() + endforeach() + list(REMOVE_DUPLICATES _new_includes) + set_target_properties(SDL2::SDL2 PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${_new_includes}" + ) + endif() endif() diff --git a/rts/builds/dedicated/CMakeLists.txt b/rts/builds/dedicated/CMakeLists.txt index f425d68bd4f..94031247fb0 100644 --- a/rts/builds/dedicated/CMakeLists.txt +++ b/rts/builds/dedicated/CMakeLists.txt @@ -24,7 +24,7 @@ list(APPEND engineDedicatedLibraries ${Boost_REGEX_LIBRARY}) list(APPEND engineDedicatedLibraries lua archives_nothreadpool 7zip prd::jsoncpp ${SPRING_MINIZIP_LIBRARY} ZLIB::ZLIB gflags_nothreads_static Tracy::TracyClient) list(APPEND engineDedicatedLibraries headlessStubs engineSystemNet) -if(UNIX AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD")) +if(UNIX AND NOT APPLE AND NOT (CMAKE_SYSTEM_NAME MATCHES "OpenBSD")) list(APPEND engineDedicatedLibraries libunwind::libunwind) endif() list(APPEND engineDedicatedLibraries ${LZMA_LIBRARY}) @@ -83,6 +83,11 @@ elseif (WIN32) set(sources_engine_Platform_CrashHandler ${ENGINE_SRC_ROOT_DIR}/System/Platform/Win/seh.cpp ${ENGINE_SRC_ROOT_DIR}/System/Platform/Win/CrashHandler.cpp) +elseif (APPLE) + set(sources_engine_Platform_CrashHandler + ${ENGINE_SRC_ROOT_DIR}/System/Platform/CpuID.cpp + ${ENGINE_SRC_ROOT_DIR}/System/Platform/Threading.cpp + ${ENGINE_SRC_ROOT_DIR}/System/Platform/Mac/CrashHandler.cpp) else () set(sources_engine_Platform_CrashHandler ${ENGINE_SRC_ROOT_DIR}/System/Platform/CpuID.cpp @@ -141,8 +146,14 @@ if (WIN32) list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/Win/WinVersion.cpp) list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/SharedLib.cpp) list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/Win/DllLib.cpp) -else () +elseif (APPLE) list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/Linux/Hardware.cpp) + list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/SharedLib.cpp) + list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/Linux/SoLib.cpp) +else () + list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/Linux/Hardware.cpp) + list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/SharedLib.cpp) + list(APPEND system_files ${ENGINE_SRC_ROOT_DIR}/System/Platform/Linux/SoLib.cpp) endif () set(engineDedicatedSources diff --git a/rts/builds/headless/CMakeLists.txt b/rts/builds/headless/CMakeLists.txt index 7a7a031c886..3dfd748a6d5 100644 --- a/rts/builds/headless/CMakeLists.txt +++ b/rts/builds/headless/CMakeLists.txt @@ -64,9 +64,22 @@ target_link_libraries(GameHeadless PRIVATE add_executable(engine-headless ${engineSources} ${ENGINE_ICON}) target_link_libraries(engine-headless no-sound ${engineHeadlessLibraries} GameHeadless no-sound) +# macOS ld uses single-pass symbol resolution. GameHeadless references symbols from +# libraries listed earlier (gflags, streflop, etc.) that were skipped on first pass. +# Re-list common libraries so they resolve against GameHeadless. +if(APPLE) + target_link_libraries(engine-headless ${engineCommonLibraries}) +endif() + # Export symbols for plugin access (replaces CMP0065 OLD behavior) set_target_properties(engine-headless PROPERTIES ENABLE_EXPORTS TRUE) +# Suppress "ignoring duplicate libraries" linker warning on macOS. +# Static library transitive deps cause benign duplicates in the link line. +if(APPLE) + target_link_options(engine-headless PRIVATE "LINKER:-no_warn_duplicate_libraries") +endif() + if(MSVC) target_link_libraries(engine-headless engine-natvis) endif() diff --git a/rts/builds/legacy/CMakeLists.txt b/rts/builds/legacy/CMakeLists.txt index cbcc906f83d..8664a80caba 100644 --- a/rts/builds/legacy/CMakeLists.txt +++ b/rts/builds/legacy/CMakeLists.txt @@ -32,7 +32,22 @@ set(engineLibraries SDL2::SDL2) set(OpenGL_GL_PREFERENCE LEGACY) find_package_static(OpenGL 3.0 REQUIRED) -list(APPEND engineLibraries OpenGL::GL) +# On macOS, linking Apple's OpenGL.framework causes its NSWindow notification +# observer to register and then bus-error in +[NSOpenGLContext currentContext] +# on macOS 26 (Tahoe). When a Mesa libEGL is provided, all GL calls in this +# binary are routed through glad function pointers loaded via eglGetProcAddress, +# so we don't need libGL at link time at all -- libEGL alone is enough. +if (APPLE AND SPRING_MAC_LIBEGL) + get_filename_component(_MESA_LIB_DIR "${SPRING_MAC_LIBEGL}" DIRECTORY) + if (EXISTS "${_MESA_LIB_DIR}/libGL.dylib") + list(APPEND engineLibraries "${_MESA_LIB_DIR}/libGL.dylib") + message(STATUS "macOS: linking Mesa libGL at ${_MESA_LIB_DIR}/libGL.dylib") + else() + message(STATUS "macOS: no libGL.dylib next to ${SPRING_MAC_LIBEGL}; relying on libEGL + eglGetProcAddress at runtime") + endif() +else() + list(APPEND engineLibraries OpenGL::GL) +endif() find_fontconfig_hack() find_package_static(Fontconfig 2.11 REQUIRED) @@ -49,15 +64,29 @@ find_freetype_hack() # hack to find different named freetype.dll find_package_static(Freetype 2.8.1 REQUIRED) list(APPEND engineLibraries Freetype::Freetype) -if (UNIX) +if (UNIX AND NOT APPLE) find_package(X11 REQUIRED) target_link_libraries(Game PRIVATE X11::Xcursor) list(APPEND engineLibraries ${X11_Xcursor_LIB} ${X11_X11_LIB}) -endif (UNIX) +endif (UNIX AND NOT APPLE) if (APPLE) find_library(COREFOUNDATION_LIBRARY Foundation) list(APPEND engineLibraries ${COREFOUNDATION_LIBRARY}) + # Metal + QuartzCore + IOSurface for the manual pbuffer->CAMetalLayer present + # path (rts/System/Platform/Mac/MetalPresent.mm). IOSurface backs the + # zero-copy glReadPixels destination shared with the MTLTexture. + find_library(METAL_LIBRARY Metal) + find_library(QUARTZCORE_LIBRARY QuartzCore) + find_library(IOSURFACE_LIBRARY IOSurface) + list(APPEND engineLibraries ${METAL_LIBRARY} ${QUARTZCORE_LIBRARY} ${IOSURFACE_LIBRARY}) + set(SPRING_MAC_LIBEGL "" CACHE FILEPATH + "Optional path to a Mesa libEGL.dylib (e.g. a Zink+KosmicKrisp build). \ +Leave empty to link only against Apple's OpenGL.framework.") + if (SPRING_MAC_LIBEGL) + list(APPEND engineLibraries "${SPRING_MAC_LIBEGL}") + endif() + list(APPEND engineLibraries objc) endif (APPLE) list(APPEND engineLibraries squish rgetc1) @@ -87,6 +116,13 @@ target_link_libraries(engine-legacy ${engineLibraries} Game) # Export symbols for plugin access (replaces CMP0065 OLD behavior) set_target_properties(engine-legacy PROPERTIES ENABLE_EXPORTS TRUE) +# Suppress "ignoring duplicate libraries" linker warning on macOS. +# Static library transitive deps (engineSim, pr-downloader, sound, etc.) propagate +# dependencies that are already in engineLibraries, causing benign duplicates. +if(APPLE) + target_link_options(engine-legacy PRIVATE "LINKER:-no_warn_duplicate_libraries") +endif() + if(MSVC) target_link_libraries(engine-legacy engine-natvis) endif() diff --git a/rts/lib/CMakeLists.txt b/rts/lib/CMakeLists.txt index 74b8b28076a..80737fba7d6 100644 --- a/rts/lib/CMakeLists.txt +++ b/rts/lib/CMakeLists.txt @@ -60,6 +60,13 @@ SET(GFLAGS_BUILD_TESTING FALSE) SET(GFLAGS_INSTALL_HEADERS FALSE) SET(GFLAGS_INSTALL_SHARED_LIBS FALSE) SET(GFLAGS_INSTALL_STATIC_LIBS FALSE) +# As a subdirectory build, gflags defaults to namespace "gflags" only. On +# macOS Homebrew's may be picked up first via +# /opt/homebrew/include (DevIL etc. drag it in), and that header pins +# GFLAGS_NAMESPACE=google -- causing the DEFINE_* macros in engine code to +# emit google::FlagRegisterer references. Build the vendored gflags with +# both namespaces so the resulting static lib resolves either spelling. +SET(GFLAGS_NAMESPACE "google;gflags") #Meh-meh if (WIN32) diff --git a/rts/lib/assimp/code/TextureTransform.h b/rts/lib/assimp/code/TextureTransform.h index ba5a8cea23c..6cf3dc2535f 100644 --- a/rts/lib/assimp/code/TextureTransform.h +++ b/rts/lib/assimp/code/TextureTransform.h @@ -121,19 +121,19 @@ struct STransformVecInfo : public aiUVTransform // We use a small epsilon here const static float epsilon = 0.05f; - if (math::fabs( mTranslation.x - other.mTranslation.x ) > epsilon || - math::fabs( mTranslation.y - other.mTranslation.y ) > epsilon) + if (math::fabs(static_cast(mTranslation.x - other.mTranslation.x)) > epsilon || + math::fabs(static_cast(mTranslation.y - other.mTranslation.y)) > epsilon) { return false; } - if (math::fabs( mScaling.x - other.mScaling.x ) > epsilon || - math::fabs( mScaling.y - other.mScaling.y ) > epsilon) + if (math::fabs(static_cast(mScaling.x - other.mScaling.x)) > epsilon || + math::fabs(static_cast(mScaling.y - other.mScaling.y)) > epsilon) { return false; } - if (math::fabs( mRotation - other.mRotation) > epsilon) + if (math::fabs(static_cast(mRotation - other.mRotation)) > epsilon) { return false; } @@ -173,8 +173,8 @@ struct STransformVecInfo : public aiUVTransform if (mRotation) { aiMatrix3x3 mRot; - mRot.a1 = mRot.b2 = math::cos(mRotation); - mRot.a2 = mRot.b1 = math::sin(mRotation); + mRot.a1 = mRot.b2 = math::cos(static_cast(mRotation)); + mRot.a2 = mRot.b1 = math::sin(static_cast(mRotation)); mRot.a2 = -mRot.a2; mOut *= mRot; } diff --git a/rts/lib/assimp/code/fast_atof.h b/rts/lib/assimp/code/fast_atof.h index 99647b671e4..1211b4a56a3 100644 --- a/rts/lib/assimp/code/fast_atof.h +++ b/rts/lib/assimp/code/fast_atof.h @@ -348,7 +348,7 @@ inline const char* fast_atoreal_move(const char* c, Real& out, bool check_comma if (einv) { exp = -exp; } - f *= math::pow(static_cast(10.0), exp); + f *= math::pow(static_cast(10.0), static_cast(exp)); } if (inv) { diff --git a/rts/lib/assimp/include/assimp/matrix3x3.inl b/rts/lib/assimp/include/assimp/matrix3x3.inl index d90c2ad2eb6..801bd8751aa 100644 --- a/rts/lib/assimp/include/assimp/matrix3x3.inl +++ b/rts/lib/assimp/include/assimp/matrix3x3.inl @@ -1,3 +1,4 @@ +#include /* --------------------------------------------------------------------------- Open Asset Import Library (assimp) @@ -52,6 +53,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "matrix4x4.h" #include +#include #include "lib/streflop/streflop_cond.h" #include diff --git a/rts/lib/assimp/include/assimp/matrix4x4.inl b/rts/lib/assimp/include/assimp/matrix4x4.inl index 42851f48bce..b54aa811dfc 100644 --- a/rts/lib/assimp/include/assimp/matrix4x4.inl +++ b/rts/lib/assimp/include/assimp/matrix4x4.inl @@ -1,3 +1,4 @@ +#include /* --------------------------------------------------------------------------- Open Asset Import Library (assimp) diff --git a/rts/lib/assimp/include/assimp/quaternion.inl b/rts/lib/assimp/include/assimp/quaternion.inl index b2bacacb35a..21eb732f548 100644 --- a/rts/lib/assimp/include/assimp/quaternion.inl +++ b/rts/lib/assimp/include/assimp/quaternion.inl @@ -1,3 +1,4 @@ +#include /* --------------------------------------------------------------------------- Open Asset Import Library (assimp) @@ -86,7 +87,7 @@ inline aiQuaterniont::aiQuaterniont( const aiMatrix3x3t &pRotMatri // large enough if( t > static_cast(0)) { - TReal s = math::sqrt(1 + t) * static_cast(2.0); + TReal s = static_cast(math::sqrt(static_cast(1 + t))) * static_cast(2.0); x = (pRotMatrix.c2 - pRotMatrix.b3) / s; y = (pRotMatrix.a3 - pRotMatrix.c1) / s; z = (pRotMatrix.b1 - pRotMatrix.a2) / s; @@ -95,7 +96,7 @@ inline aiQuaterniont::aiQuaterniont( const aiMatrix3x3t &pRotMatri else if( pRotMatrix.a1 > pRotMatrix.b2 && pRotMatrix.a1 > pRotMatrix.c3 ) { // Column 0: - TReal s = math::sqrt( static_cast(1.0) + pRotMatrix.a1 - pRotMatrix.b2 - pRotMatrix.c3) * static_cast(2.0); + TReal s = static_cast(math::sqrt(static_cast(static_cast(1.0) + pRotMatrix.a1 - pRotMatrix.b2 - pRotMatrix.c3))) * static_cast(2.0); x = static_cast(0.25) * s; y = (pRotMatrix.b1 + pRotMatrix.a2) / s; z = (pRotMatrix.a3 + pRotMatrix.c1) / s; @@ -104,7 +105,7 @@ inline aiQuaterniont::aiQuaterniont( const aiMatrix3x3t &pRotMatri else if( pRotMatrix.b2 > pRotMatrix.c3) { // Column 1: - TReal s = math::sqrt( static_cast(1.0) + pRotMatrix.b2 - pRotMatrix.a1 - pRotMatrix.c3) * static_cast(2.0); + TReal s = static_cast(math::sqrt(static_cast(static_cast(1.0) + pRotMatrix.b2 - pRotMatrix.a1 - pRotMatrix.c3))) * static_cast(2.0); x = (pRotMatrix.b1 + pRotMatrix.a2) / s; y = static_cast(0.25) * s; z = (pRotMatrix.c2 + pRotMatrix.b3) / s; @@ -112,7 +113,7 @@ inline aiQuaterniont::aiQuaterniont( const aiMatrix3x3t &pRotMatri } else { // Column 2: - TReal s = math::sqrt( static_cast(1.0) + pRotMatrix.c3 - pRotMatrix.a1 - pRotMatrix.b2) * static_cast(2.0); + TReal s = static_cast(math::sqrt(static_cast(static_cast(1.0) + pRotMatrix.c3 - pRotMatrix.a1 - pRotMatrix.b2))) * static_cast(2.0); x = (pRotMatrix.a3 + pRotMatrix.c1) / s; y = (pRotMatrix.c2 + pRotMatrix.b3) / s; z = static_cast(0.25) * s; @@ -125,12 +126,12 @@ inline aiQuaterniont::aiQuaterniont( const aiMatrix3x3t &pRotMatri template inline aiQuaterniont::aiQuaterniont( TReal fPitch, TReal fYaw, TReal fRoll ) { - const TReal fSinPitch(math::sin(fPitch*static_cast(0.5))); - const TReal fCosPitch(math::cos(fPitch*static_cast(0.5))); - const TReal fSinYaw(math::sin(fYaw*static_cast(0.5))); - const TReal fCosYaw(math::cos(fYaw*static_cast(0.5))); - const TReal fSinRoll(math::sin(fRoll*static_cast(0.5))); - const TReal fCosRoll(math::cos(fRoll*static_cast(0.5))); + const TReal fSinPitch(static_cast(math::sin(static_cast(fPitch*static_cast(0.5))))); + const TReal fCosPitch(static_cast(math::cos(static_cast(fPitch*static_cast(0.5))))); + const TReal fSinYaw(static_cast(math::sin(static_cast(fYaw*static_cast(0.5))))); + const TReal fCosYaw(static_cast(math::cos(static_cast(fYaw*static_cast(0.5))))); + const TReal fSinRoll(static_cast(math::sin(static_cast(fRoll*static_cast(0.5))))); + const TReal fCosRoll(static_cast(math::cos(static_cast(fRoll*static_cast(0.5))))); const TReal fCosPitchCosYaw(fCosPitch*fCosYaw); const TReal fSinPitchSinYaw(fSinPitch*fSinYaw); x = fSinRoll * fCosPitchCosYaw - fCosRoll * fSinPitchSinYaw; @@ -165,8 +166,8 @@ inline aiQuaterniont::aiQuaterniont( aiVector3t axis, TReal angle) { axis.Normalize(); - const TReal sin_a = math::sin( angle / 2 ); - const TReal cos_a = math::cos( angle / 2 ); + const TReal sin_a = static_cast(math::sin(static_cast(angle / 2))); + const TReal cos_a = static_cast(math::cos(static_cast(angle / 2))); x = axis.x * sin_a; y = axis.y * sin_a; z = axis.z * sin_a; @@ -186,7 +187,7 @@ inline aiQuaterniont::aiQuaterniont( aiVector3t normalized) if (t < static_cast(0.0)) { w = static_cast(0.0); } - else w = math::sqrt (t); + else w = static_cast(math::sqrt(static_cast(t))); } // --------------------------------------------------------------------------- @@ -216,10 +217,10 @@ inline void aiQuaterniont::Interpolate( aiQuaterniont& pOut, const aiQuat { // Standard case (slerp) TReal omega, sinom; - omega = math::acos( cosom); // extract theta from dot product's cos theta - sinom = math::sin( omega); - sclp = math::sin( (static_cast(1.0) - pFactor) * omega) / sinom; - sclq = math::sin( pFactor * omega) / sinom; + omega = static_cast(math::acos(static_cast(cosom))); // extract theta from dot product's cos theta + sinom = static_cast(math::sin(static_cast(omega))); + sclp = static_cast(math::sin(static_cast((static_cast(1.0) - pFactor) * omega))) / sinom; + sclq = static_cast(math::sin(static_cast(pFactor * omega))) / sinom; } else { // Very close, do linear interp (because it's faster) @@ -238,7 +239,7 @@ template inline aiQuaterniont& aiQuaterniont::Normalize() { // compute the magnitude and divide through it - const TReal mag = math::sqrt(x*x + y*y + z*z + w*w); + const TReal mag = static_cast(math::sqrt(static_cast(x*x + y*y + z*z + w*w))); if (mag) { const TReal invMag = static_cast(1.0)/mag; diff --git a/rts/lib/assimp/include/assimp/types.h b/rts/lib/assimp/include/assimp/types.h index b083fbf0a36..dae31011d25 100644 --- a/rts/lib/assimp/include/assimp/types.h +++ b/rts/lib/assimp/include/assimp/types.h @@ -65,6 +65,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "quaternion.h" #ifdef __cplusplus +#include #include #include // for std::nothrow_t #include // for aiString::Set(const std::string&) @@ -219,7 +220,7 @@ struct aiColor3D /** Check whether a color is black */ bool IsBlack() const { static const ai_real epsilon = ai_real(10e-3); - return math::fabs( r ) < epsilon && math::fabs( g ) < epsilon && math::fabs( b ) < epsilon; + return math::fabs( static_cast(r) ) < epsilon && math::fabs( static_cast(g) ) < epsilon && math::fabs( static_cast(b) ) < epsilon; } #endif // !__cplusplus diff --git a/rts/lib/assimp/include/assimp/vector2.inl b/rts/lib/assimp/include/assimp/vector2.inl index e2750e42784..561d98f1488 100644 --- a/rts/lib/assimp/include/assimp/vector2.inl +++ b/rts/lib/assimp/include/assimp/vector2.inl @@ -1,3 +1,4 @@ +#include /* --------------------------------------------------------------------------- Open Asset Import Library (assimp) @@ -50,6 +51,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifdef __cplusplus #include "vector2.h" +#include #include "lib/streflop/streflop_cond.h" // ------------------------------------------------------------------------------------------------ diff --git a/rts/lib/assimp/include/assimp/vector3.inl b/rts/lib/assimp/include/assimp/vector3.inl index b33b60c5b08..ebca15fe0db 100644 --- a/rts/lib/assimp/include/assimp/vector3.inl +++ b/rts/lib/assimp/include/assimp/vector3.inl @@ -1,3 +1,4 @@ +#include /* --------------------------------------------------------------------------- Open Asset Import Library (assimp) @@ -50,6 +51,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #ifdef __cplusplus #include "vector3.h" +#include #include "lib/streflop/streflop_cond.h" // ------------------------------------------------------------------------------------------------ @@ -94,7 +96,7 @@ AI_FORCE_INLINE TReal aiVector3t::SquareLength() const { // ------------------------------------------------------------------------------------------------ template AI_FORCE_INLINE TReal aiVector3t::Length() const { - return math::sqrt( SquareLength()); + return static_cast(math::sqrt(static_cast(SquareLength()))); } // ------------------------------------------------------------------------------------------------ template diff --git a/rts/lib/glad/CMakeLists.txt b/rts/lib/glad/CMakeLists.txt index a67e710dd36..5ff6a5724f2 100644 --- a/rts/lib/glad/CMakeLists.txt +++ b/rts/lib/glad/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.5) project(Glad) -if (UNIX AND NOT MINGW) +if (UNIX AND NOT MINGW AND NOT APPLE) add_library(glad glad.c glad_glx.c) -else (UNIX AND NOT MINGW) +else () add_library(glad glad.c) -endif (UNIX AND NOT MINGW) +endif () target_include_directories(glad PUBLIC /) \ No newline at end of file diff --git a/rts/lib/headlessStubs/gladstub.cpp b/rts/lib/headlessStubs/gladstub.cpp index f34a055a716..b62348fbef9 100644 --- a/rts/lib/headlessStubs/gladstub.cpp +++ b/rts/lib/headlessStubs/gladstub.cpp @@ -72,6 +72,8 @@ int GLAD_GL_ARB_conservative_depth = 0; int GLAD_GL_ARB_clip_control = 0; int GLAD_GL_ARB_buffer_storage = 0; int GLAD_GL_KHR_debug = 0; +int GLAD_GL_ATI_meminfo = 0; +int GLAD_GL_NVX_gpu_memory_info = 0; GLenum APIENTRY impl_glCheckFramebufferStatus(GLenum target) { return GL_FRAMEBUFFER_COMPLETE; @@ -303,6 +305,7 @@ decltype(glad_glMemoryBarrier) glad_glMemoryBarrier = nullptr; decltype(glad_glMinSampleShading) glad_glMinSampleShading = nullptr; decltype(glad_glMultMatrixd) glad_glMultMatrixd = nullptr; decltype(glad_glMultMatrixf) glad_glMultMatrixf = nullptr; +decltype(glad_glMultiDrawArraysIndirect) glad_glMultiDrawArraysIndirect = nullptr; decltype(glad_glMultiDrawElementsIndirect) glad_glMultiDrawElementsIndirect = nullptr; decltype(glad_glMultiTexCoord1f) glad_glMultiTexCoord1f = nullptr; decltype(glad_glMultiTexCoord2f) glad_glMultiTexCoord2f = nullptr; @@ -372,6 +375,7 @@ decltype(glad_glTexParameterf) glad_glTexParameterf = nullptr; decltype(glad_glTexParameterfv) glad_glTexParameterfv = nullptr; decltype(glad_glTexParameteri) glad_glTexParameteri = nullptr; decltype(glad_glTexParameteriv) glad_glTexParameteriv = nullptr; +decltype(glad_glTexStorage1D) glad_glTexStorage1D = nullptr; decltype(glad_glTexStorage2D) glad_glTexStorage2D = nullptr; decltype(glad_glTexStorage3D) glad_glTexStorage3D = nullptr; decltype(glad_glTexSubImage2D) glad_glTexSubImage2D = nullptr; @@ -526,6 +530,8 @@ decltype(glad_glClearBufferuiv) glad_glClearBufferuiv = nullptr; decltype(glad_glClearBufferiv) glad_glClearBufferiv = nullptr; decltype(glad_glClearBufferfv) glad_glClearBufferfv = nullptr; decltype(glad_glGetTextureSubImage) glad_glGetTextureSubImage = nullptr; +decltype(glad_glDebugMessageCallback) glad_glDebugMessageCallback = nullptr; +decltype(glad_glDebugMessageControl) glad_glDebugMessageControl = nullptr; namespace Impl { template @@ -742,6 +748,7 @@ int gladLoadGL(void) { glad_glMinSampleShading = MakeStubImpl(glad_glMinSampleShading); glad_glMultMatrixd = MakeStubImpl(glad_glMultMatrixd); glad_glMultMatrixf = MakeStubImpl(glad_glMultMatrixf); + glad_glMultiDrawArraysIndirect = MakeStubImpl(glad_glMultiDrawArraysIndirect); glad_glMultiDrawElementsIndirect = MakeStubImpl(glad_glMultiDrawElementsIndirect); glad_glMultiTexCoord1f = MakeStubImpl(glad_glMultiTexCoord1f); glad_glMultiTexCoord2f = MakeStubImpl(glad_glMultiTexCoord2f); @@ -811,6 +818,7 @@ int gladLoadGL(void) { glad_glTexParameterfv = MakeStubImpl(glad_glTexParameterfv); glad_glTexParameteri = MakeStubImpl(glad_glTexParameteri); glad_glTexParameteriv = MakeStubImpl(glad_glTexParameteriv); + glad_glTexStorage1D = MakeStubImpl(glad_glTexStorage1D); glad_glTexStorage2D = MakeStubImpl(glad_glTexStorage2D); glad_glTexStorage3D = MakeStubImpl(glad_glTexStorage3D); glad_glTexSubImage2D = MakeStubImpl(glad_glTexSubImage2D); @@ -965,6 +973,8 @@ int gladLoadGL(void) { glad_glClearBufferiv = MakeStubImpl(glad_glClearBufferiv); glad_glClearBufferfv = MakeStubImpl(glad_glClearBufferfv); glad_glGetTextureSubImage = MakeStubImpl(glad_glGetTextureSubImage); + glad_glDebugMessageCallback = MakeStubImpl(glad_glDebugMessageCallback); + glad_glDebugMessageControl = MakeStubImpl(glad_glDebugMessageControl); return 0; } \ No newline at end of file diff --git a/rts/lib/smmalloc/smmalloc.h b/rts/lib/smmalloc/smmalloc.h index 4bc0786b3a3..2ff57577329 100644 --- a/rts/lib/smmalloc/smmalloc.h +++ b/rts/lib/smmalloc/smmalloc.h @@ -28,6 +28,7 @@ #include #include #include +#include //#define SMMALLOC_STATS_SUPPORT @@ -660,7 +661,7 @@ struct TlsPoolBucket } }; -static_assert((std::is_trivial::value && std::is_standard_layout::value) == true, "TlsPoolBucket must be POD type, stored in TLS"); +static_assert(std::is_trivially_copyable_v, "TlsPoolBucket must be POD type, stored in TLS"); static_assert(sizeof(TlsPoolBucket) <= 64, "TlsPoolBucket sizeof must be less than CPU cache line"); } // namespace internal diff --git a/rts/lib/smmalloc/smmalloc_generic.cpp b/rts/lib/smmalloc/smmalloc_generic.cpp index 6b3be01835d..1e3447b160d 100644 --- a/rts/lib/smmalloc/smmalloc_generic.cpp +++ b/rts/lib/smmalloc/smmalloc_generic.cpp @@ -1,3 +1,4 @@ +#include // The MIT License (MIT) // // Copyright (c) 2017-2021 Sergey Makeev @@ -20,7 +21,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. #include "smmalloc.h" -#include +#include struct Header { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 81e3d5aaf8c..5b7250e8c56 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -311,9 +311,15 @@ endif() list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Win/Hardware.cpp") list(APPEND test_libs ${IPHLPAPI_LIBRARY}) - else (WIN32) + elseif (APPLE) list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Linux/Hardware.cpp") - endif (WIN32) + list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/SharedLib.cpp") + list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Linux/SoLib.cpp") + else () + list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Linux/Hardware.cpp") + list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/SharedLib.cpp") + list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Linux/SoLib.cpp") + endif () add_spring_test(${test_name} "${test_src}" "${test_libs}" "") add_dependencies(test_${test_name} generateVersionFiles) include_directories("${ENGINE_SOURCE_DIR}/lib") @@ -395,9 +401,11 @@ endif() ) if (WIN32) list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Win/CpuTopology.cpp") - else (WIN32) + elseif (APPLE) + list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Mac/CpuTopology.cpp") + else () list(APPEND test_src "${ENGINE_SOURCE_DIR}/System/Platform/Linux/CpuTopology.cpp") - endif (WIN32) + endif () set(test_libs ${WINMM_LIBRARY} ) diff --git a/test/other/testMutex.cpp b/test/other/testMutex.cpp index 0a84f129e45..ab701c6b579 100644 --- a/test/other/testMutex.cpp +++ b/test/other/testMutex.cpp @@ -12,8 +12,12 @@ #include #ifndef _WIN32 - #include - #include + #ifndef __APPLE__ + #include + #include + #else + #include + #endif #endif #ifdef _WIN32 @@ -37,6 +41,19 @@ InitSpringTime ist; *m = 0; } +#ifdef __APPLE__ + static os_unfair_lock apple_lock = OS_UNFAIR_LOCK_INIT; + static void futex_lock(futex* m) + { + os_unfair_lock_lock(&apple_lock); + *m = 1; + } + static void futex_unlock(futex* m) + { + *m = 0; + os_unfair_lock_unlock(&apple_lock); + } +#else static void futex_lock(futex* m) { futex c; @@ -56,6 +73,7 @@ InitSpringTime ist; } } #endif +#endif diff --git a/tools/unitsync/CMakeLists.txt b/tools/unitsync/CMakeLists.txt index 00f124f0a78..892bf3f5a7e 100644 --- a/tools/unitsync/CMakeLists.txt +++ b/tools/unitsync/CMakeLists.txt @@ -62,12 +62,12 @@ set(main_files "${ENGINE_SRC_ROOT}/Game/GameVersion.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaConstEngine.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaEncoding.cpp" + "${ENGINE_SRC_ROOT}/Lua/LuaLibs.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaMathExtra.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaMemPool.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaParser.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaUtils.cpp" "${ENGINE_SRC_ROOT}/Lua/LuaIO.cpp" - "${ENGINE_SRC_ROOT}/Lua/LuaLibs.cpp" "${ENGINE_SRC_ROOT}/Map/MapParser.cpp" "${ENGINE_SRC_ROOT}/Map/SMF/SMFMapFile.cpp" "${ENGINE_SRC_ROOT}/Sim/Misc/SideParser.cpp" @@ -107,11 +107,16 @@ if (WIN32) list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Win/WinVersion.cpp") list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/SharedLib.cpp") list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Win/DllLib.cpp") -else (WIN32) +elseif (APPLE) + list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Mac/CpuTopology.cpp") + list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Linux/Hardware.cpp") + list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/SharedLib.cpp") + list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Linux/SoLib.cpp") +else () list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Linux/CpuTopology.cpp") list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Linux/Hardware.cpp") list(APPEND main_files "${ENGINE_SRC_ROOT}/System/Platform/Linux/ThreadSupport.cpp") -endif (WIN32) +endif () set(unitsync_files ${sources_engine_System_FileSystem}