macOS (Apple silicon and Intel): Fry3DX.app with its content in Resources and MoltenVK in Frameworks, OpenGL 4.1 when there's no Vulkan; the program's own path, memory, OS and CPU names on a Mac; files dropped on the window (and opened from the Finder) open; presets macos-arm64/macos-x64, a CI job, tools/package.py macos makes one universal app

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-20 17:36:24 +02:00
commit e22cd2e3e6
15 changed files with 273 additions and 40 deletions

View file

@ -2,6 +2,7 @@
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <thread>
@ -15,6 +16,11 @@
#include <sys/utsname.h>
#include <unistd.h>
#endif
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <sys/sysctl.h>
#endif
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__x86_64__) || defined(__i386__)
@ -30,6 +36,14 @@ std::string trim(std::string s) {
return s.substr(i);
}
#if defined(__APPLE__)
std::string sysctlText(const char* name) {
char buf[256] = {};
size_t size = sizeof buf - 1;
return sysctlbyname(name, buf, &size, nullptr, 0) == 0 ? trim(buf) : std::string();
}
#endif
// The CPU's own name for itself (x86: cpuid's brand string; elsewhere /proc/cpuinfo).
std::string cpuName() {
#if defined(_MSC_VER) || defined(__x86_64__) || defined(__i386__)
@ -46,6 +60,9 @@ std::string cpuName() {
char brand[49] = {};
std::memcpy(brand, regs, 48);
if (brand[0]) return trim(brand);
#endif
#if defined(__APPLE__)
if (std::string s = sysctlText("machdep.cpu.brand_string"); !s.empty()) return s; // ("Apple M2 Pro" too)
#endif
std::ifstream in("/proc/cpuinfo");
for (std::string line; std::getline(in, line);)
@ -64,6 +81,11 @@ std::string osName() {
fn(&v);
const char* name = v.dwMajorVersion == 10 ? (v.dwBuildNumber >= 22000 ? "Windows 11" : "Windows 10") : "Windows";
return std::string(name) + " (build " + std::to_string(v.dwBuildNumber) + ")";
#elif defined(__APPLE__)
utsname u{};
uname(&u);
const std::string v = sysctlText("kern.osproductversion");
return "macOS " + (v.empty() ? std::string("?") : v) + " (" + u.machine + ")";
#else
utsname u{};
uname(&u);
@ -95,6 +117,19 @@ Memory memory() {
if (GlobalMemoryStatusEx(&s)) m.total = s.ullTotalPhys, m.used = s.ullTotalPhys - s.ullAvailPhys;
PROCESS_MEMORY_COUNTERS pmc{sizeof pmc};
if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof pmc)) m.ours = pmc.WorkingSetSize;
#elif defined(__APPLE__)
uint64_t total = 0;
size_t size = sizeof total;
if (sysctlbyname("hw.memsize", &total, &size, nullptr, 0) == 0) m.total = total;
vm_statistics64_data_t vm{};
mach_msg_type_number_t count = HOST_VM_INFO64_COUNT;
if (host_statistics64(mach_host_self(), HOST_VM_INFO64, reinterpret_cast<host_info64_t>(&vm), &count) == KERN_SUCCESS) {
const uint64_t page = uint64_t(sysconf(_SC_PAGESIZE)); // (used as Activity Monitor counts it: in use by apps, wired, compressed)
m.used = (uint64_t(vm.internal_page_count) - vm.purgeable_count + vm.wire_count + vm.compressor_page_count) * page;
}
mach_task_basic_info_data_t task{};
mach_msg_type_number_t taskCount = MACH_TASK_BASIC_INFO_COUNT;
if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, reinterpret_cast<task_info_t>(&task), &taskCount) == KERN_SUCCESS) m.ours = task.resident_size;
#else
uint64_t available = 0;
std::ifstream in("/proc/meminfo");
@ -114,4 +149,39 @@ Memory memory() {
return m;
}
std::string executablePath() {
#if defined(_WIN32)
char exe[1024] = {};
GetModuleFileNameA(nullptr, exe, sizeof exe);
return exe;
#elif defined(__APPLE__)
char exe[4096] = {};
uint32_t size = sizeof exe;
if (_NSGetExecutablePath(exe, &size) != 0) return {};
std::error_code ec;
const auto real = std::filesystem::canonical(exe, ec);
return ec ? std::string(exe) : real.string();
#else
std::error_code ec;
const auto p = std::filesystem::read_symlink("/proc/self/exe", ec);
return ec ? std::string() : p.string();
#endif
}
bool debuggerAttached() {
#if defined(_WIN32)
return IsDebuggerPresent();
#elif defined(__APPLE__)
kinfo_proc info{};
size_t size = sizeof info;
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
return sysctl(mib, 4, &info, &size, nullptr, 0) == 0 && (info.kp_proc.p_flag & P_TRACED) != 0;
#else
std::ifstream f("/proc/self/status");
for (std::string line; std::getline(f, line);)
if (line.rfind("TracerPid:", 0) == 0) return std::atoi(line.c_str() + 10) != 0;
return false;
#endif
}
} // namespace keel::sys

