macOS (Apple Silicon) support: native build running under Rosetta 2 with MoltenVK (#102)

* macos: POSIX platform layer (host fault handler, virtual memory)

- hostException: Mach/POSIX signal-based host fault handler mirroring the
  Windows vectored handler (SIGSEGV/SIGBUS/SIGILL), behind
  '#elif defined(__APPLE__)'. Windows and Linux branches are untouched.
- sysLinuxVirtual: mach_vm_region-based is_mapped (no /proc/self/maps on
  macOS), PTHREAD_MUTEX_NORMAL, and a MAP_FIXED carve-in-place allocator that
  never leaves an unmapped hole for dyld/Rosetta/Metal to claim. All
  __APPLE__-guarded; the original Linux allocator path is preserved verbatim.
- sysLinuxDbg/sysLinuxFileIO: libgen.h for basename(), and a POSIX
  opendir/readdir implementation of SysFileGetDents (previously a stub).

* macos: guest kernel backing, threads, and POSIX libs

- memory/memoryAddressSpace: anonymous shm_open backing (macOS has no
  memfd_create) and re-reserve-on-unmap so a guest MAP_FIXED remap never
  destroys a host mapping (__APPLE__-guarded). Drops host <sys/mman.h> MAP_*
  macros so the guest's constexpr MAP_* constants compile (no-op on Windows).
- pthread: undef the host PTHREAD_STACK_MIN macro; on non-Windows, pin the
  stack-switch asm's guest rsp/rbp to callee-saved r14/r15 (the template
  clobbers r12/r13); __APPLE__ no-op for the absent pthread_condattr_setclock.
  The Windows stack-switch asm is byte-identical.
- network: define SOCKET/INVALID_SOCKET for the non-Windows paths (were
  undefined at 20+ use sites), and rename the non-Windows kernel_clock_*
  helpers to the CamelCase names the callers already use. Both repair the
  shared non-Windows build. Integer reinterpret_cast -> static_cast.

* loader: pin stack-switch asm operands to callee-saved registers

RunEntry switches to the guest stack with an asm template that clobbers
r12/r13 before consuming its inputs. With plain "r" constraints the compiler
may place func/guest_rsp/guest_rbp into r12/r13, so 'callq *func' jumps
through the saved host rsp -- a latent miscompile on every platform that any
unrelated codegen change can trigger. Pin the inputs to rbx/r14/r15, which the
template never touches and the SysV callee preserves.

General correctness fix (not macOS-specific); the same fix is applied to
pthread RunOnGuestStack.

* macos: Vulkan/MoltenVK graphics enablement

- pageManager: GPU write-tracking via Mach VM queries + mprotect (mprotect
  cannot report the previous protection, so the expected-old check is dropped),
  thread id via pthread_mach_thread_np, 4 KB page-size check -- __APPLE__-guarded.
- shaders/renderDraw/vulkanWindow: MoltenVK lacks VK_EXT_color_write_enable,
  VK_EXT_depth_clip_enable and depthBounds; fall back to static color-write
  masks and default depth clipping, request VK_KHR_portability_subset, and
  reuse the graphics queue for present when only one queue is exposed. Non-Apple
  pipeline/extension setup is unchanged.
- window: optional borderless window (KYTY_BORDERLESS) to sidestep a Rosetta
  NSException in macOS window chrome.

* graphics: macOS thread identity for region tracking

Region-ownership locks resolve the current thread via GetCurrentThreadId()
on Windows and EXIT elsewhere. Use the Mach thread port on macOS (nonzero
per-thread id; 0 stays the no-owner sentinel).

* macos: marshal AppKit window operations to the main thread

The present worker thread shows the window, updates its icon and title, and
recreates a lost Vulkan surface. AppKit traps with 'Must only be used from the
main thread' when these run off the main thread. Route them through a small
task queue drained by the SDL main loop (an SDL_USEREVENT wakes the loop when
it is blocked in SDL_WaitEvent). Title updates are fire-and-forget; showing
the window and surface recreation wait for completion.

