// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "common/Assertions.h" #include "common/BitUtils.h" #include "common/Console.h" #include "common/CrashHandler.h" #include "common/Darwin/DarwinMisc.h" #include "common/Error.h" #include "common/Pcsx2Types.h" #include "common/Threading.h" #include "common/WindowInfo.h" #include "common/HostSys.h" #include "fmt/format.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // Darwin (OSX) is a bit different from Linux when requesting properties of // the OS because of its BSD/Mach heritage. Helpfully, most of this code // should translate pretty well to other *BSD systems. (e.g.: the sysctl(3) // interface). // // For an overview of all of Darwin's sysctls, check: // https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man3/sysctl.3.html // Return the total physical memory on the machine, in bytes. Returns 0 on // failure (not supported by the operating system). u64 GetPhysicalMemory() { u64 getmem = 0; size_t len = sizeof(getmem); int mib[] = {CTL_HW, HW_MEMSIZE}; if (sysctl(mib, std::size(mib), &getmem, &len, NULL, 0) < 0) perror("sysctl:"); return getmem; } u64 GetAvailablePhysicalMemory() { const mach_port_t host_port = mach_host_self(); vm_size_t page_size; // Get the system's page size. if (host_page_size(host_port, &page_size) != KERN_SUCCESS) return 0; vm_statistics64_data_t vm_stat; mach_msg_type_number_t host_size = sizeof(vm_statistics64_data_t) / sizeof(integer_t); // Get system memory statistics. if (host_statistics64(host_port, HOST_VM_INFO, reinterpret_cast(&vm_stat), &host_size) != KERN_SUCCESS) return 0; // Get the number of free and inactive pages. const u64 free_pages = static_cast(vm_stat.free_count); const u64 inactive_pages = static_cast(vm_stat.inactive_count); // Calculate available memory. const u64 get_available_mem = (free_pages + inactive_pages) * page_size; return get_available_mem; } static mach_timebase_info_data_t s_timebase_info; static const u64 tickfreq = []() { if (mach_timebase_info(&s_timebase_info) != KERN_SUCCESS) abort(); return (u64)1e9 * (u64)s_timebase_info.denom / (u64)s_timebase_info.numer; }(); // returns the performance-counter frequency: ticks per second (Hz) // // usage: // u64 seconds_passed = GetCPUTicks() / GetTickFrequency(); // u64 millis_passed = (GetCPUTicks() * 1000) / GetTickFrequency(); // // NOTE: multiply, subtract, ... your ticks before dividing by // GetTickFrequency() to maintain good precision. u64 GetTickFrequency() { return tickfreq; } // return the number of "ticks" since some arbitrary, fixed time in the // past. On OSX x86(-64), this is actually the number of nanoseconds passed, // because mach_timebase_info.numer == denom == 1. So "ticks" == // nanoseconds. u64 GetCPUTicks() { return mach_absolute_time(); } static std::string sysctl_str(int category, int name) { char buf[32]; size_t len = sizeof(buf); int mib[] = {category, name}; sysctl(mib, std::size(mib), buf, &len, nullptr, 0); return std::string(buf, len > 0 ? len - 1 : 0); } template static std::optional sysctlbyname_T(const char* name) { T output = 0; size_t output_size = sizeof(output); if (sysctlbyname(name, &output, &output_size, nullptr, 0) != 0) return std::nullopt; if (output_size != sizeof(output)) { ERROR_LOG("(DarwinMisc) sysctl {} gave unexpected size {}", name, output_size); return std::nullopt; } return output; } std::string GetOSVersionString() { std::string type = sysctl_str(CTL_KERN, KERN_OSTYPE); std::string release = sysctl_str(CTL_KERN, KERN_OSRELEASE); std::string arch = sysctl_str(CTL_HW, HW_MACHINE); return type + " " + release + " " + arch; } static IOPMAssertionID s_pm_assertion; bool Common::InhibitScreensaver(bool inhibit) { if (s_pm_assertion) { IOPMAssertionRelease(s_pm_assertion); s_pm_assertion = 0; } if (inhibit) IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, CFSTR("Playing a game"), &s_pm_assertion); return true; } void Common::SetMousePosition(int x, int y) { // Little bit ugly but; // Creating mouse move events and posting them wasn't very reliable. // Calling CGWarpMouseCursorPosition without CGAssociateMouseAndMouseCursorPosition(false) // ends up with the cursor feeling "sticky". CGAssociateMouseAndMouseCursorPosition(false); CGWarpMouseCursorPosition(CGPointMake(x, y)); CGAssociateMouseAndMouseCursorPosition(true); // The default state return; } CFMachPortRef mouseEventTap = nullptr; CFRunLoopSourceRef mouseRunLoopSource = nullptr; static std::function fnMouseMoveCb; CGEventRef mouseMoveCallback(CGEventTapProxy, CGEventType type, CGEventRef event, void* arg) { if (type == kCGEventMouseMoved) { const CGPoint location = CGEventGetLocation(event); fnMouseMoveCb(location.x, location.y); } return event; } bool Common::AttachMousePositionCb(std::function cb) { if (!AXIsProcessTrusted()) { Console.Warning("Process isn't trusted with accessibility permissions. Mouse tracking will not work!"); } fnMouseMoveCb = cb; mouseEventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, CGEventMaskBit(kCGEventMouseMoved), mouseMoveCallback, nullptr); if (!mouseEventTap) { Console.Warning("Unable to create mouse moved event tap. Mouse tracking will not work!"); return false; } mouseRunLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, mouseEventTap, 0); CFRunLoopAddSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopCommonModes); return true; } void Common::DetachMousePositionCb() { if (mouseRunLoopSource) { CFRunLoopRemoveSource(CFRunLoopGetCurrent(), mouseRunLoopSource, kCFRunLoopCommonModes); CFRelease(mouseRunLoopSource); } if (mouseEventTap) { CFRelease(mouseEventTap); } mouseRunLoopSource = nullptr; mouseEventTap = nullptr; } void Threading::Sleep(int ms) { usleep(1000 * ms); } void Threading::SleepUntil(u64 ticks) { // This is definitely sub-optimal, but apparently clock_nanosleep() doesn't exist. const s64 diff = static_cast(ticks - GetCPUTicks()); if (diff <= 0) return; const u64 nanos = (static_cast(diff) * static_cast(s_timebase_info.denom)) / static_cast(s_timebase_info.numer); if (nanos == 0) return; struct timespec ts; ts.tv_sec = nanos / 1000000000ULL; ts.tv_nsec = nanos % 1000000000ULL; nanosleep(&ts, nullptr); } std::vector DarwinMisc::GetCPUClasses() { std::vector out; if (std::optional nperflevels = sysctlbyname_T("hw.nperflevels")) { char name[64]; for (u32 i = 0; i < *nperflevels; i++) { snprintf(name, sizeof(name), "hw.perflevel%u.physicalcpu", i); std::optional physicalcpu = sysctlbyname_T(name); snprintf(name, sizeof(name), "hw.perflevel%u.logicalcpu", i); std::optional logicalcpu = sysctlbyname_T(name); char levelname[64]; size_t levelname_size = sizeof(levelname); snprintf(name, sizeof(name), "hw.perflevel%u.name", i); if (0 != sysctlbyname(name, levelname, &levelname_size, nullptr, 0)) strcpy(levelname, "???"); if (!physicalcpu.has_value() || !logicalcpu.has_value()) { Console.Warning("(DarwinMisc) Perf level %u is missing data on %s cpus!", i, !physicalcpu.has_value() ? "physical" : "logical"); continue; } out.push_back({levelname, *physicalcpu, *logicalcpu}); } } else if (std::optional physcpu = sysctlbyname_T("hw.physicalcpu")) { out.push_back({"Default", *physcpu, sysctlbyname_T("hw.logicalcpu").value_or(*physcpu)}); } else { Console.Warning("(DarwinMisc) Couldn't get cpu core count!"); } return out; } static CPUInfo CalcCPUInfo() { CPUInfo out; char name[256]; size_t name_size = sizeof(name); if (0 != sysctlbyname("machdep.cpu.brand_string", name, &name_size, nullptr, 0)) strcpy(name, "Unknown"); out.name = name; if (sysctlbyname_T("sysctl.proc_translated").value_or(0)) out.name += " (Rosetta)"; std::vector classes = DarwinMisc::GetCPUClasses(); out.num_clusters = static_cast(classes.size()); out.num_big_cores = classes.empty() ? 0 : classes[0].num_physical; out.num_threads = classes.empty() ? 0 : classes[0].num_logical; out.num_small_cores = 0; for (std::size_t i = 1; i < classes.size(); i++) { out.num_small_cores += classes[i].num_physical; out.num_threads += classes[i].num_logical; } return out; } const CPUInfo& GetCPUInfo() { static const CPUInfo info = CalcCPUInfo(); return info; } size_t HostSys::GetRuntimePageSize() { return sysctlbyname_T("hw.pagesize").value_or(0); } size_t HostSys::GetRuntimeCacheLineSize() { return static_cast(std::max(sysctlbyname_T("hw.cachelinesize").value_or(0), 0)); } void* HostSys::CreateMappingFromFile(FILE* file) { return reinterpret_cast(static_cast(fileno(file))); } void* HostSys::MapMapping(void* handle, size_t size, const PageProtectionMode& mode) { const u32 mmap_prot = (mode.CanWrite() ? (PROT_READ | PROT_WRITE) : (PROT_READ)) | (mode.CanExecute() ? PROT_EXEC : 0); return mmap(nullptr, size, mmap_prot, MAP_PRIVATE, static_cast(reinterpret_cast(handle)), 0); } void HostSys::DestroyMapping(void* handle) { // The handle mmap requires is the same as the file descriptor. return; } void HostSys::FlushMapping(void* handle, [[maybe_unused]] void* baseAddr, size_t size) { msync(handle, size, MS_SYNC); } #ifdef ARCH_ARM64 static thread_local int s_code_write_depth = 0; void HostSys::BeginCodeWrite() { if ((s_code_write_depth++) == 0) pthread_jit_write_protect_np(0); } void HostSys::EndCodeWrite() { pxAssert(s_code_write_depth > 0); if ((--s_code_write_depth) == 0) pthread_jit_write_protect_np(1); } [[maybe_unused]] static bool IsStoreInstruction(const void* ptr) { u32 bits; std::memcpy(&bits, ptr, sizeof(bits)); // Based on vixl's disassembler Instruction::IsStore(). // if (Mask(LoadStoreAnyFMask) != LoadStoreAnyFixed) if ((bits & 0x0a000000) != 0x08000000) return false; // if (Mask(LoadStorePairAnyFMask) == LoadStorePairAnyFixed) if ((bits & 0x3a000000) == 0x28000000) { // return Mask(LoadStorePairLBit) == 0 return (bits & (1 << 22)) == 0; } switch (bits & 0xC4C00000) { case 0x00000000: // STRB_w case 0x40000000: // STRH_w case 0x80000000: // STR_w case 0xC0000000: // STR_x case 0x04000000: // STR_b case 0x44000000: // STR_h case 0x84000000: // STR_s case 0xC4000000: // STR_d case 0x04800000: // STR_q return true; default: return false; } } #endif // ARCH_ARM64 #define USE_MACH_EXCEPTION_PORTS namespace PageFaultHandler { #ifdef USE_MACH_EXCEPTION_PORTS static void SignalHandler(mach_port_t port); static mach_port_t s_port = 0; #else static void SignalHandler(int sig, siginfo_t* info, void* ctx); #endif static std::recursive_mutex s_exception_handler_mutex; static bool s_in_exception_handler = false; static bool s_installed = false; } // namespace PageFaultHandler #ifdef USE_MACH_EXCEPTION_PORTS #if defined(ARCH_X86) #define THREAD_STATE64_COUNT x86_THREAD_STATE64_COUNT #define THREAD_STATE64 x86_THREAD_STATE64 #define thread_state64_t x86_thread_state64_t #elif defined(ARCH_ARM64) #define THREAD_STATE64_COUNT ARM_THREAD_STATE64_COUNT #define THREAD_STATE64 ARM_THREAD_STATE64 #define thread_state64_t arm_thread_state64_t #else #error Unknown Darwin Platform #endif void PageFaultHandler::SignalHandler(mach_port_t port) { Threading::SetNameOfCurrentThread("Mach Exception Thread"); #pragma pack(4) struct { mach_msg_header_t Head; NDR_record_t NDR; exception_type_t exception; mach_msg_type_number_t codeCnt; int64_t code[2]; int flavor; mach_msg_type_number_t old_stateCnt; natural_t old_state[THREAD_STATE64_COUNT]; mach_msg_trailer_t trailer; } msg_in; struct { mach_msg_header_t Head; NDR_record_t NDR; kern_return_t RetCode; int flavor; mach_msg_type_number_t new_stateCnt; natural_t new_state[THREAD_STATE64_COUNT]; } msg_out; #pragma pack() memset(&msg_in, 0xee, sizeof(msg_in)); memset(&msg_out, 0xee, sizeof(msg_out)); mach_msg_size_t send_size = 0; mach_msg_option_t option = MACH_RCV_MSG; while (true) { kern_return_t r; if ((r = mach_msg_overwrite(&msg_out.Head, option, send_size, sizeof(msg_in), port, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL, &msg_in.Head, 0))) { pxFail(fmt::format("CRITICAL: mach_msg_overwrite: {:x}", r).c_str()); } if (msg_in.Head.msgh_id == MACH_NOTIFY_NO_SENDERS) { // the other thread exited mach_port_deallocate(mach_task_self(), port); return; } if (msg_in.Head.msgh_id != 2406) { pxFailRel("unknown message received"); return; } if (msg_in.flavor != THREAD_STATE64) { pxFailRel(fmt::format("unknown flavour {}, expected {}", msg_in.flavor, THREAD_STATE64).c_str()); return; } thread_state64_t* state = (thread_state64_t*)msg_in.old_state; HandlerResult result = HandlerResult::ExecuteNextHandler; if (!s_in_exception_handler) { s_in_exception_handler = true; #ifdef ARCH_ARM64 result = HandlePageFault(reinterpret_cast(state->__pc), reinterpret_cast(msg_in.code[1]), (msg_in.code[0] & 2) != 0); #else result = HandlePageFault(reinterpret_cast(state->__rip), reinterpret_cast(msg_in.code[1]), (msg_in.code[0] & 2) != 0); #endif s_in_exception_handler = false; } // Set up the reply. msg_out.Head.msgh_bits = MACH_MSGH_BITS(MACH_MSGH_BITS_REMOTE(msg_in.Head.msgh_bits), 0); msg_out.Head.msgh_remote_port = msg_in.Head.msgh_remote_port; msg_out.Head.msgh_local_port = MACH_PORT_NULL; msg_out.Head.msgh_id = msg_in.Head.msgh_id + 100; msg_out.NDR = msg_in.NDR; if (result != HandlerResult::ContinueExecution) // cooked { // Continue to the next exception handler (debugger or crash) msg_out.RetCode = KERN_FAILURE; msg_out.flavor = 0; msg_out.new_stateCnt = 0; } else { // Resumes execution right where we left off (re-executes instruction that caused the SIGSEGV) msg_out.RetCode = KERN_SUCCESS; msg_out.flavor = THREAD_STATE64; msg_out.new_stateCnt = THREAD_STATE64_COUNT; memcpy(msg_out.new_state, msg_in.old_state, THREAD_STATE64_COUNT * sizeof(natural_t)); } msg_out.Head.msgh_size = offsetof(__typeof__(msg_out), new_state) + msg_out.new_stateCnt * sizeof(natural_t); send_size = msg_out.Head.msgh_size; option |= MACH_SEND_MSG; } } bool PageFaultHandler::Install(Error* error) { exception_mask_t masks[EXC_TYPES_COUNT]; mach_port_t ports[EXC_TYPES_COUNT]; exception_behavior_t behaviors[EXC_TYPES_COUNT]; thread_state_flavor_t flavors[EXC_TYPES_COUNT]; mach_msg_type_number_t count = EXC_TYPES_COUNT; kern_return_t r = task_get_exception_ports(mach_task_self(), EXC_MASK_ALL, masks, &count, ports, behaviors, flavors); mach_port_t port; if ((r = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port))) { pxFailRel(fmt::format("mach_port_allocate: {:x}", r).c_str()); return false; } std::thread sig_thread(PageFaultHandler::SignalHandler, port); sig_thread.detach(); if ((r = mach_port_insert_right(mach_task_self(), port, port, MACH_MSG_TYPE_MAKE_SEND))) { mach_port_deallocate(mach_task_self(), port); pxFailRel(fmt::format("mach_port_insert_right: {:x}", r).c_str()); return false; } task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, THREAD_STATE_NONE); if ((r = thread_set_exception_ports(mach_thread_self(), EXC_MASK_BAD_ACCESS, port, EXCEPTION_STATE | MACH_EXCEPTION_CODES, THREAD_STATE64))) { mach_port_deallocate(mach_task_self(), port); pxFailRel(fmt::format("thread_set_exception_ports: {:x}", r).c_str()); return false; } mach_port_t previous; if ((r = mach_port_request_notification(mach_task_self(), port, MACH_NOTIFY_NO_SENDERS, 0, port, MACH_MSG_TYPE_MAKE_SEND_ONCE, &previous))) { mach_port_deallocate(mach_task_self(), port); pxFailRel(fmt::format("mach_port_request_notification: {:x}", r).c_str()); return false; } s_installed = true; s_port = port; return true; } bool PageFaultHandler::InstallSecondaryThread() { kern_return_t r = thread_set_exception_ports(mach_thread_self(), EXC_MASK_BAD_ACCESS, s_port, EXCEPTION_STATE | MACH_EXCEPTION_CODES, THREAD_STATE64); if (r) { pxFailRel(fmt::format("thread_set_exception_ports(secondary): {:x}", r).c_str()); return false; } return true; } #else void PageFaultHandler::SignalHandler(int sig, siginfo_t* info, void* ctx) { #if defined(ARCH_X86) void* const exception_address = reinterpret_cast(static_cast(ctx)->uc_mcontext->__es.__faultvaddr); void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext->__ss.__rip); const bool is_write = (static_cast(ctx)->uc_mcontext->__es.__err & 2) != 0; #elif defined(ARCH_ARM64) void* const exception_address = reinterpret_cast(static_cast(ctx)->uc_mcontext->__es.__far); void* const exception_pc = reinterpret_cast(static_cast(ctx)->uc_mcontext->__ss.__pc); const bool is_write = IsStoreInstruction(exception_pc); #endif // Executing the handler concurrently from multiple threads wouldn't go down well. s_exception_handler_mutex.lock(); // Prevent recursive exception filtering. HandlerResult result = HandlerResult::ExecuteNextHandler; if (!s_in_exception_handler) { s_in_exception_handler = true; result = HandlePageFault(exception_pc, exception_address, is_write); s_in_exception_handler = false; } s_exception_handler_mutex.unlock(); // Resumes execution right where we left off (re-executes instruction that caused the SIGSEGV). if (result == HandlerResult::ContinueExecution) return; // We couldn't handle it. Pass it off to the crash dumper. CrashHandler::CrashSignalHandler(sig, info, ctx); } bool PageFaultHandler::Install(Error* error) { std::unique_lock lock(s_exception_handler_mutex); pxAssertRel(!s_installed, "Page fault handler has already been installed."); struct sigaction sa; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = SignalHandler; // MacOS uses SIGBUS for memory permission violations, as well as SIGSEGV on ARM64. if (sigaction(SIGBUS, &sa, nullptr) != 0) { Error::SetErrno(error, "sigaction() for SIGBUS failed: ", errno); return false; } #ifdef ARCH_ARM64 if (sigaction(SIGSEGV, &sa, nullptr) != 0) { Error::SetErrno(error, "sigaction() for SIGSEGV failed: ", errno); return false; } #endif // Allow us to ignore faults when running under lldb. task_set_exception_ports(mach_task_self(), EXC_MASK_BAD_ACCESS, MACH_PORT_NULL, EXCEPTION_DEFAULT, 0); s_installed = true; return true; } bool PageFaultHandler::InstallSecondaryThread() { return true; } #endif