feature: add apple silicon native macOS support (#81)

* feature: add apple silicon native macOS support - #81

* (macos): Fix crash

This fixes a crash when viewing the rear camera

* fix(macos): keep interpolated presentation on main thread

* fix(macos): supply Retro-WFC payload during setup

* perf(windows): compile out flat-memory fallback check

* remove duplicate smoke test

* test(macos): name and focus host platform tests

* fix(macos): validate Retro-WFC payload cache

* fix(payload): preserve staged file access failures

* Limit flat-page checks to variable-page hosts

---------

Co-authored-by: patchzyy <64382339+patchzyy@users.noreply.github.com>
This commit is contained in:
Michael G
2026-09-01 12:57:15 -04:00
committed by GitHub
parent ae3096c89b
commit 5c76e2b0df
36 changed files with 1742 additions and 251 deletions
+5 -10
View File
@@ -6,6 +6,7 @@
#include <mutex>
#include <thread>
#include <unordered_map>
#include <vector>
#if defined(_WIN32)
#ifndef NOMINMAX
@@ -22,7 +23,7 @@
// Forward declarations
struct CpuContext;
// GuestFiberManager: each guest OSThread maps to a Windows Fiber. A scheduler fiber picks
// GuestFiberManager: each guest OSThread maps to a host context. A scheduler context picks
// which guest fiber runs; a real timer thread queues VI retraces at the VI cadence. Guest
// threads only switch at explicit yield points (OSSleepThread, OSYieldThread, ...), matching
// Wii cooperative semantics exactly.
@@ -39,7 +40,7 @@ enum class ThreadState : uint32_t {
// Information about a guest fiber
struct GuestFiber {
void* fiber = nullptr; // Windows fiber handle
void* fiber = nullptr; // Host context handle
uint32_t entryPoint = 0; // Thread entry function
uint32_t entryArg = 0; // Argument to entry function
CpuContext cpuContext{}; // Saved CPU context for this fiber
@@ -102,10 +103,6 @@ private:
static void CALLBACK FiberProc(void* param);
#else
static void FiberProc(void* param);
// libco's co_create() entry points take no argument (unlike CreateFiber's FiberProc(void*)),
// so this trampoline reads the guest thread address staged by CreateGuestFiber() and forwards
// into the (platform-neutral-bodied) FiberProc above. See fiber_manager.cpp.
static void FiberProcTrampoline();
#endif
// Switch from whichever fiber is currently active straight to the scheduler fiber, without
// the SwitchToThread bookkeeping (CPU context save/restore, s_currentGuestThread). Used for
@@ -117,9 +114,8 @@ private:
static std::mutex s_mutex;
static std::unordered_map<uint32_t, GuestFiber> s_fibers;
static std::vector<void*> s_fibersPendingDelete;
// The scheduler's own "fiber": a Windows HFIBER, or (non-Windows) libco's cothread_t for
// whichever native call stack first called GuestFiberManager::Initialize() - both are
// plain void* handles, so one field serves both platforms.
// The scheduler's own host context. Its opaque handle is supplied by the
// active HostContext backend, so one field serves every supported host.
static void* s_schedulerFiber;
static uint32_t s_currentGuestThread;
static bool s_initialized;
@@ -135,4 +131,3 @@ private:
extern std::atomic<uint32_t> g_viRetracePendingCount;
} // namespace Fiber
+26
View File
@@ -14,10 +14,17 @@ namespace GuestFlat {
// Fixed base so the emitted access is `[reg + imm64-in-register]` with no load
// of a global.
inline constexpr uint64_t kGuestSpaceSize = 0x1'0000'0000ull;
inline constexpr size_t kGuestPageSize = 0x1000;
#if defined(__x86_64__)
// 16 TiB: clear of the Windows ASan shadow (32 TiB) and of the usual image/heap
// placement.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'1000'0000'0000ull;
#elif defined(__aarch64__) && defined(__APPLE__)
// Keep this well above the low address ranges that Darwin's ASLR may use for
// a PIE executable and its shared cache. Apple Silicon's user VA is wider
// than Linux's 39-bit minimum, so this 512 GiB region is available while the
// Linux AArch64 target retains its 64 GiB placement below.
inline constexpr uintptr_t kFixedFlatGuestBase = 0x0000'0080'0000'0000ull;
#elif defined(__aarch64__)
// 16 TiB (this arch's x86_64 sibling value) is unreachable on any AArch64
// kernel configured for 39-bit virtual addresses (512 GiB ceiling) - common on
@@ -58,6 +65,25 @@ struct FaultCounters {
// True once the reservation exists and translated code may use the flat path.
bool IsActive();
// True when a host VM page covers more than one 4 KiB Wii page. In that
// configuration, guest-view page protection cannot safely represent per-Wii-
// page MMIO, deferred-read, or executable-write state, so general translated
// accesses must use the checked Memory::* path.
// Windows user mode and x86-64 always use a 4 KiB base page, so those builds
// fold this to a compile-time false: it appears in every flat access and must
// not become a hot-path load. Only AArch64, where the page size is a kernel
// configuration (4/16/64 KiB), has to probe it at runtime.
#if defined(_WIN32) || defined(__x86_64__)
#define MKW_GUEST_FLAT_FIXED_PAGE_SIZE 1
#endif
#if defined(MKW_GUEST_FLAT_FIXED_PAGE_SIZE)
inline constexpr bool RequiresCheckedAccess() noexcept { return false; }
#else
extern bool g_requiresCheckedAccess;
inline bool RequiresCheckedAccess() noexcept { return g_requiresCheckedAccess; }
#endif
// Reserves the 4 GiB space (once per process) and maps every requested region
// into both views. Throws std::runtime_error with a precise diagnosis when the
// reservation, the section objects or a view cannot be created - a silent
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstddef>
// HostContext is the deliberately small boundary between the guest scheduler
// and the host's cooperative-context facility. Windows uses native Fibers and
// Linux uses libco; macOS AArch64 uses the local assembly backend because it
// must preserve Darwin's platform-reserved x18 register, which libco's AArch64
// backend does not save. Its handles are only valid on the thread that
// initialized the scheduler.
namespace HostContext {
using Handle = void*;
using Entry = void (*)(void*);
bool InitializeScheduler(Handle* scheduler);
void ShutdownScheduler(Handle scheduler);
Handle Create(std::size_t stackSize, Entry entry, void* argument);
void Destroy(Handle context);
bool IsCurrent(Handle context);
void Switch(Handle target);
} // namespace HostContext
+4
View File
@@ -264,6 +264,8 @@ inline void PpcWritePairPsqInline(uint32_t addr, T first, T second)
// reading stale bytes, and unmapped pages commit on demand, same as MemoryInline::Flat* loads.
MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr)
{
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
return MKW_FLAT_GUEST_BASE + addr;
}
@@ -274,6 +276,8 @@ MKW_PPC_FORCE_INLINE const uint8_t* PpcTryGetPsqReadableHostInline(uint32_t addr
// executable, and unmapped pages still trap.
MKW_PPC_FORCE_INLINE uint8_t* PpcTryGetPsqWritableHostInline(uint32_t addr)
{
if (GuestFlat::RequiresCheckedAccess()) [[unlikely]]
return nullptr;
if (addr > UINT32_MAX - 7u) [[unlikely]]
return nullptr;
if (MemoryInline::FlatWriteNeedsPolicy(addr) ||
+34 -5
View File
@@ -231,6 +231,13 @@ MKW_MEMORY_FORCE_INLINE uint8_t* ResolveRangeHost(uint32_t base, int32_t minOffs
(void)needsRead;
const uint32_t guestStart = base + static_cast<uint32_t>(minOffset);
if (length == 0 || length > kPageSize || guestStart > UINT32_MAX - (length - 1)) return nullptr;
if (GuestFlat::RequiresCheckedAccess()) {
// A host page can cover multiple independently-special Wii pages.
// Returning null keeps resolved accesses on the checked Memory::*
// path, which materializes deferred reads and applies write policy.
(void)needsWrite;
return nullptr;
}
if (needsWrite &&
(FlatWriteNeedsPolicy(guestStart) || FlatWriteNeedsPolicy(guestStart + (length - 1))))
[[unlikely]] return nullptr;
@@ -522,6 +529,13 @@ MKW_MEMORY_FORCE_INLINE void WriteResolvedFloat64(uint8_t* r, uint32_t o, uint32
// around `*(T*)(base + addr)`, no page-table load or limit check (interception model documented
// in guest_flat_memory.h). The one exception kept inline is the MMIO write policy, since the
// written value can't be recovered from a fault record.
//
// When a host VM page is larger than a 4 KiB Wii page, guest-view protections
// cannot distinguish adjacent special Wii pages. The general FlatRead*/
// FlatWrite* helpers then use the checked page-table path, which materializes
// deferred reads and applies executable-write/MMIO policy before touching RAM.
// FlatWriteRam* remains direct because the translator emits it only for
// addresses it has proven are ordinary RAM.
template <typename T>
MKW_MEMORY_FORCE_INLINE T FlatLoad(uint32_t address) {
@@ -536,47 +550,62 @@ MKW_MEMORY_FORCE_INLINE void FlatStore(uint32_t address, T value) {
std::memcpy(MKW_FLAT_GUEST_BASE + address, &swapped, sizeof(T));
}
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) { return FlatLoad<uint8_t>(address); }
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) { return FlatLoad<uint16_t>(address); }
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) { return FlatLoad<uint32_t>(address); }
MKW_MEMORY_FORCE_INLINE uint8_t FlatRead8(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read8(address);
return FlatLoad<uint8_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint16_t FlatRead16(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read16(address);
return FlatLoad<uint16_t>(address);
}
MKW_MEMORY_FORCE_INLINE uint32_t FlatRead32(uint32_t address) {
if (GuestFlat::RequiresCheckedAccess()) return Memory::Read32(address);
return FlatLoad<uint32_t>(address);
}
MKW_MEMORY_FORCE_INLINE float FlatReadFloat32(uint32_t address) {
const uint32_t bits = FlatLoad<uint32_t>(address);
const uint32_t bits = FlatRead32(address);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE double FlatReadFloat64(uint32_t address) {
const uint64_t bits = FlatLoad<uint64_t>(address);
const uint64_t bits = GuestFlat::RequiresCheckedAccess()
? Memory::Read64(address) : FlatLoad<uint64_t>(address);
double value = 0.0;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
MKW_MEMORY_FORCE_INLINE void FlatWrite8(uint32_t address, uint8_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write8(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write8Slow(address, value); return; }
FlatStore<uint8_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWrite16(uint32_t address, uint16_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write16(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write16Slow(address, value); return; }
FlatStore<uint16_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWrite32(uint32_t address, uint32_t value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::Write32(address, value); return; }
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { Write32Slow(address, value); return; }
FlatStore<uint32_t>(address, value);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat32(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat32(address, value); return; }
const uint32_t bits = ConvertPpcDoubleToSingleBits(value);
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat32Slow(address, value); return; }
FlatStore<uint32_t>(address, bits);
}
MKW_MEMORY_FORCE_INLINE void FlatWriteFloat64(uint32_t address, double value) {
if (GuestFlat::RequiresCheckedAccess()) { Memory::WriteFloat64(address, value); return; }
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
if (FlatWriteNeedsPolicy(address)) [[unlikely]] { WriteFloat64Slow(address, value); return; }
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string_view>
// Small host-services boundary for functionality that must not leak Win32
// assumptions into runtime or game code. Guest execution, virtual memory, and
// cooperative contexts remain outside this layer until dedicated macOS
// prototypes establish a safe abstraction.
namespace RuntimePlatform {
std::optional<std::filesystem::path> ExecutableDirectory() noexcept;
// Returns the platform's conventional per-user application-data directory.
// It does not create the directory, leaving that policy to the caller.
std::filesystem::path ApplicationDataDirectory(std::string_view applicationName);
// The root for per-run diagnostics. Keeping this here ensures log placement
// follows the same host convention as configuration and other user data.
std::filesystem::path LogDirectory(std::string_view applicationName);
uint64_t CurrentProcessId() noexcept;
} // namespace RuntimePlatform
+12
View File
@@ -17,6 +17,7 @@
#include <utility>
#include <vector>
#include <toml.hpp>
#include "platform/host_platform.h"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
@@ -141,7 +142,14 @@ inline bool IsSupportedResolutionMultiplier(float value) {
// Must stay in step with the backend table in main.cpp, which is what actually
// maps these to AuroraBackend.
inline bool IsSupportedGraphicsApi(std::string_view value) {
#if defined(__APPLE__)
static constexpr std::array<std::string_view, 2> values{"auto", "metal"};
// only vulkan for linux
#elif defined(__linux__)
static constexpr std::array<std::string_view, 2> values{"auto", "vulkan"};
#elif defined(_WIN32)
static constexpr std::array<std::string_view, 3> values{"auto", "d3d12", "vulkan"};
#endif
return std::find(values.begin(), values.end(), value) != values.end();
}
@@ -172,6 +180,8 @@ inline std::optional<std::filesystem::path> ExecutableDirectory() {
}
buffer.resize(buffer.size() * 2);
}
#elif defined(__APPLE__)
return RuntimePlatform::ExecutableDirectory();
#else
// /proc/self/exe is a Linux-specific magic symlink to the running executable; readlink()
// does not NUL-terminate and silently truncates if the buffer is too small, so this grows
@@ -229,6 +239,8 @@ inline std::filesystem::path ApplicationDataDirectory() {
CoTaskMemFree(rawPath);
return directory;
}
#elif defined(__APPLE__)
return RuntimePlatform::ApplicationDataDirectory(kApplicationDirectoryName);
#else
// XDG Base Directory spec equivalent of FOLDERID_LocalAppData: $XDG_DATA_HOME if set and
// non-empty, otherwise its default of $HOME/.local/share.
+1
View File
@@ -36,6 +36,7 @@ extern thread_local uint32_t g_sehLastAccessType;
void WriteFatalLog(std::string_view reason);
void SetRuntimeExitCode(int code);
void MarkFatalErrorReported();
// Centralized crash reporting (defined in main.cpp). Every fatal path funnels
// through these so the per-run log folder always receives the same artifact