Windows and Linux call the SDL functions directly, as before.

* build: ignore the _Build output directory

_Build is the conventional out-of-tree build location (only _Build/vscode-clang
was ignored); a stray git add could sweep build artifacts into a commit.

* macos: isolate stack-switch workaround

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
M. Abdullah
2026-07-28 22:03:15 +05:00
committed by GitHub
parent 8fe4765f9e
commit 4d79f19089
16 changed files with 577 additions and 39 deletions
+125
View File
@@ -6,6 +6,9 @@
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#include <windows.h> // IWYU pragma: keep
#elif defined(__APPLE__)
#include <csignal>
#include <sys/ucontext.h>
#endif
// IWYU pragma: no_include <errhandlingapi.h>
@@ -118,6 +121,98 @@ static LONG WINAPI ExceptionFilter(PEXCEPTION_POINTERS exception) {
return handler(info) ? EXCEPTION_CONTINUE_EXECUTION : EXCEPTION_CONTINUE_SEARCH;
}
#elif defined(__APPLE__)
static std::atomic<Handler> g_handler {nullptr};
static std::atomic_uint32_t g_install_state {0};
static thread_local bool g_in_exception_filter = false;
static_assert(decltype(g_handler)::is_always_lock_free);
static_assert(decltype(g_install_state)::is_always_lock_free);
[[noreturn]] static void FailFast(const char* reason) noexcept {
std::fputs("HostException fail-fast: ", stderr);
std::fputs(reason != nullptr ? reason : "unspecified", stderr);
std::fputc('\n', stderr);
std::fflush(stderr);
std::_Exit(321);
}
// Translate the x86-64 page-fault error code (mcontext __es.__err) into an access type.
// bit 1 (0x2) = write, bit 4 (0x10) = instruction fetch, otherwise a read.
static AccessViolationType DecodeAccess(uint64_t err) {
if ((err & 0x10u) != 0) {
return AccessViolationType::Execute;
}
if ((err & 0x2u) != 0) {
return AccessViolationType::Write;
}
return AccessViolationType::Read;
}
// POSIX signal handler that mirrors the Windows vectored handler: build an ExceptionInfo
// from the mcontext and dispatch. A resolved fault (handler returns true) simply returns,
// re-executing the faulting instruction against the now-fixed protection. An unresolved
// fault restores the default disposition so the retry terminates the process.
static void SignalHandler(int sig, siginfo_t* si, void* uctx) {
if (g_in_exception_filter) {
FailFast("nested exception while resolving a host fault");
}
g_in_exception_filter = true;
auto* uc = static_cast<ucontext_t*>(uctx);
const auto* mc = uc->uc_mcontext;
const auto& ss = mc->__ss;
ExceptionInfo info {};
info.exception_address = ss.__rip;
info.native_code = static_cast<uint32_t>(si->si_code);
info.native_context = uctx;
if (sig == SIGILL) {
info.type = ExceptionType::IllegalInstruction;
} else {
info.type = ExceptionType::AccessViolation;
info.access_violation_type = DecodeAccess(mc->__es.__err);
info.access_violation_vaddr = reinterpret_cast<uint64_t>(si->si_addr);
}
info.rax = ss.__rax;
info.rbx = ss.__rbx;
info.rcx = ss.__rcx;
info.rdx = ss.__rdx;
info.rsi = ss.__rsi;
info.rdi = ss.__rdi;
info.rbp = ss.__rbp;
info.rsp = ss.__rsp;
info.r8 = ss.__r8;
info.r9 = ss.__r9;
info.r10 = ss.__r10;
info.r11 = ss.__r11;
info.r12 = ss.__r12;
info.r13 = ss.__r13;
info.r14 = ss.__r14;
info.r15 = ss.__r15;
const auto handler = g_handler.load(std::memory_order_acquire);
if (handler == nullptr) {
FailFast("host exception callback is null");
}
const bool resolved = handler(info);
g_in_exception_filter = false;
if (resolved) {
return; // retry the faulting instruction against the fixed mapping
}
// Unresolved: restore the default action so the re-executed instruction terminates.
struct sigaction dfl {};
dfl.sa_handler = SIG_DFL;
sigemptyset(&dfl.sa_mask);
sigaction(sig, &dfl, nullptr);
}
#endif
bool InstallHandler(Handler handler) {
@@ -140,6 +235,36 @@ bool InstallHandler(Handler handler) {
return false;
}
g_install_state.store(2, std::memory_order_release);
return true;
#elif defined(__APPLE__)
if (handler == nullptr) {
return false;
}
uint32_t expected_state = 0;
if (!g_install_state.compare_exchange_strong(expected_state, 1, std::memory_order_acq_rel)) {
return expected_state == 2 && g_handler.load(std::memory_order_acquire) == handler;
}
g_handler.store(handler, std::memory_order_release);
struct sigaction sa {};
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
// macOS raises SIGBUS for protection faults on some paths and SIGSEGV on others;
// SIGILL covers instructions the host cannot execute (routed to the x64 emulator).
bool ok = sigaction(SIGSEGV, &sa, nullptr) == 0 && sigaction(SIGBUS, &sa, nullptr) == 0 &&
sigaction(SIGILL, &sa, nullptr) == 0;
if (!ok) {
g_handler.store(nullptr, std::memory_order_release);
g_install_state.store(0, std::memory_order_release);
printf("sigaction() failed to install the host fault handler\n");
return false;
}
g_install_state.store(2, std::memory_order_release);
return true;
#else
+21 -2
View File
@@ -11,6 +11,7 @@
#include <cerrno>
#include <cstdlib>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <utime.h>
@@ -536,8 +537,26 @@ void SysFileFindFiles(const std::filesystem::path& /*path*/,
EXIT("not implemented\n");
}
void SysFileGetDents(const std::filesystem::path& /*path*/, std::vector<sys_dir_entry_t>& /*out*/) {
EXIT("not implemented\n");
void SysFileGetDents(const std::filesystem::path& path, std::vector<sys_dir_entry_t>& out) {
DIR* dir = opendir(path.c_str());
if (dir == nullptr) {
return;
}
for (dirent* entry = readdir(dir); entry != nullptr; entry = readdir(dir)) {
sys_dir_entry_t r {};
r.name = entry->d_name;
if (entry->d_type == DT_REG) {
r.is_file = true;
} else if (entry->d_type == DT_DIR) {
r.is_file = false;
} else {
// DT_UNKNOWN / symlink: resolve with stat.
struct stat st {};
r.is_file = (stat((path / entry->d_name).c_str(), &st) == 0) ? S_ISREG(st.st_mode) : true;
}
out.push_back(std::move(r));
}
closedir(dir);
}
bool SysFileCopyFile(const std::filesystem::path& /*src*/, const std::filesystem::path& /*dst*/) {
+82
View File
@@ -12,6 +12,11 @@
#include <pthread.h>
#include <sys/mman.h>
#if defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_vm.h>
#endif
// IWYU pragma: no_include <asm/mman-common.h>
// IWYU pragma: no_include <asm/mman.h>
// IWYU pragma: no_include <bits/pthread_types.h>
@@ -129,6 +134,32 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
ret_addr = reinterpret_cast<uintptr_t>(ptr);
if (ptr != MAP_FAILED) {
#if defined(__APPLE__)
// Carve the aligned subrange out of the live mapping with MAP_FIXED (in-place
// replacement) and trim the slack; never munmap the whole range first, or a
// concurrent host mapping (dyld, Rosetta, Metal) could claim the hole and be
// destroyed by the MAP_FIXED. Other platforms keep the original path below.
auto aligned_addr = align_up(ret_addr, alignment);
// NOLINTNEXTLINE
void* fixed = mmap(reinterpret_cast<void*>(aligned_addr), size, protect,
MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0);
if (fixed == MAP_FAILED) {
munmap(ptr, size + alignment);
ret_addr = 0;
ptr = MAP_FAILED;
} else {
if (aligned_addr > ret_addr) {
munmap(reinterpret_cast<void*>(ret_addr), aligned_addr - ret_addr);
}
const uintptr_t tail_start = aligned_addr + size;
const uintptr_t resv_end = ret_addr + size + alignment;
if (resv_end > tail_start) {
munmap(reinterpret_cast<void*>(tail_start), resv_end - tail_start);
}
ptr = fixed;
ret_addr = aligned_addr;
}
#else
munmap(ptr, size + alignment);
auto aligned_addr = align_up(ret_addr, alignment);
#ifdef KYTY_FIXED_NOREPLACE
@@ -146,6 +177,7 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
ret_addr = 0;
ptr = MAP_FAILED;
}
#endif
}
}
@@ -165,6 +197,27 @@ uint64_t SysVirtualAllocAligned(uint64_t address, uint64_t size, VirtualMemory::
return ret_addr;
}
#if defined(__APPLE__)
// macOS has no /proc/self/maps; query the Mach VM map directly. mach_vm_region returns
// the first mapped region at or above `region_addr`; if it begins before the end of the
// requested range, the range overlaps an existing mapping.
static bool is_mapped(void* ptr, size_t length) {
auto query_addr = reinterpret_cast<mach_vm_address_t>(ptr);
mach_vm_address_t region_addr = query_addr;
mach_vm_size_t region_size = 0;
vm_region_basic_info_data_64_t info {};
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
mach_port_t object_name = MACH_PORT_NULL;
kern_return_t kr =
mach_vm_region(mach_task_self(), &region_addr, &region_size, VM_REGION_BASIC_INFO_64,
reinterpret_cast<vm_region_info_t>(&info), &count, &object_name);
if (kr != KERN_SUCCESS) {
return false; // no region at or above the address → unmapped
}
return region_addr < (query_addr + length);
}
#else
static bool is_mapped(void* ptr, size_t length) {
FILE* file = fopen("/proc/self/maps", "r");
char line[1024];
@@ -189,6 +242,7 @@ static bool is_mapped(void* ptr, size_t length) {
fclose(file);
return ret;
}
#endif
bool SysVirtualAllocFixed(uint64_t address, uint64_t size, VirtualMemory::Mode mode) {
EXIT_IF(g_allocs == nullptr);
@@ -261,6 +315,33 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); // NOLINT
ret_addr = reinterpret_cast<uintptr_t>(ptr);
if (ptr != MAP_FAILED) {
#if defined(__APPLE__)
// Carve the aligned subrange out of the live reservation with MAP_FIXED (an
// in-place replacement), then trim the slack. The range must never be
// returned to the OS in between: another thread (dyld, Rosetta, Metal,
// malloc) could claim the hole, and the subsequent MAP_FIXED would silently
// destroy its mapping. Other platforms keep the original path below.
auto aligned_addr = align_up(ret_addr, alignment);
// NOLINTNEXTLINE
void* fixed = mmap(reinterpret_cast<void*>(aligned_addr), size, PROT_NONE,
MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0);
if (fixed == MAP_FAILED) {
munmap(ptr, size + alignment);
ret_addr = 0;
ptr = MAP_FAILED;
} else {
if (aligned_addr > ret_addr) {
munmap(reinterpret_cast<void*>(ret_addr), aligned_addr - ret_addr);
}
const uintptr_t tail_start = aligned_addr + size;
const uintptr_t resv_end = ret_addr + size + alignment;
if (resv_end > tail_start) {
munmap(reinterpret_cast<void*>(tail_start), resv_end - tail_start);
}
ptr = fixed;
ret_addr = aligned_addr;
}
#else
munmap(ptr, size + alignment);
auto aligned_addr = align_up(ret_addr, alignment);
#ifdef KYTY_FIXED_NOREPLACE
@@ -278,6 +359,7 @@ uint64_t SysVirtualReserveAligned(uint64_t address, uint64_t size, uint64_t alig
ret_addr = 0;
ptr = MAP_FAILED;
}
#endif
}
}