diff --git a/BuildMacOSUniversalBinary.py b/BuildMacOSUniversalBinary.py new file mode 100755 index 0000000000..ea6f83d077 --- /dev/null +++ b/BuildMacOSUniversalBinary.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +""" +The current tooling supported in CMake, Homebrew, and Qt5 are insufficient for +creating macOS universal binaries automatically for applications like Dolphin +which have more complicated build requirements (like different libraries, build +flags and source files for each target architecture). + +So instead, this script manages the configuration and compilation of distinct +builds and project files for each target architecture and then merges the two +binaries into a single universal binary. + +Running this script will: +1) Generate Xcode project files for the ARM build (if project files don't + already exist) +2) Generate Xcode project files for the x64 build (if project files don't + already exist) +3) Build the ARM project for the selected build_target +4) Build the x64 project for the selected build_target +5) Generate universal .app packages combining the ARM and x64 packages +6) Use the lipo tool to combine the binary objects inside each of the + packages into universal binaries +7) Code sign the final universal binaries using the specified + codesign_identity +""" + +import argparse +import filecmp +import glob +import json +import multiprocessing +import os +import shutil +import subprocess + +# The config variables listed below are the defaults, but they can be +# overridden by command line arguments see parse_args(), or run: +# BuildMacOSUniversalBinary.py --help +DEFAULT_CONFIG = { + + # Location of destination universal binary + "dst_app": "universal/", + # Build Target (dolphin-emu to just build the emulator and skip the tests) + "build_target": "ALL_BUILD", + + # Location for CMake to search for files (default is for homebrew) + "arm64_cmake_prefix": "/opt/homebrew", + "x86_64_cmake_prefix": "/usr/local", + + # Locations to qt5 directories for arm and x64 libraries + # The default values of these paths are taken from the default + # paths used for homebrew + "arm64_qt5_path": "/opt/homebrew/opt/qt5", + "x86_64_qt5_path": "/usr/local/opt/qt5", + + # Identity to use for code signing. "-" indicates that the app will not + # be cryptographically signed/notarized but will instead just use a + # SHA checksum to verify the integrity of the app. This doesn't + # protect against malicious actors, but it does protect against + # running corrupted binaries and allows for access to the extended + # permisions needed for ARM builds + "codesign_identity": "-", + # Entitlements file to use for code signing + "entitlements": "../Source/Core/DolphinQt/DolphinEmu.entitlements", + + # Minimum macOS version for each architecture slice + "arm64_mac_os_deployment_target": "11.0.0", + "x86_64_mac_os_deployment_target": "10.12.0", + + # CMake Generator to use for building + "generator": "Unix Makefiles", + "build_type": "Release", + +} + +# Architectures to build for. This is explicity left out of the command line +# config options for several reasons: +# 1) Adding new architectures will generally require more code changes +# 2) Single architecture builds should utilize the normal generated cmake +# project files rather than this wrapper script + +ARCHITECTURES = ["x86_64", "arm64"] + + +def parse_args(conf=DEFAULT_CONFIG): + """ + Parses the command line arguments into a config dictionary. + """ + + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument( + "--target", + help="Build target in generated project files", + default=conf["build_target"], + dest="build_target") + parser.add_argument( + "-G", + help="CMake Generator to use for creating project files", + default=conf["generator"], + dest="generator") + parser.add_argument( + "--build_type", + help="CMake build type [Debug, Release, RelWithDebInfo, MinSizeRel]", + default=conf["build_type"], + dest="build_type") + parser.add_argument( + "--dst_app", + help="Directory where universal binary will be stored", + default=conf["dst_app"]) + + parser.add_argument( + "--entitlements", + help="Path to .entitlements file for code signing", + default=conf["entitlements"]) + + parser.add_argument( + "--codesign", + help="Code signing identity to use to sign the applications", + default=conf["codesign_identity"], + dest="codesign_identity") + + for arch in ARCHITECTURES: + parser.add_argument( + f"--{arch}_cmake_prefix", + help="Folder for cmake to search for packages", + default=conf[arch+"_cmake_prefix"], + dest=arch+"_cmake_prefix") + + parser.add_argument( + f"--{arch}_qt5_path", + help=f"Install path for {arch} qt5 libraries", + default=conf[arch+"_qt5_path"]) + + parser.add_argument( + f"--{arch}_mac_os_deployment_target", + help=f"Deployment architecture for {arch} slice", + default=conf[arch+"_mac_os_deployment_target"]) + + return vars(parser.parse_args()) + + +def lipo(path0, path1, dst): + if subprocess.call(["lipo", "-create", "-output", dst, path0, path1]) != 0: + print(f"WARNING: {path0} and {path1} cannot be lipo'd") + + shutil.copy(path0, dst) + + +def recursive_merge_binaries(src0, src1, dst): + """ + Merges two build trees together for different architectures into a single + universal binary. + + The rules for merging are: + + 1) Files that exist in either src tree are copied into the dst tree + 2) Files that exist in both trees and are identical are copied over + unmodified + 3) Files that exist in both trees and are non-identical are lipo'd + 4) Symlinks are created in the destination tree to mirror the hierarchy in + the source trees + """ + + # Check that all files present in the folder are of the same type and that + # links link to the same relative location + for newpath0 in glob.glob(src0+"/*"): + filename = os.path.basename(newpath0) + newpath1 = os.path.join(src1, filename) + if not os.path.exists(newpath1): + continue + + if os.path.islink(newpath0) and os.path.islink(newpath1): + if os.path.relpath(newpath0, src0) == os.path.relpath(newpath1, src1): + continue + + if os.path.isdir(newpath0) and os.path.isdir(newpath1): + continue + + # isfile() can be true for links so check that both are not links + # before checking if they are both files + if (not os.path.islink(newpath0)) and (not os.path.islink(newpath1)): + if os.path.isfile(newpath0) and os.path.isfile(newpath1): + continue + + raise Exception(f"{newpath0} and {newpath1} cannot be " + + "merged into a universal binary because they are of " + + "incompatible types. Perhaps the installed libraries" + + " are from different versions for each architecture") + + for newpath0 in glob.glob(src0+"/*"): + filename = os.path.basename(newpath0) + newpath1 = os.path.join(src1, filename) + new_dst_path = os.path.join(dst, filename) + if os.path.islink(newpath0): + # Symlinks will be fixed after files are resolved + continue + + if not os.path.exists(newpath1): + if os.path.isdir(newpath0): + shutil.copytree(newpath0, new_dst_path) + else: + shutil.copy(newpath0, new_dst_path) + + continue + + if os.path.isdir(newpath1): + os.mkdir(new_dst_path) + recursive_merge_binaries(newpath0, newpath1, new_dst_path) + continue + + if filecmp.cmp(newpath0, newpath1): + shutil.copy(newpath0, new_dst_path) + else: + lipo(newpath0, newpath1, new_dst_path) + + # Loop over files in src1 and copy missing things over to dst + for newpath1 in glob.glob(src1+"/*"): + filename = os.path.basename(newpath1) + newpath0 = os.path.join(src0, filename) + new_dst_path = os.path.join(dst, filename) + if (not os.path.exists(newpath0)) and (not os.path.islink(newpath1)): + if os.path.isdir(newpath1): + shutil.copytree(newpath1, new_dst_path) + else: + shutil.copy(newpath1, new_dst_path) + + # Fix up symlinks for path0 + for newpath0 in glob.glob(src0+"/*"): + filename = os.path.basename(newpath0) + new_dst_path = os.path.join(dst, filename) + if os.path.islink(newpath0): + relative_path = os.path.relpath(os.path.realpath(newpath0), src0) + os.symlink(relative_path, new_dst_path) + # Fix up symlinks for path1 + for newpath1 in glob.glob(src1+"/*"): + filename = os.path.basename(newpath1) + new_dst_path = os.path.join(dst, filename) + newpath0 = os.path.join(src0, filename) + if os.path.islink(newpath1) and not os.path.exists(newpath0): + relative_path = os.path.relpath(os.path.realpath(newpath1), src1) + os.symlink(relative_path, new_dst_path) + + +def build(config): + """ + Builds the project with the parameters specified in config. + """ + + print("Building config:") + print(json.dumps(config, indent=4)) + + # Configure and build single architecture builds for each architecture + for arch in ARCHITECTURES: + if not os.path.exists(arch): + os.mkdir(arch) + + env = os.environ.copy() + env["Qt5_DIR"] = config[arch+"_qt5_path"] + env["CMAKE_OSX_ARCHITECTURES"] = arch + env["CMAKE_PREFIX_PATH"] = config[arch+"_cmake_prefix"] + + # Add the other architecture's prefix path to the ignore path so that + # CMake doesn't try to pick up the wrong architecture's libraries when + # cross compiling. + ignore_path = "" + for a in ARCHITECTURES: + if a != arch: + ignore_path = config[a+"_cmake_prefix"] + + subprocess.check_call([ + "cmake", "../../", "-G", config["generator"], + "-DCMAKE_BUILD_TYPE=" + config["build_type"], + '-DCMAKE_CXX_FLAGS="-DMACOS_UNIVERSAL_BUILD=1"', + '-DCMAKE_C_FLAGS="-DMACOS_UNIVERSAL_BUILD=1"', + # System name needs to be specified for CMake to use + # the specified CMAKE_SYSTEM_PROCESSOR + "-DCMAKE_SYSTEM_NAME=Darwin", + "-DCMAKE_PREFIX_PATH="+config[arch+"_cmake_prefix"], + "-DCMAKE_SYSTEM_PROCESSOR="+arch, + "-DCMAKE_IGNORE_PATH="+ignore_path, + "-DCMAKE_OSX_DEPLOYMENT_TARGET=" + + config[arch+"_mac_os_deployment_target"], + "-DMACOS_CODE_SIGNING_IDENTITY=" + + config["codesign_identity"], + "-DMACOS_CODE_SIGNING_IDENTITY_UPDATER=" + + config["codesign_identity"], + '-DMACOS_CODE_SIGNING="ON"' + ], + env=env, cwd=arch) + + threads = multiprocessing.cpu_count() + subprocess.check_call(["cmake", "--build", ".", + "--config", config["build_type"], + "--parallel", f"{threads}"], cwd=arch) + + dst_app = config["dst_app"] + + if os.path.exists(dst_app): + shutil.rmtree(dst_app) + + # Create and codesign the universal binary/ + os.mkdir(dst_app) + + # Source binary trees to merge together + src_app0 = ARCHITECTURES[0]+"/Binaries/" + src_app1 = ARCHITECTURES[1]+"/Binaries/" + + recursive_merge_binaries(src_app0, src_app1, dst_app) + for path in glob.glob(dst_app+"/*"): + if os.path.isdir(path) and os.path.splitext(path)[1] != ".app": + continue + + subprocess.check_call([ + "codesign", + "-d", + "--force", + "-s", + config["codesign_identity"], + "--options=runtime", + "--entitlements", config["entitlements"], + "--deep", + "--verbose=2", + path]) + + +if __name__ == "__main__": + conf = parse_args() + build(conf) + print("Built Universal Binary successfully!") diff --git a/CMakeLists.txt b/CMakeLists.txt index e1565844e4..a480467b3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,13 +2,17 @@ # General setup # cmake_minimum_required(VERSION 3.10) -set(CMAKE_OSX_ARCHITECTURES "x86_64") + # Minimum OS X version. # This is inserted into the Info.plist as well. -# MacOS prior to 10.12 did not fully support C++17, which is used to -# handle configuration options -set(CMAKE_OSX_DEPLOYMENT_TARGET "10.12.0" CACHE STRING "") +# MacOS prior to 10.14 did not support aligned alloc which is used to implement +# std::unique_ptr in the arm64 C++ standard library. x86_64 builds can override +# this to 10.12.0 using -DCMAKE_OSX_DEPLOYMENT_TARGET="10.12.0" without issue. +# This is done in the universal binary building script to build a binary that +# runs on 10.12 on x86_64 computers, while still containing an arm64 slice. + +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14.0" CACHE STRING "") set(CMAKE_USER_MAKE_RULES_OVERRIDE "CMake/FlagsOverride.cmake") @@ -68,8 +72,12 @@ else() endif() if(APPLE) - option(OSX_USE_DEFAULT_SEARCH_PATH "Don't prioritize system library paths" OFF) - option(SKIP_POSTPROCESS_BUNDLE "Skip postprocessing bundle for redistributability" OFF) + option(MACOS_USE_DEFAULT_SEARCH_PATH "Don't prioritize system library paths" OFF) + option(SKIP_POSTPROCESS_BUNDLE "Skip postprocessing bundle for redistributability" OFF) + # Enable adhoc code signing by default (otherwise makefile builds on ARM will not work) + option(MACOS_CODE_SIGNING "Enable codesigning" ON) + set(MACOS_CODE_SIGNING_IDENTITY "-" CACHE STRING "The identity used for codesigning.") + set(MACOS_CODE_SIGNING_IDENTITY_UPDATER "-" CACHE STRING "The identity used for codesigning, for the updater.") endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") @@ -290,7 +298,7 @@ else() endif() if(CMAKE_SYSTEM_NAME MATCHES "Darwin") - if(NOT OSX_USE_DEFAULT_SEARCH_PATH) + if(NOT MACOS_USE_DEFAULT_SEARCH_PATH) # Hack up the path to prioritize the path to built-in OS libraries to # increase the chance of not depending on a bunch of copies of them # installed by MacPorts, Fink, Homebrew, etc, and ending up copying @@ -306,9 +314,10 @@ if(CMAKE_SYSTEM_NAME MATCHES "Darwin") set(CMAKE_XCODE_ATTRIBUTE_GCC_STRICT_ALIASING NO) # Specify target CPUs. - check_and_add_flag(HAVE_MSSSE3 -mssse3) - check_and_add_flag(HAVE_ARCH_CORE2 -march=core2) - + if(_ARCH_64 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64") + check_and_add_flag(HAVE_MSSSE3 -mssse3) + check_and_add_flag(HAVE_ARCH_CORE2 -march=core2) + endif() # Linker flags. # Drop unreachable code and data. set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-dead_strip,-dead_strip_dylibs") diff --git a/Externals/MoltenVK/libvulkan.dylib b/Externals/MoltenVK/libvulkan.dylib index efb0db74b8..4eddd174d4 100755 Binary files a/Externals/MoltenVK/libvulkan.dylib and b/Externals/MoltenVK/libvulkan.dylib differ diff --git a/Externals/MoltenVK/version.txt b/Externals/MoltenVK/version.txt index 9f1cd7fc3d..2454c57670 100644 --- a/Externals/MoltenVK/version.txt +++ b/Externals/MoltenVK/version.txt @@ -1 +1 @@ -MoltenVK from https://github.com/KhronosGroup/MoltenVK, commit b9b78def172074872bfbb1015ccf75eeec554ae2 +MoltenVK from https://vulkan.lunarg.com/sdk/home#mac, version 1.2.170.0 \ No newline at end of file diff --git a/Readme.md b/Readme.md index 2093723871..4a16b75fc9 100644 --- a/Readme.md +++ b/Readme.md @@ -65,6 +65,8 @@ missing packages yourself. ### macOS Build Steps: +A binary supporting a single architecture can be built using the following steps: + 1. `mkdir build` 2. `cd build` 3. `cmake ..` @@ -72,6 +74,18 @@ missing packages yourself. An application bundle will be created in `./Binaries`. +A script is also provided to build universal binaries supporting both x64 and ARM in the same +application bundle using the following steps: + +1. `mkdir build` +2. `cd build` +3. `python ../BuildMacOSUniversalBinary.py` +4. Universal binaries will be available in the `universal` folder + +Doing this is more complex as it requires installation of library dependencies for both x64 and ARM (or universal library +equivalents) and may require specifying additional arguments to point to relevant library locations. +Execute BuildMacOSUniversalBinary.py --help for more details. + ### Linux Global Build Steps: To install to your system. diff --git a/Source/Core/Common/Arm64Emitter.cpp b/Source/Core/Common/Arm64Emitter.cpp index ab701d612f..4d6feb222f 100644 --- a/Source/Core/Common/Arm64Emitter.cpp +++ b/Source/Core/Common/Arm64Emitter.cpp @@ -21,6 +21,9 @@ #ifdef _WIN32 #include #endif +#ifdef __APPLE__ +#include +#endif namespace Arm64Gen { @@ -342,7 +345,7 @@ void ARM64XEmitter::FlushIcacheSection(u8* start, u8* end) if (start == end) return; -#if defined(IOS) +#if defined(IOS) || defined(__APPLE__) // Header file says this is equivalent to: sys_icache_invalidate(start, end - start); sys_cache_control(kCacheFunctionPrepareForExecution, start, end - start); #elif defined(WIN32) diff --git a/Source/Core/Common/ArmCPUDetect.cpp b/Source/Core/Common/ArmCPUDetect.cpp index a603ff03fd..4100df9f82 100644 --- a/Source/Core/Common/ArmCPUDetect.cpp +++ b/Source/Core/Common/ArmCPUDetect.cpp @@ -8,7 +8,7 @@ #include #include -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__APPLE__) #ifndef __FreeBSD__ #include #endif @@ -71,7 +71,17 @@ void CPUInfo::Detect() vendor = CPUVendor::ARM; bFlushToZero = true; -#ifdef _WIN32 +#ifdef __APPLE__ + num_cores = std::thread::hardware_concurrency(); + + // M-series CPUs have all of these + bFP = true; + bASIMD = true; + bAES = true; + bSHA1 = true; + bSHA2 = true; + bCRC32 = true; +#elif defined(_WIN32) num_cores = std::thread::hardware_concurrency(); // Windows does not provide any mechanism for querying the system registers on ARMv8, unlike Linux diff --git a/Source/Core/Common/MemoryUtil.cpp b/Source/Core/Common/MemoryUtil.cpp index 44a6015027..ce5182c976 100644 --- a/Source/Core/Common/MemoryUtil.cpp +++ b/Source/Core/Common/MemoryUtil.cpp @@ -16,6 +16,7 @@ #include #include "Common/StringUtil.h" #else +#include #include #include #include @@ -38,9 +39,15 @@ void* AllocateExecutableMemory(size_t size) #if defined(_WIN32) void* ptr = VirtualAlloc(nullptr, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE); #else - void* ptr = - mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); - + int map_flags = MAP_ANON | MAP_PRIVATE; +#if defined(__APPLE__) + // This check is in place to prepare for x86_64 MAP_JIT support. While MAP_JIT did exist + // prior to 10.14, it had restrictions on the number of JIT allocations that were removed + // in 10.14. + if (__builtin_available(macOS 10.14, *)) + map_flags |= MAP_JIT; +#endif + void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, map_flags, -1, 0); if (ptr == MAP_FAILED) ptr = nullptr; #endif @@ -50,6 +57,79 @@ void* AllocateExecutableMemory(size_t size) return ptr; } +// This function is used to provide a counter for the JITPageWrite*Execute* +// functions to enable nesting. The static variable is wrapped in a a function +// to allow those functions to be called inside of the constructor of a static +// variable portably. +// +// The variable is thread_local as the W^X mode is specific to each running thread. +static int& JITPageWriteNestCounter() +{ + static thread_local int nest_counter = 0; + return nest_counter; +} + +// Certain platforms (Mac OS on ARM) enforce that a single thread can only have write or +// execute permissions to pages at any given point of time. The two below functions +// are used to toggle between having write permissions or execute permissions. +// +// The default state of these allocations in Dolphin is for them to be executable, +// but not writeable. So, functions that are updating these pages should wrap their +// writes like below: + +// JITPageWriteEnableExecuteDisable(); +// PrepareInstructionStreamForJIT(); +// JITPageWriteDisableExecuteEnable(); + +// These functions can be nested, in which case execution will only be enabled +// after the call to the JITPageWriteDisableExecuteEnable from the top most +// nesting level. Example: + +// [JIT page is in execute mode for the thread] +// JITPageWriteEnableExecuteDisable(); +// [JIT page is in write mode for the thread] +// JITPageWriteEnableExecuteDisable(); +// [JIT page is in write mode for the thread] +// JITPageWriteDisableExecuteEnable(); +// [JIT page is in write mode for the thread] +// JITPageWriteDisableExecuteEnable(); +// [JIT page is in execute mode for the thread] + +// Allows a thread to write to executable memory, but not execute the data. +void JITPageWriteEnableExecuteDisable() +{ +#if defined(_M_ARM_64) && defined(__APPLE__) + if (JITPageWriteNestCounter() == 0) + { + if (__builtin_available(macOS 11.0, *)) + { + pthread_jit_write_protect_np(0); + } + } +#endif + JITPageWriteNestCounter()++; +} +// Allows a thread to execute memory allocated for execution, but not write to it. +void JITPageWriteDisableExecuteEnable() +{ + JITPageWriteNestCounter()--; + + // Sanity check the NestCounter to identify underflow + // This can indicate the calls to JITPageWriteDisableExecuteEnable() + // are not matched with previous calls to JITPageWriteEnableExecuteDisable() + if (JITPageWriteNestCounter() < 0) + PanicAlertFmt("JITPageWriteNestCounter() underflowed"); + +#if defined(_M_ARM_64) && defined(__APPLE__) + if (JITPageWriteNestCounter() == 0) + { + if (__builtin_available(macOS 11.0, *)) + { + pthread_jit_write_protect_np(1); + } + } +#endif +} void* AllocateMemoryPages(size_t size) { @@ -128,7 +208,10 @@ void WriteProtectMemory(void* ptr, size_t size, bool allowExecute) DWORD oldValue; if (!VirtualProtect(ptr, size, allowExecute ? PAGE_EXECUTE_READ : PAGE_READONLY, &oldValue)) PanicAlertFmt("WriteProtectMemory failed!\nVirtualProtect: {}", GetLastErrorString()); -#else +#elif !(defined(_M_ARM_64) && defined(__APPLE__)) + // MacOS 11.2 on ARM does not allow for changing the access permissions of pages + // that were marked executable, instead it uses the protections offered by MAP_JIT + // for write protection. if (mprotect(ptr, size, allowExecute ? (PROT_READ | PROT_EXEC) : PROT_READ) != 0) PanicAlertFmt("WriteProtectMemory failed!\nmprotect: {}", LastStrerrorString()); #endif @@ -140,7 +223,10 @@ void UnWriteProtectMemory(void* ptr, size_t size, bool allowExecute) DWORD oldValue; if (!VirtualProtect(ptr, size, allowExecute ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE, &oldValue)) PanicAlertFmt("UnWriteProtectMemory failed!\nVirtualProtect: {}", GetLastErrorString()); -#else +#elif !(defined(_M_ARM_64) && defined(__APPLE__)) + // MacOS 11.2 on ARM does not allow for changing the access permissions of pages + // that were marked executable, instead it uses the protections offered by MAP_JIT + // for write protection. if (mprotect(ptr, size, allowExecute ? (PROT_READ | PROT_WRITE | PROT_EXEC) : PROT_WRITE | PROT_READ) != 0) { diff --git a/Source/Core/Common/MemoryUtil.h b/Source/Core/Common/MemoryUtil.h index 72eb4f68f9..f13c55dab4 100644 --- a/Source/Core/Common/MemoryUtil.h +++ b/Source/Core/Common/MemoryUtil.h @@ -10,6 +10,23 @@ namespace Common { void* AllocateExecutableMemory(size_t size); + +// These two functions control the executable/writable state of the W^X memory +// allocations. More detailed documentation about them is in the .cpp file. +// In general where applicable the ScopedJITPageWriteAndNoExecute wrapper +// should be used to prevent bugs from not pairing up the calls properly. + +// Allows a thread to write to executable memory, but not execute the data. +void JITPageWriteEnableExecuteDisable(); +// Allows a thread to execute memory allocated for execution, but not write to it. +void JITPageWriteDisableExecuteEnable(); +// RAII Wrapper around JITPageWrite*Execute*(). When this is in scope the thread can +// write to executable memory but not execute it. +struct ScopedJITPageWriteAndNoExecute +{ + ScopedJITPageWriteAndNoExecute() { JITPageWriteEnableExecuteDisable(); } + ~ScopedJITPageWriteAndNoExecute() { JITPageWriteDisableExecuteEnable(); } +}; void* AllocateMemoryPages(size_t size); void FreeMemoryPages(void* ptr, size_t size); void* AllocateAlignedMemory(size_t size, size_t alignment); diff --git a/Source/Core/Core/DolphinAnalytics.cpp b/Source/Core/Core/DolphinAnalytics.cpp index 07e541af2b..11071d0678 100644 --- a/Source/Core/Core/DolphinAnalytics.cpp +++ b/Source/Core/Core/DolphinAnalytics.cpp @@ -289,11 +289,17 @@ void DolphinAnalytics::MakeBaseBuilder() s64 minor_version; // NSInteger minorVersion s64 patch_version; // NSInteger patchVersion }; - + // Under arm64, we need to call objc_msgSend to recieve a struct. + // On x86_64, we need to explicitly call objc_msgSend_stret for a struct. +#if _M_ARM_64 +#define msgSend objc_msgSend +#else +#define msgSend objc_msgSend_stret +#endif // NSOperatingSystemVersion version = [processInfo operatingSystemVersion] - OSVersion version = reinterpret_cast(objc_msgSend_stret)( + OSVersion version = reinterpret_cast(msgSend)( processInfo, sel_getUid("operatingSystemVersion")); - +#undef msgSend builder.AddData("osx-ver-major", version.major_version); builder.AddData("osx-ver-minor", version.minor_version); builder.AddData("osx-ver-bugfix", version.patch_version); diff --git a/Source/Core/Core/MachineContext.h b/Source/Core/Core/MachineContext.h index 1ae0a1cc48..afad4ba0ee 100644 --- a/Source/Core/Core/MachineContext.h +++ b/Source/Core/Core/MachineContext.h @@ -67,6 +67,12 @@ typedef x86_thread_state64_t SContext; #define CTX_R14 __r14 #define CTX_R15 __r15 #define CTX_RIP __rip +#elif _M_ARM_64 +typedef arm_thread_state64_t SContext; +#define CTX_REG(x) __x[x] +#define CTX_LR __x[30] +#define CTX_SP __sp +#define CTX_PC __pc #else #error No context definition for architecture #endif diff --git a/Source/Core/Core/MemTools.cpp b/Source/Core/Core/MemTools.cpp index 7559b1b6bc..e269252a4a 100644 --- a/Source/Core/Core/MemTools.cpp +++ b/Source/Core/Core/MemTools.cpp @@ -25,6 +25,20 @@ #include // Needed for _POSIX_VERSION #endif +#if defined(__APPLE__) +#ifdef _M_X86_64 +#define THREAD_STATE64_COUNT x86_THREAD_STATE64_COUNT +#define THREAD_STATE64 x86_THREAD_STATE64 +#define thread_state64_t x86_thread_state64_t +#elif defined(_M_ARM_64) +#define THREAD_STATE64_COUNT ARM_THREAD_STATE64_COUNT +#define THREAD_STATE64 ARM_THREAD_STATE64 +#define thread_state64_t arm_thread_state64_t +#else +#error Unsupported architecture +#endif +#endif + namespace EMM { #ifdef _WIN32 @@ -123,7 +137,7 @@ static void ExceptionThread(mach_port_t port) int64_t code[2]; int flavor; mach_msg_type_number_t old_stateCnt; - natural_t old_state[x86_THREAD_STATE64_COUNT]; + natural_t old_state[THREAD_STATE64_COUNT]; mach_msg_trailer_t trailer; } msg_in; @@ -134,7 +148,7 @@ static void ExceptionThread(mach_port_t port) kern_return_t RetCode; int flavor; mach_msg_type_number_t new_stateCnt; - natural_t new_state[x86_THREAD_STATE64_COUNT]; + natural_t new_state[THREAD_STATE64_COUNT]; } msg_out; #pragma pack() memset(&msg_in, 0xee, sizeof(msg_in)); @@ -165,13 +179,13 @@ static void ExceptionThread(mach_port_t port) return; } - if (msg_in.flavor != x86_THREAD_STATE64) + if (msg_in.flavor != THREAD_STATE64) { - PanicAlertFmt("unknown flavor {} (expected {})", msg_in.flavor, x86_THREAD_STATE64); + PanicAlertFmt("unknown flavor {} (expected {})", msg_in.flavor, THREAD_STATE64); return; } - x86_thread_state64_t* state = (x86_thread_state64_t*)msg_in.old_state; + thread_state64_t* state = (thread_state64_t*)msg_in.old_state; bool ok = JitInterface::HandleFault((uintptr_t)msg_in.code[1], state); @@ -184,9 +198,9 @@ static void ExceptionThread(mach_port_t port) if (ok) { msg_out.RetCode = KERN_SUCCESS; - msg_out.flavor = x86_THREAD_STATE64; - msg_out.new_stateCnt = x86_THREAD_STATE64_COUNT; - memcpy(msg_out.new_state, msg_in.old_state, x86_THREAD_STATE64_COUNT * sizeof(natural_t)); + 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)); } else { @@ -218,7 +232,7 @@ void InstallExceptionHandler() // Debuggers set the task port, so we grab the thread port. CheckKR("thread_set_exception_ports", thread_set_exception_ports(mach_thread_self(), EXC_MASK_BAD_ACCESS, port, - EXCEPTION_STATE | MACH_EXCEPTION_CODES, x86_THREAD_STATE64)); + EXCEPTION_STATE | MACH_EXCEPTION_CODES, THREAD_STATE64)); // ...and get rid of our copy so that MACH_NOTIFY_NO_SENDERS works. CheckKR("mach_port_mod_refs", mach_port_mod_refs(mach_task_self(), port, MACH_PORT_RIGHT_SEND, -1)); diff --git a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp index 2d838d7ba6..b606b071ea 100644 --- a/Source/Core/Core/PowerPC/JitArm64/Jit.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/Jit.cpp @@ -73,6 +73,8 @@ void JitArm64::Init() bool JitArm64::HandleFault(uintptr_t access_address, SContext* ctx) { + // Ifdef this since the exception handler runs on a separate thread on macOS (ARM) +#if !(defined(__APPLE__) && defined(_M_ARM_64)) // We can't handle any fault from other threads. if (!Core::IsCPUThread()) { @@ -80,6 +82,7 @@ bool JitArm64::HandleFault(uintptr_t access_address, SContext* ctx) DoBacktrace(access_address, ctx); return false; } +#endif bool success = false; @@ -124,6 +127,7 @@ void JitArm64::ClearCache() m_handler_to_loc.clear(); blocks.Clear(); + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; ClearCodeSpace(); farcode.ClearCodeSpace(); UpdateMemoryOptions(); @@ -596,6 +600,7 @@ void JitArm64::Jit(u32) { ClearCache(); } + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; std::size_t block_size = m_code_buffer.size(); const u32 em_address = PowerPC::ppcState.pc; diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64Cache.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64Cache.cpp index 66a2d3eab4..6a3e80bf35 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64Cache.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64Cache.cpp @@ -59,11 +59,11 @@ void JitArm64BlockCache::WriteLinkBlock(Arm64Gen::ARM64XEmitter& emit, void JitArm64BlockCache::WriteLinkBlock(const JitBlock::LinkData& source, const JitBlock* dest) { + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; u8* location = source.exitPtrs; ARM64XEmitter emit(location); WriteLinkBlock(emit, source, dest); - emit.FlushIcache(); } @@ -71,9 +71,8 @@ void JitArm64BlockCache::WriteDestroyBlock(const JitBlock& block) { // Only clear the entry points as we might still be within this block. ARM64XEmitter emit(block.checkedEntry); - + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; while (emit.GetWritableCodePtr() <= block.normalEntry) emit.BRK(0x123); - emit.FlushIcache(); } diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp index 4f1aca8e60..23236a524e 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_BackPatch.cpp @@ -289,6 +289,7 @@ bool JitArm64::HandleFastmemFault(uintptr_t access_address, SContext* ctx) if ((const u8*)ctx->CTX_PC - fault_location > fastmem_area_length) return false; + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; ARM64XEmitter emitter((u8*)fault_location); emitter.BL(slow_handler_iter->second.slowmem_code); @@ -300,6 +301,7 @@ bool JitArm64::HandleFastmemFault(uintptr_t access_address, SContext* ctx) m_fault_to_handler.erase(slow_handler_iter); emitter.FlushIcache(); + ctx->CTX_PC = reinterpret_cast(fault_location); return true; } diff --git a/Source/Core/Core/PowerPC/JitArm64/JitAsm.cpp b/Source/Core/Core/PowerPC/JitArm64/JitAsm.cpp index ddd11e4114..0fe91bd34d 100644 --- a/Source/Core/Core/PowerPC/JitArm64/JitAsm.cpp +++ b/Source/Core/Core/PowerPC/JitArm64/JitAsm.cpp @@ -25,6 +25,8 @@ using namespace Arm64Gen; void JitArm64::GenerateAsm() { + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; + // This value is all of the callee saved registers that we are required to save. // According to the AACPS64 we need to save R19 ~ R30 and Q8 ~ Q15. const u32 ALL_CALLEE_SAVED = 0x7FF80000; diff --git a/Source/Core/Core/PowerPC/JitInterface.cpp b/Source/Core/Core/PowerPC/JitInterface.cpp index e36b27314c..adb12f2fa3 100644 --- a/Source/Core/Core/PowerPC/JitInterface.cpp +++ b/Source/Core/Core/PowerPC/JitInterface.cpp @@ -71,9 +71,8 @@ CPUCoreBase* InitJitCore(PowerPC::CPUCore core) break; default: - PanicAlertFmtT("The selected CPU emulation core ({0}) is not available. " - "Please select a different CPU emulation core in the settings.", - core); + // Under this case the caller overrides the CPU core to the default and logs that + // it performed the override. g_jit = nullptr; return nullptr; } diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 288e313e3f..9ae4714317 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -473,6 +473,8 @@ if(APPLE) set_target_properties(dolphin-emu PROPERTIES MACOSX_BUNDLE true MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in + XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/DolphinEmu.entitlements" + XCODE_ATTRIBUTE_OTHER_CODE_SIGN_FLAGS "--deep --options=runtime" OUTPUT_NAME Dolphin ) @@ -516,6 +518,22 @@ if(APPLE) POST_BUILD COMMAND ${CMAKE_INSTALL_NAME_TOOL} -add_rpath "@executable_path/../Frameworks/" $) + + if(MACOS_CODE_SIGNING) + # Code sign make file builds + add_custom_command(TARGET dolphin-emu + POST_BUILD COMMAND + /usr/bin/codesign -f -s "${MACOS_CODE_SIGNING_IDENTITY}" --deep --options=runtime --entitlements "${CMAKE_SOURCE_DIR}/Source/Core/DolphinQt/DolphinEmu.entitlements" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/Dolphin.app" || true) + + # Code sign builds for build systems that do have release/debug variants (Xcode) + add_custom_command(TARGET dolphin-emu + POST_BUILD COMMAND + /usr/bin/codesign -f -s "${MACOS_CODE_SIGNING_IDENTITY}" --deep --options=runtime --entitlements "${CMAKE_SOURCE_DIR}/Source/Core/DolphinQt/DolphinEmu.entitlements" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG}/Dolphin.app" || true) + + add_custom_command(TARGET dolphin-emu + POST_BUILD COMMAND + /usr/bin/codesign -f -s "${MACOS_CODE_SIGNING_IDENTITY}" --deep --options=runtime --entitlements "${CMAKE_SOURCE_DIR}/Source/Core/DolphinQt/DolphinEmu.entitlements" "${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE}/Dolphin.app" || true) + endif() else() install(TARGETS dolphin-emu RUNTIME DESTINATION ${bindir}) endif() diff --git a/Source/Core/DolphinQt/DolphinEmu.entitlements b/Source/Core/DolphinQt/DolphinEmu.entitlements new file mode 100644 index 0000000000..50c57c58c1 --- /dev/null +++ b/Source/Core/DolphinQt/DolphinEmu.entitlements @@ -0,0 +1,17 @@ + + + + + com.apple.security.cs.allow-jit + + + com.apple.security.device.audio-input + + + com.apple.security.automation.apple-events + + + com.apple.security.cs.disable-library-validation + + + diff --git a/Source/Core/MacUpdater/CMakeLists.txt b/Source/Core/MacUpdater/CMakeLists.txt index 725917e030..65e7d99a92 100644 --- a/Source/Core/MacUpdater/CMakeLists.txt +++ b/Source/Core/MacUpdater/CMakeLists.txt @@ -13,6 +13,8 @@ set(SOURCES add_executable(MacUpdater ${SOURCES}) set(MacUpdater_NAME "Dolphin Updater") +set(MacUpdater_BIN_DIR ${CMAKE_BINARY_DIR}/Binaries) +set(MacUpdater_BUNDLE_PATH ${MacUpdater_BIN_DIR}/${MacUpdater_NAME}.app) set_target_properties(MacUpdater PROPERTIES MACOSX_BUNDLE true @@ -53,8 +55,24 @@ foreach(sb ${STORYBOARDS}) add_custom_command(TARGET MacUpdater POST_BUILD COMMAND ${IBTOOL} --errors --warnings --notices --output-format human-readable-text - --compile ${MacUpdater_BIN_DIR}/${MacUpdater_NAME}.app/Contents/Resources/${sb}c + --compile ${MacUpdater_BUNDLE_PATH}/Contents/Resources/${sb}c ${CMAKE_CURRENT_SOURCE_DIR}/${sb} COMMENT "Compiling Storyboard ${sb}...") endforeach() +if(MACOS_CODE_SIGNING) + if (MACOS_CODE_SIGNING_IDENTITY_UPDATER STREQUAL "") + set(MACOS_CODE_SIGNING_IDENTITY_UPDATER "${MACOS_CODE_SIGNING_IDENTITY}") + endif() + + # Make file build code sign + add_custom_command(TARGET MacUpdater POST_BUILD + COMMAND test ${MacUpdater_BUNDLE_PATH} || /usr/bin/codesign -f -s "${MACOS_CODE_SIGNING_IDENTITY_UPDATER}" --deep --options runtime ${MacUpdater_BUNDLE_PATH}) + + # Xcode build code sign + add_custom_command(TARGET MacUpdater POST_BUILD + COMMAND test "${CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG}/${MacUpdater_NAME}.app" || /usr/bin/codesign -f -s "${MACOS_CODE_SIGNING_IDENTITY_UPDATER}" --deep --options runtime "${CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG}/${MacUpdater_NAME}.app") + + add_custom_command(TARGET MacUpdater POST_BUILD + COMMAND test "${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE}/${MacUpdater_NAME}.app" || /usr/bin/codesign -f -s "${MACOS_CODE_SIGNING_IDENTITY_UPDATER}" --deep --options runtime "${CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE}/${MacUpdater_NAME}.app") +endif() diff --git a/Source/Core/UICommon/AutoUpdate.cpp b/Source/Core/UICommon/AutoUpdate.cpp index 0a5ad10c40..5808d3ac2e 100644 --- a/Source/Core/UICommon/AutoUpdate.cpp +++ b/Source/Core/UICommon/AutoUpdate.cpp @@ -140,7 +140,11 @@ static std::string GetPlatformID() #if defined _WIN32 return "win"; #elif defined __APPLE__ +#if defined(MACOS_UNIVERSAL_BUILD) + return "macos-universal"; +#else return "macos"; +#endif #else return "unknown"; #endif diff --git a/Source/Core/VideoCommon/VertexLoaderARM64.cpp b/Source/Core/VideoCommon/VertexLoaderARM64.cpp index e546c42be8..afd2f0f21d 100644 --- a/Source/Core/VideoCommon/VertexLoaderARM64.cpp +++ b/Source/Core/VideoCommon/VertexLoaderARM64.cpp @@ -54,6 +54,7 @@ VertexLoaderARM64::VertexLoaderARM64(const TVtxDesc& vtx_desc, const VAT& vtx_at : VertexLoaderBase(vtx_desc, vtx_att), m_float_emit(this) { AllocCodeSpace(4096); + const Common::ScopedJITPageWriteAndNoExecute enable_jit_page_writes; ClearCodeSpace(); GenerateVertexLoader(); WriteProtect();