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
+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