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 19:03:15 +02:00
committed by GitHub
co-authored by nmzik
parent 8fe4765f9e
commit 4d79f19089
16 changed files with 577 additions and 39 deletions
+2 -1
View File
@@ -2,4 +2,5 @@
.vs/
.idea/
build/
_Build/vscode-clang/
_Build/vscode-clang/
_Build/
+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
}
}
+88 -7
View File
@@ -19,6 +19,12 @@
#include <windows.h>
#undef min
#undef max
#elif defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace Libs::Graphics {
@@ -28,16 +34,55 @@ constexpr uint64_t PAGE_SIZE = TRACKER_PAGE_SIZE;
constexpr uint64_t REGION_SIZE = TRACKER_REGION_SIZE;
constexpr uint64_t ADDRESS_SIZE = TRACKER_ADDRESS_SIZE;
constexpr uint64_t REGION_COUNT = ADDRESS_SIZE / REGION_SIZE;
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
// The tracker reuses Win32 memory-protection tags as internal page-state values (on
// Windows they come from <windows.h> and are what VirtualQuery returns). Mirror the
// canonical Win32 numeric values so the shared state-machine logic is identical.
constexpr uint32_t PAGE_NOACCESS = 0x01;
constexpr uint32_t PAGE_READONLY = 0x02;
constexpr uint32_t PAGE_READWRITE = 0x04;
#endif
constexpr uint64_t REGION_PAGES = REGION_SIZE / PAGE_SIZE;
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
constexpr uint32_t NO_ACCESS_PROTECTION = PAGE_NOACCESS;
constexpr uint32_t READ_ONLY_PROTECTION = PAGE_READONLY;
constexpr uint32_t NO_ACCESS_PROTECTION = PAGE_NOACCESS;
constexpr uint32_t READ_ONLY_PROTECTION = PAGE_READONLY;
constexpr uint32_t READ_WRITE_PROTECTION = PAGE_READWRITE;
#else
constexpr uint32_t NO_ACCESS_PROTECTION = 0;
constexpr uint32_t READ_ONLY_PROTECTION = 1;
constexpr uint32_t READ_WRITE_PROTECTION = 2;
#if defined(__APPLE__)
// Map the tracker's Win32-style protection tags to POSIX mprotect flags.
static int PageProtToPosix(uint32_t protection) {
switch (protection) {
case PAGE_NOACCESS: return PROT_NONE;
case PAGE_READONLY: return PROT_READ;
case PAGE_READWRITE: return PROT_READ | PROT_WRITE;
default: return PROT_NONE;
}
}
// Query the current protection of the page containing vaddr via the Mach VM map and
// collapse it to the tracker's read/write tags (execute is irrelevant to write tracking).
static uint32_t MachQueryPageProt(uint64_t vaddr) {
auto region_addr = static_cast<mach_vm_address_t>(vaddr);
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 || region_addr > vaddr) {
return PAGE_NOACCESS; // no region covering vaddr
}
if ((info.protection & VM_PROT_WRITE) != 0) {
return PAGE_READWRITE;
}
if ((info.protection & VM_PROT_READ) != 0) {
return PAGE_READONLY;
}
return PAGE_NOACCESS;
}
#endif
thread_local bool g_in_fault_resolution = false;
@@ -78,6 +123,8 @@ thread_local bool g_in_fault_resolution = false;
uint32_t CurrentThread() noexcept {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
return GetCurrentThreadId();
#elif defined(__APPLE__)
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
#else
FailFast();
#endif
@@ -145,6 +192,12 @@ struct PageManager::Impl {
Fatal("unsupported host page size 0x%08" PRIx32,
static_cast<uint32_t>(info.dwPageSize));
}
#elif defined(__APPLE__)
// Under Rosetta the host page size is 4 KB, matching TRACKER_PAGE_SIZE.
if (static_cast<uint64_t>(getpagesize()) != PAGE_SIZE) {
Fatal("unsupported host page size 0x%08" PRIx32,
static_cast<uint32_t>(getpagesize()));
}
#else
Fatal("page-fault invalidation is not implemented on this platform");
#endif
@@ -225,6 +278,14 @@ struct PageManager::Impl {
vaddr, static_cast<uint32_t>(info.State), static_cast<uint32_t>(info.Protect));
}
return info.Protect;
#elif defined(__APPLE__)
const uint32_t protection = MachQueryPageProt(vaddr);
if (protection != PAGE_READWRITE) {
Fatal("basic path requires PAGE_READWRITE at 0x%016" PRIx64 " (protection=0x%08" PRIx32
")",
vaddr, protection);
}
return protection;
#else
(void)vaddr;
Fatal("page query is unsupported on this platform");
@@ -245,6 +306,14 @@ struct PageManager::Impl {
case PageFaultAccess::Write: return info.Protect == PAGE_READWRITE;
default: return false;
}
#elif defined(__APPLE__)
const uint32_t protection = MachQueryPageProt(vaddr);
switch (access) {
case PageFaultAccess::Read:
return protection == PAGE_READONLY || protection == PAGE_READWRITE;
case PageFaultAccess::Write: return protection == PAGE_READWRITE;
default: return false;
}
#else
(void)vaddr;
return false;
@@ -265,6 +334,18 @@ struct PageManager::Impl {
", expected=0x%08" PRIx32 ", new=0x%08" PRIx32,
vaddr, static_cast<uint32_t>(old_protection), expected_old, protection);
}
#elif defined(__APPLE__)
// mprotect cannot report the previous protection, so the expected_old comparison
// is dropped; the tracker is the sole mutator of these pages and drives the
// transition from its own shadow state.
(void)expected_old;
if (mprotect(reinterpret_cast<void*>(static_cast<uintptr_t>(vaddr)), PAGE_SIZE,
PageProtToPosix(protection)) != 0) {
if (fault_path) {
FailFast("mprotect fault transition failed");
}
Fatal("mprotect failed at 0x%016" PRIx64 ", new=0x%08" PRIx32, vaddr, protection);
}
#else
(void)vaddr;
(void)protection;
+5
View File
@@ -16,6 +16,8 @@
#include <windows.h>
#undef min
#undef max
#elif defined(__APPLE__)
#include <pthread.h>
#endif
namespace Libs::Graphics {
@@ -49,6 +51,9 @@ private:
static uint32_t CurrentThread() noexcept {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
return GetCurrentThreadId();
#elif defined(__APPLE__)
// mach thread port is a nonzero per-thread id (0 is the "no owner" sentinel).
return static_cast<uint32_t>(pthread_mach_thread_np(pthread_self()));
#else
EXIT("region tracking thread identity is unsupported on this platform\n");
#endif
@@ -395,6 +395,10 @@ static void SetDynamicParams(const RenderCommandBuffer& buffer, vk::CommandBuffe
dynamic_params.stencil_back.reference);
}
#if defined(__APPLE__)
// MoltenVK has no VK_EXT_color_write_enable; the pipeline is created without the
// eColorWriteEnableEXT dynamic state and relies on the static colorWriteMask instead.
#else
vk::Bool32 enable[RENDER_COLOR_ATTACHMENTS_MAX] = {};
for (uint32_t i = 0; i < dynamic_params.color_write_count; i++) {
enable[i] = (dynamic_params.color_write_enable[i] ? VK_TRUE : VK_FALSE);
@@ -402,6 +406,7 @@ static void SetDynamicParams(const RenderCommandBuffer& buffer, vk::CommandBuffe
if (dynamic_params.color_write_count != 0) {
vk_buffer.setColorWriteEnableEXT(dynamic_params.color_write_count, enable);
}
#endif
}
static bool DrawHasValidVertexShader(const HW::Shader& sh_ctx) {
+19 -1
View File
@@ -729,7 +729,13 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
vk::PipelineRasterizationStateCreateInfo rasterizer {};
rasterizer.sType = vk::StructureType::ePipelineRasterizationStateCreateInfo;
// MoltenVK lacks VK_EXT_depth_clip_enable; omit the depth-clip struct on macOS and accept
// Vulkan's default depth clipping (enabled) instead of the PS5's clamp behavior.
#if defined(__APPLE__)
rasterizer.pNext = nullptr;
#else
rasterizer.pNext = &clip_ext;
#endif
rasterizer.flags = {};
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
@@ -807,7 +813,13 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
vk::PipelineColorBlendStateCreateInfo color_blending {};
color_blending.sType = vk::StructureType::ePipelineColorBlendStateCreateInfo;
// MoltenVK lacks VK_EXT_color_write_enable; drop the dynamic color-write struct on macOS
// and rely on each attachment's static colorWriteMask (all channels enabled by default).
#if defined(__APPLE__)
color_blending.pNext = nullptr;
#else
color_blending.pNext = &color_write;
#endif
color_blending.flags = {};
color_blending.logicOpEnable = VK_FALSE;
color_blending.logicOp = vk::LogicOp::eCopy;
@@ -873,7 +885,11 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
depth_stencil_info.depthWriteEnable = (static_params.depth_write_enable ? VK_TRUE : VK_FALSE);
depth_stencil_info.depthCompareOp = static_params.depth_compare_op;
depth_stencil_info.depthBoundsTestEnable =
#if defined(__APPLE__)
VK_FALSE; // MoltenVK lacks the depthBounds feature; depth-bounds testing is disabled
#else
(static_params.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
#endif
depth_stencil_info.stencilTestEnable = (static_params.stencil_test_enable ? VK_TRUE : VK_FALSE);
depth_stencil_info.front.failOp = static_params.stencil_front.failOp;
depth_stencil_info.front.passOp = static_params.stencil_front.passOp;
@@ -893,7 +909,9 @@ void CreatePipelineInternal(GraphicContext& graphics, DescriptorCache& descripto
vk::DynamicState::eStencilCompareMask,
vk::DynamicState::eStencilReference,
vk::DynamicState::eStencilWriteMask,
vk::DynamicState::eColorWriteEnableEXT,
#if !defined(__APPLE__)
vk::DynamicState::eColorWriteEnableEXT, // unsupported by MoltenVK; static mask instead
#endif
};
const auto dynamic_states_count =
static_cast<uint32_t>(sizeof(dynamic_states) / sizeof(dynamic_states[0]));
@@ -586,7 +586,13 @@ void Swapchain::RefreshSurfaceSize() {
void Swapchain::Recreate(bool surface_lost) {
Destroy();
if (surface_lost) {
#if defined(__APPLE__)
// Surface recreation goes through SDL_Vulkan_CreateSurface, which touches the
// window's view/layer and must run on the main thread on macOS.
m_window.RunOnMainThread([this] { m_window.RecreateSurface(); }, true);
#else
m_window.RecreateSurface();
#endif
}
RefreshSurfaceSize();
Create();
@@ -797,9 +803,20 @@ void Presenter::Present(Frame& frame, bool reuse) {
auto& window = m_impl->window;
if (window.window_hidden) {
#if defined(__APPLE__)
// AppKit traps if a window is shown off the main thread; marshal and wait so the
// swapchain below is recreated against a visible window.
window.RunOnMainThread(
[&window] {
window.UpdateIcon();
SDL_ShowWindow(window.window);
},
true);
#else
window.UpdateIcon();
SDL_ShowWindow(window.window);
#endif
window.window_hidden = false;
m_impl->RecoverSwapchain(Swapchain::Status::Recreate);
@@ -217,7 +217,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
if (color_write_ext.colorWriteEnable != VK_TRUE) {
LOGF("colorWriteEnable is not supported\n");
#if !defined(__APPLE__)
skip_device = true;
#endif
}
if (depth_clip_control.depthClipControl != VK_TRUE) {
@@ -226,7 +228,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
}
if (depth_clip_enable.depthClipEnable != VK_TRUE) {
LOGF("depthClipEnable is not supported\n");
#if !defined(__APPLE__)
skip_device = true;
#endif
}
if (features12.samplerMirrorClampToEdge != VK_TRUE) {
@@ -271,7 +275,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
}
if (device_features2.features.depthBounds != VK_TRUE) {
LOGF("depthBounds is not supported\n");
#if !defined(__APPLE__)
skip_device = true;
#endif
}
if (device_features2.features.shaderStorageImageWriteWithoutFormat != VK_TRUE) {
LOGF("shaderStorageImageWriteWithoutFormat is not supported\n");
@@ -494,7 +500,14 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V
vk::PhysicalDeviceDepthClipControlFeaturesEXT depth_clip_control {};
depth_clip_control.sType = vk::StructureType::ePhysicalDeviceDepthClipControlFeaturesEXT;
// MoltenVK lacks VK_EXT_depth_clip_enable and VK_EXT_color_write_enable, so drop those
// feature structs from the chain on macOS (the renderer falls back to default depth
// clipping and static color-write masks).
#if defined(__APPLE__)
depth_clip_control.pNext = nullptr;
#else
depth_clip_control.pNext = &depth_clip_enable;
#endif
depth_clip_control.depthClipControl = VK_TRUE;
vk::PhysicalDeviceVulkan12Features features12 {};
@@ -541,7 +554,9 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V
device_features.fragmentStoresAndAtomics = VK_TRUE;
device_features.samplerAnisotropy = VK_TRUE;
device_features.robustBufferAccess = VK_TRUE;
device_features.depthBounds = VK_TRUE;
#if !defined(__APPLE__)
device_features.depthBounds = VK_TRUE; // unsupported by MoltenVK
#endif
device_features.shaderStorageImageWriteWithoutFormat = VK_TRUE;
device_features.shaderStorageImageReadWithoutFormat = VK_TRUE;
device_features.shaderImageGatherExtended = VK_TRUE;
@@ -894,10 +909,20 @@ void WindowContext::CreateVulkan() {
}
surface = native_surface;
std::vector<const char*> device_extensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME, VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME,
VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME, VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME,
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME, "VK_KHR_maintenance1"};
std::vector<const char*> device_extensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME,
VK_EXT_DEPTH_CLIP_CONTROL_EXTENSION_NAME,
VK_KHR_PUSH_DESCRIPTOR_EXTENSION_NAME,
"VK_KHR_maintenance1"};
#if defined(__APPLE__)
// MoltenVK lacks VK_EXT_depth_clip_enable and VK_EXT_color_write_enable; the renderer
// falls back to default depth clipping and static color-write masks on macOS. It also
// requires VK_KHR_portability_subset per the Vulkan portability spec.
device_extensions.push_back("VK_KHR_portability_subset");
#else
device_extensions.push_back(VK_EXT_DEPTH_CLIP_ENABLE_EXTENSION_NAME);
device_extensions.push_back(VK_EXT_COLOR_WRITE_ENABLE_EXTENSION_NAME);
#endif
#ifdef KYTY_ENABLE_DEBUG_PRINTF
if (Config::SpirvDebugPrintfEnabled()) {
+67 -1
View File
@@ -1,5 +1,7 @@
#include "graphics/presentation/window.h"
#include <cstdlib>
#include "SDL.h"
#include "SDL_error.h"
#include "SDL_events.h"
@@ -697,6 +699,52 @@ void WindowContext::ProcessEvent(double time_s) {
}
}
#if defined(__APPLE__)
void WindowContext::RunOnMainThread(std::function<void()> task, bool wait) {
if (Common::Thread::IsMainThread()) {
task();
return;
}
uint64_t ticket = 0;
{
Common::LockGuard lock(main_task_mutex);
main_tasks.push_back(std::move(task));
ticket = ++main_tasks_queued;
}
// Wake the main loop in case it is blocked in SDL_WaitEvent.
SDL_Event event {};
event.type = SDL_USEREVENT;
SDL_PushEvent(&event);
if (!wait) {
return;
}
Common::LockGuard lock(main_task_mutex);
while (main_tasks_run < ticket) {
main_task_done.Wait(&main_task_mutex);
}
}
void WindowContext::DrainMainThreadTasks() {
std::vector<std::function<void()>> tasks;
{
Common::LockGuard lock(main_task_mutex);
tasks.swap(main_tasks);
}
if (tasks.empty()) {
return;
}
for (auto& task: tasks) {
task();
}
Common::LockGuard lock(main_task_mutex);
main_tasks_run += tasks.size();
main_task_done.SignalAll();
}
#endif
void WindowContext::Run() {
Common::Timer timer;
timer.Start();
@@ -706,6 +754,9 @@ void WindowContext::Run() {
loop.paused.store(false, std::memory_order_release);
while (!loop.need_exit) {
#if defined(__APPLE__)
DrainMainThreadTasks();
#endif
if (SDL_PollEvent(&loop.event) != 0) {
ProcessEvent(timer.GetTimeS());
continue;
@@ -747,9 +798,18 @@ static void WindowCreate(WindowContext& context) {
LOGF("WindowCreate(): width = %d, height = %d\n", width, height);
uint32_t window_flags = KYTY_SDL_WINDOW_FLAGS;
#if defined(__APPLE__)
// macOS 26 window chrome (CoreUI asset decode, SwiftUI titlebar) has been observed
// throwing NSExceptions under Rosetta during the first CATransaction commit. A
// borderless window skips that machinery entirely.
if (std::getenv("KYTY_BORDERLESS") != nullptr) {
window_flags |= static_cast<uint32_t>(SDL_WINDOW_BORDERLESS);
}
#endif
context.window =
SDL_CreateWindow(KYTY_SDL_WINDOW_CAPTION, KYTY_SDL_WINDOWPOS_CENTERED,
KYTY_SDL_WINDOWPOS_CENTERED, width, height, KYTY_SDL_WINDOW_FLAGS);
KYTY_SDL_WINDOWPOS_CENTERED, width, height, window_flags);
context.window_hidden = true;
@@ -896,7 +956,13 @@ void WindowContext::UpdateTitle() {
(has_app_ver ? " " : ""), device_name, processor_name,
frame_num, current_fps);
#if defined(__APPLE__)
// AppKit traps on title changes off the main thread; fire-and-forget keeps present pacing.
RunOnMainThread([this, fps = std::move(fps)] { SDL_SetWindowTitle(window, fps.c_str()); },
false);
#else
SDL_SetWindowTitle(window, fps.c_str());
#endif
}
} // namespace Libs::Graphics
@@ -12,6 +12,10 @@
#include <memory>
#include <vector>
#if defined(__APPLE__)
#include <functional>
#endif
namespace Libs::Graphics {
class Presenter;
@@ -47,6 +51,14 @@ struct WindowContext {
void ProcessEvent(double time_seconds);
void Run();
#if defined(__APPLE__)
// AppKit only allows window operations (show, icon, title, view/layer changes) on the
// main thread; the present thread marshals them through the SDL main loop with these.
// wait=true blocks until the task has run on the main thread.
void RunOnMainThread(std::function<void()> task, bool wait);
void DrainMainThreadTasks();
#endif
GraphicContext graphic_ctx;
SDL_Window* window = nullptr;
bool window_hidden = true;
@@ -60,6 +72,14 @@ struct WindowContext {
char processor_name[64] = {0};
Common::Mutex mutex;
#if defined(__APPLE__)
Common::Mutex main_task_mutex;
Common::CondVar main_task_done;
std::vector<std::function<void()>> main_tasks; // guarded by main_task_mutex
uint64_t main_tasks_queued = 0; // guarded by main_task_mutex
uint64_t main_tasks_run = 0; // guarded by main_task_mutex
#endif
};
} // namespace Libs::Graphics
+17 -1
View File
@@ -191,9 +191,15 @@ public:
SetFailure(failure_reason, FailureReason::ReserveAlignedFailed);
return 0;
}
#if !defined(__APPLE__)
// On macOS the PROT_NONE reservation is left in place: MapView's MAP_FIXED
// replaces it atomically, so the range never becomes a hole that dyld,
// Rosetta, or Metal background threads could claim (which the subsequent
// MAP_FIXED would then silently destroy).
if (!ReleaseReservedProbe(reserved, failure_reason)) {
return 0;
}
#endif
if (MapFixed(reserved, size, backing_offset, mode, failure_reason)) {
return reserved;
}
@@ -1061,7 +1067,17 @@ private:
return ptr;
}
static bool UnmapView(void* vaddr, uint64_t size) { return munmap(vaddr, size) == 0; }
static bool UnmapView(void* vaddr, uint64_t size) {
#if defined(__APPLE__)
// Re-reserve instead of freeing: the guest may MAP_FIXED-remap this range
// later, and that must never destroy a host mapping (dyld, Rosetta, Metal)
// that moved into the hole in the meantime.
return mmap(vaddr, size, PROT_NONE, MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE,
-1, 0) != MAP_FAILED;
#else
return munmap(vaddr, size) == 0;
#endif
}
static bool UnmapViewPreservePlaceholder(void* /*vaddr*/, uint64_t /*size*/) { return false; }
+30 -4
View File
@@ -58,6 +58,12 @@ namespace LibKernel {
LIB_NAME("libkernel", "libkernel");
// macOS's <pthread.h>/<limits.h> define PTHREAD_STACK_MIN as a macro; the emulator
// wants its own guest-side value with the same name, so drop the host macro here.
#ifdef PTHREAD_STACK_MIN
#undef PTHREAD_STACK_MIN
#endif
constexpr int KEYS_MAX = 256;
constexpr int DESTRUCTOR_ITERATIONS = 4;
constexpr size_t PTHREAD_STACK_DEFAULT = 0x100000;
@@ -795,9 +801,9 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
}
// The guest ABI expects the entry argument in rdi and a 16-byte aligned stack before call.
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
asm volatile("pushq %%r12\n\t"
"pushq %%r13\n\t"
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
"pushq %%r14\n\t"
"pushq %%r15\n\t"
"movq %%gs:0x08, %%r14\n\t"
@@ -805,7 +811,6 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
"xorq %%rcx, %%rcx\n\t"
"movq %%rcx, %%gs:0x08\n\t"
"movq %%rcx, %%gs:0x10\n\t"
#endif
"movq %%rsp, %%r12\n\t"
"movq %%rbp, %%r13\n\t"
"movq %[guest_rsp], %%rsp\n\t"
@@ -813,12 +818,10 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
"callq *%%rsi\n\t"
"movq %%r13, %%rbp\n\t"
"movq %%r12, %%rsp\n\t"
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
"movq %%r14, %%gs:0x08\n\t"
"movq %%r15, %%gs:0x10\n\t"
"popq %%r15\n\t"
"popq %%r14\n\t"
#endif
"popq %%r13\n\t"
"popq %%r12\n\t"
: "=a"(ret), "+D"(arg), "+S"(func)
@@ -826,6 +829,29 @@ static KYTY_SYSV_ABI void* RunOnGuestStack(void* arg, pthread_entry_func_t func,
: "cc", "memory", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11",
"xmm12", "xmm13", "xmm14", "xmm15");
#else
// Pin the stack operands to registers the template never touches (r14/r15 are
// SysV callee-saved, so guest code preserves them): plain "r" constraints may
// allocate them into r12/r13, which the template overwrites before use.
register uintptr_t guest_rsp_reg asm("r14") = guest_rsp;
register uintptr_t guest_rbp_reg asm("r15") = guest_rbp;
asm volatile("pushq %%r12\n\t"
"pushq %%r13\n\t"
"movq %%rsp, %%r12\n\t"
"movq %%rbp, %%r13\n\t"
"movq %[guest_rsp], %%rsp\n\t"
"movq %[guest_rbp], %%rbp\n\t"
"callq *%%rsi\n\t"
"movq %%r13, %%rbp\n\t"
"movq %%r12, %%rsp\n\t"
"popq %%r13\n\t"
"popq %%r12\n\t"
: "=a"(ret), "+D"(arg), "+S"(func)
: [guest_rsp] "r"(guest_rsp_reg), [guest_rbp] "r"(guest_rbp_reg)
: "cc", "memory", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11",
"xmm12", "xmm13", "xmm14", "xmm15");
#endif
g_guest_entry_return_rsp = 0;
if (g_pthread_self != nullptr) {
+7
View File
@@ -14,6 +14,13 @@
#endif
#else
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
// POSIX uses plain int file descriptors for sockets; provide the Winsock spellings
// the shared (non-guarded) code paths reference.
using SOCKET = int;
static constexpr SOCKET INVALID_SOCKET = -1;
#endif
#include "common/assert.h"
+42 -17
View File
@@ -345,28 +345,48 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu
guest_root_frame[0] = 0;
guest_root_frame[1] = 0;
asm volatile("pushq %%r12\n\t"
"pushq %%r13\n\t"
"movq %%rsp, %%r12\n\t"
"movq %%rbp, %%r13\n\t"
"movq %[guest_rsp], %%rsp\n\t"
"movq %[guest_rbp], %%rbp\n\t"
"callq *%[func]\n\t"
"movq %%r13, %%rbp\n\t"
"movq %%r12, %%rsp\n\t"
"popq %%r13\n\t"
"popq %%r12\n\t"
:
: [func] "r"(func), "D"(params),
"S"(atexit_func), [guest_rsp] "r"(guest_rsp), [guest_rbp] "r"(guest_rbp)
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0",
"xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9",
"xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15");
#if defined(__APPLE__)
// Clang on macOS can allocate plain "r" inputs to r12/r13, which the template
// clobbers before consuming them. Pin the inputs to registers the SysV guest
// preserves without changing register allocation on Windows or Linux.
register entry_func_t func_reg asm("rbx") = func;
register uintptr_t guest_rsp_reg asm("r14") = guest_rsp;
register uintptr_t guest_rbp_reg asm("r15") = guest_rbp;
#endif
asm volatile(
"pushq %%r12\n\t"
"pushq %%r13\n\t"
"movq %%rsp, %%r12\n\t"
"movq %%rbp, %%r13\n\t"
"movq %[guest_rsp], %%rsp\n\t"
"movq %[guest_rbp], %%rbp\n\t"
"callq *%[func]\n\t"
"movq %%r13, %%rbp\n\t"
"movq %%r12, %%rsp\n\t"
"popq %%r13\n\t"
"popq %%r12\n\t"
:
#if defined(__APPLE__)
: [func] "r"(func_reg), "D"(params),
"S"(atexit_func), [guest_rsp] "r"(guest_rsp_reg), [guest_rbp] "r"(guest_rbp_reg)
#else
: [func] "r"(func), "D"(params),
"S"(atexit_func), [guest_rsp] "r"(guest_rsp), [guest_rbp] "r"(guest_rbp)
#endif
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1", "xmm2",
"xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12",
"xmm13", "xmm14", "xmm15");
return;
}
uintptr_t guest_root_frame[2] = {};
#if defined(__APPLE__)
register entry_func_t func_reg asm("rbx") = func;
register uintptr_t guest_rbp_reg asm("r14") = reinterpret_cast<uintptr_t>(guest_root_frame);
#endif
asm volatile("pushq %%r12\n\t"
"pushq %%r13\n\t"
"movq %%rbp, %%r12\n\t"
@@ -376,8 +396,13 @@ static KYTY_SYSV_ABI void RunEntry(uint64_t addr, EntryParams* params, atexit_fu
"popq %%r13\n\t"
"popq %%r12\n\t"
:
#if defined(__APPLE__)
: [func] "r"(func_reg), "D"(params),
"S"(atexit_func), [guest_rbp] "r"(guest_rbp_reg)
#else
: [func] "r"(func), "D"(params),
"S"(atexit_func), [guest_rbp] "r"(guest_root_frame)
#endif
: "cc", "memory", "rax", "rcx", "rdx", "r8", "r9", "r10", "r11", "xmm0", "xmm1",
"xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7", "xmm8", "xmm9", "xmm10", "xmm11",
"xmm12", "xmm13", "xmm14", "xmm15");