View file

@ -18,4 +18,9 @@ struct Memory {
};
Memory memory();
// This program's file (for starting it again: crash reports, restarts). Empty if unknown.
std::string executablePath();
// Is a debugger watching (the freeze watchdog waits then)?
bool debuggerAttached();
} // namespace keel::sys

View file

@ -1,6 +1,7 @@
#include "crash.h"
#include <core/log.h>
#include <core/sysinfo.h>
#include <SDL3/SDL.h>
@ -122,17 +123,6 @@ void onSignal(int sig) {
}
#endif
bool debuggerAttached() {
#ifdef _WIN32
return IsDebuggerPresent();
#else
std::ifstream f("/proc/self/status");
for (std::string line; std::getline(f, line);)
if (line.rfind("TracerPid:", 0) == 0) return std::atoi(line.c_str() + 10) != 0;
return false;
#endif
}
void watchdog() {
constexpr double kTimeout = 45.0;
for (;;) {
@ -141,7 +131,7 @@ void watchdog() {
g_beat = seconds();
continue;
}
if (seconds() - g_beat < kTimeout || debuggerAttached()) continue;
if (seconds() - g_beat < kTimeout || sys::debuggerAttached()) continue;
#ifndef _WIN32
pthread_kill(g_main, SIGUSR2); // its handler reports from the stuck thread (its backtrace)
std::this_thread::sleep_for(std::chrono::seconds(3));
@ -153,12 +143,8 @@ void watchdog() {
void install(const std::string& prefPath, const std::vector<std::string>& args, bool dog) {
g_report = prefPath + "crash.txt";
#ifdef _WIN32
char exe[1024] = {};
GetModuleFileNameA(nullptr, exe, sizeof exe);
g_exe = exe;
#else
g_exe = std::filesystem::read_symlink("/proc/self/exe").string();
g_exe = sys::executablePath();
#ifndef _WIN32
g_main = pthread_self();
#endif
g_args = {g_exe, "-crashreport", g_report, "--"};

View file

@ -4,6 +4,7 @@
#include "core/console.h"
#include "core/cvar.h"
#include "core/log.h"
#include "core/sysinfo.h"
#include "core/time.h"
#include "fs/vfs.h"
#include "platform/input.h"
@ -289,6 +290,10 @@ int runClient(Game& game) {
#if defined(__ANDROID__)
if (first != RenderApi::Vulkan) first = RenderApi::GLES30;
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::GLES30};
#elif defined(__APPLE__)
// (A Mac: Vulkan through MoltenVK, else OpenGL as far as macOS has it: 4.1.)
if (first != RenderApi::Vulkan) first = RenderApi::OpenGL40;
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::OpenGL40};
#else
const std::initializer_list<RenderApi> all{RenderApi::Vulkan, RenderApi::OpenGL, RenderApi::OpenGL40, RenderApi::OpenGL31, RenderApi::GLES30};
#endif
@ -356,6 +361,10 @@ int runClient(Game& game) {
else game.onEscape();
continue;
}
if (ev.type == SDL_EVENT_DROP_FILE && ev.drop.data) {
game.onFileDropped(ev.drop.data);
continue;
}
// Don't let the toggle key's character land in the console input.
if (ev.type == SDL_EVENT_TEXT_INPUT && ev.text.text[0] == '`') continue;
input::handleEvent(ev);
@ -443,20 +452,17 @@ const char* buildVersion() { return KEEL_COMMIT ", " KEEL_COMMIT_DATE; }
// Replaces this process with a fresh one (Windows: starts it, then this one exits).
void relaunch(const std::vector<std::string>& args) {
std::vector<std::string> a = args;
const std::string exe = sys::executablePath();
#ifdef _WIN32
char exe[1024] = {};
GetModuleFileNameA(nullptr, exe, sizeof exe);
for (std::string& x : a)
if (x.find(' ') != std::string::npos) x = "\"" + x + "\"";
#else
const std::string exe = std::filesystem::read_symlink("/proc/self/exe").string();
#endif
std::vector<char*> argv;
for (std::string& x : a) argv.push_back(x.data());
argv.push_back(nullptr);
log::info("Restarting");
#ifdef _WIN32
_spawnv(_P_NOWAIT, exe, argv.data());
_spawnv(_P_NOWAIT, exe.c_str(), argv.data());
#else
execv(exe.c_str(), argv.data());
log::error("Restart failed");

View file

@ -33,6 +33,9 @@ public:
// The window's close button (or the OS asking it to close): false keeps it open (to ask
// about saving first; the game quits later itself). The quit command always quits.
virtual bool allowQuit() { return true; }
// A file dropped on the window, or opened with the program from the desktop (a Mac's Finder
// hands it over this way, not on the command line).
virtual void onFileDropped(const std::string& path) { (void)path; }
// Hot reload: called twice a second (hotreload 1) after the engine reloaded what changed
// (`changed`: textures, models, sounds); the game reloads its own (scripts, the map).
virtual void hotReload(const std::vector<std::string>& changed) { (void)changed; }

View file

@ -21,7 +21,14 @@ namespace {
// one) unless asked. Vulkan picks the discrete GPU itself; this makes OpenGL do the same. Set
// before SDL loads the GL library; anything the user already set wins. KEEL_IGPU=1 opts out.
void preferDedicatedGpu() {
#ifndef _WIN32
#if defined(__APPLE__)
// macOS has no Vulkan of its own: the app carries MoltenVK (Vulkan on Metal) in
// Contents/Frameworks. (Unless the user pointed SDL somewhere else.)
if (const char* base = SDL_GetBasePath(); base && !SDL_GetHint(SDL_HINT_VULKAN_LIBRARY)) {
const std::filesystem::path mvk = std::filesystem::path(base) / ".." / "Frameworks" / "libMoltenVK.dylib";
if (std::filesystem::exists(mvk)) SDL_SetHint(SDL_HINT_VULKAN_LIBRARY, mvk.lexically_normal().string().c_str());
}
#elif !defined(_WIN32)
if (std::getenv("KEEL_IGPU")) return;
if (std::filesystem::exists("/proc/driver/nvidia")) { // NVIDIA's own driver: PRIME render offload
setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 0);

View file

@ -46,6 +46,11 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback(VkDebugUtilsMessageSeverityF
bool Device::init(const Window& window, bool validation) {
window_ = &window;
#if defined(__APPLE__)
// The library SDL loaded for the window (the app's MoltenVK, or a Vulkan SDK's loader).
if (auto gipa = reinterpret_cast<PFN_vkGetInstanceProcAddr>(SDL_Vulkan_GetVkGetInstanceProcAddr())) volkInitializeCustom(gipa);
else
#endif
if (volkInitialize() != VK_SUCCESS) {
log::error("No Vulkan loader found - is a Vulkan driver installed?");
return false;