Add macOS Support (#537)

This commit is contained in:
David Chavez 2025-03-14 21:07:07 +01:00 committed by GitHub
parent 91db87632c
commit 25e7b31228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 594 additions and 65 deletions

26
.github/macos/Info.plist.in vendored Normal file
View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleExecutable</key>
<string>Zelda64Recompiled</string>
<key>CFBundleIconFile</key>
<string>${MACOSX_BUNDLE_ICON_FILE}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSMinimumSystemVersion</key>
<string>11</string>
<key>GCSupportsGameMode</key>
<true/>
</dict>
</plist>

87
.github/macos/apple_bundle.cmake vendored Normal file
View File

@ -0,0 +1,87 @@
# Define the path to the entitlements file
set(ENTITLEMENTS_FILE ${CMAKE_SOURCE_DIR}/.github/macos/entitlements.plist)
# Set bundle properties
set_target_properties(Zelda64Recompiled PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME "Zelda64Recompiled"
MACOSX_BUNDLE_GUI_IDENTIFIER "com.github.zelda64recompiled"
MACOSX_BUNDLE_BUNDLE_VERSION "1.0"
MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0"
MACOSX_BUNDLE_ICON_FILE "AppIcon.icns"
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_BINARY_DIR}/Info.plist
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "-"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${ENTITLEMENTS_FILE}
)
# Create icon files for macOS bundle
set(ICON_SOURCE ${CMAKE_SOURCE_DIR}/icons/512.png)
set(ICONSET_DIR ${CMAKE_BINARY_DIR}/AppIcon.iconset)
set(ICNS_FILE ${CMAKE_BINARY_DIR}/resources/AppIcon.icns)
# Create iconset directory and add PNG file
add_custom_command(
OUTPUT ${ICONSET_DIR}
COMMAND ${CMAKE_COMMAND} -E make_directory ${ICONSET_DIR}
COMMAND ${CMAKE_COMMAND} -E copy ${ICON_SOURCE} ${ICONSET_DIR}/icon_512x512.png
COMMAND ${CMAKE_COMMAND} -E copy ${ICON_SOURCE} ${ICONSET_DIR}/icon_512x512@2x.png
COMMAND touch ${ICONSET_DIR}
COMMENT "Creating iconset directory and copying PNG file"
)
# Convert iconset to icns
add_custom_command(
OUTPUT ${ICNS_FILE}
DEPENDS ${ICONSET_DIR}
COMMAND iconutil -c icns ${ICONSET_DIR} -o ${ICNS_FILE}
COMMENT "Converting iconset to icns"
)
# Custom target to ensure icns creation
add_custom_target(create_icns ALL DEPENDS ${ICNS_FILE})
# Set source file properties for the resulting icns file
set_source_files_properties(${ICNS_FILE} PROPERTIES
MACOSX_PACKAGE_LOCATION "Resources"
)
# Add the icns file to the executable target
target_sources(Zelda64Recompiled PRIVATE ${ICNS_FILE})
# Ensure Zelda64Recompiled depends on create_icns
add_dependencies(Zelda64Recompiled create_icns)
# Configure Info.plist
configure_file(${CMAKE_SOURCE_DIR}/.github/macos/Info.plist.in ${CMAKE_BINARY_DIR}/Info.plist @ONLY)
# Install the app bundle
install(TARGETS Zelda64Recompiled BUNDLE DESTINATION .)
# Ensure the entitlements file exists
if(NOT EXISTS ${ENTITLEMENTS_FILE})
message(FATAL_ERROR "Entitlements file not found at ${ENTITLEMENTS_FILE}")
endif()
# Post-build steps for macOS bundle
add_custom_command(TARGET Zelda64Recompiled POST_BUILD
# Copy and fix frameworks first
COMMAND ${CMAKE_COMMAND} -D CMAKE_BUILD_TYPE=$<CONFIG> -D CMAKE_GENERATOR=${CMAKE_GENERATOR} -P ${CMAKE_SOURCE_DIR}/.github/macos/fixup_bundle.cmake
# Copy all resources
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/assets ${CMAKE_BINARY_DIR}/temp_assets
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/temp_assets/scss
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_BINARY_DIR}/temp_assets $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/Resources/assets
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/temp_assets
# Copy controller database
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/Resources/
# Set RPATH
COMMAND install_name_tool -add_rpath "@executable_path/../Frameworks/" $<TARGET_BUNDLE_DIR:Zelda64Recompiled>/Contents/MacOS/Zelda64Recompiled
# Sign the bundle
COMMAND codesign --verbose=4 --options=runtime --no-strict --sign - --entitlements ${ENTITLEMENTS_FILE} --deep --force $<TARGET_BUNDLE_DIR:Zelda64Recompiled>
COMMENT "Performing post-build steps for macOS bundle"
VERBATIM
)

14
.github/macos/entitlements.plist vendored Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>

44
.github/macos/fixup_bundle.cmake vendored Normal file
View File

@ -0,0 +1,44 @@
include(BundleUtilities)
# Check for pkgx installation
find_program(PKGX_EXECUTABLE pkgx)
# Xcode generator puts the build type in the build directory
set(BUILD_PREFIX "")
if (CMAKE_GENERATOR STREQUAL "Xcode")
set(BUILD_PREFIX "${CMAKE_BUILD_TYPE}/")
endif()
# Use generator expressions to get the absolute path to the bundle
set(APPS "${BUILD_PREFIX}Zelda64Recompiled.app/Contents/MacOS/Zelda64Recompiled")
# Set up framework search paths
set(DIRS "${BUILD_PREFIX}Zelda64Recompiled.app/Contents/Frameworks")
# Detect if we're using pkgx
if(PKGX_EXECUTABLE)
message(STATUS "pkgx detected, adding pkgx directories to framework search path")
list(APPEND DIRS "$ENV{HOME}/.pkgx/")
endif()
# Convert all paths to absolute paths
file(REAL_PATH ${APPS} APPS)
set(RESOLVED_DIRS "")
foreach(DIR IN LISTS DIRS)
# Handle home directory expansion
string(REPLACE "~" "$ENV{HOME}" DIR "${DIR}")
# Convert to absolute path, but don't fail if directory doesn't exist
if(EXISTS "${DIR}")
file(REAL_PATH "${DIR}" RESOLVED_DIR)
list(APPEND RESOLVED_DIRS "${RESOLVED_DIR}")
endif()
endforeach()
# Debug output
message(STATUS "Bundle fixup paths:")
message(STATUS " App: ${APPS}")
message(STATUS " Search dirs: ${RESOLVED_DIRS}")
# Fix up the bundle
fixup_bundle("${APPS}" "" "${RESOLVED_DIRS}")

73
.github/macos/ld64 vendored Executable file
View File

@ -0,0 +1,73 @@
#!/usr/bin/python3
"""
Custom ld64 wrapper for macOS
This script wraps the standard macOS linker (/usr/bin/ld) to modify executable memory
protection flags in the resulting Mach-O binary. It works in three stages:
1. First, it passes through all arguments to the regular macOS linker to create the binary
2. Then, it parses command line arguments to identify output file and segment protection flags
3. Finally, it modifies the output binary's Mach-O headers to ensure segments (particularly __TEXT)
have the maximum protection flags (rwx) we specify, even if the default macOS linker would restrict them
This is necessary because macOS restricts writable+executable memory by default,
but certain applications need this capability for dynamic code generation or JIT compilation.
Usage: Same as the standard ld64 linker, with the added benefit that -segprot options
will have their max_prot values properly preserved in the output binary.
"""
import sys
import subprocess
from itertools import takewhile
from macholib import MachO, ptypes
def parse_rwx(text):
return ('r' in text and 1) | ('w' in text and 2) | ('x' in text and 4)
def apply_maxprots(path, maxprots):
mach = MachO.MachO(path)
header = mach.headers[0]
offset = ptypes.sizeof(header.mach_header)
for cload, ccmd, cdata in header.commands:
if not hasattr(ccmd, 'segname'):
break
if hasattr(ccmd.segname, 'to_str'):
segname = ccmd.segname.to_str().decode('utf-8').strip('\0')
else:
segname = ccmd.segname.decode('utf-8').strip('\0')
if segname in maxprots and ccmd.maxprot != maxprots[segname]:
fields = list(takewhile(lambda field: field[0] != 'maxprot', cload._fields_ + ccmd._fields_))
index = offset + sum(ptypes.sizeof(typ) for _, typ in fields)
with open(path, 'r+b') as fh:
fh.seek(index)
fh.write(bytes([maxprots[segname]]))
offset += cload.cmdsize
try:
subprocess.check_call(['/usr/bin/ld'] + sys.argv[1:])
except subprocess.CalledProcessError as ex:
sys.exit(ex.returncode)
output_file = 'a.out'
segprots = {'__TEXT': parse_rwx('rwx')} # maxprot = rwx
i = 1
while i < len(sys.argv):
if sys.argv[i] == '-o' and i + 1 < len(sys.argv):
output_file = sys.argv[i + 1]
i += 2
elif sys.argv[i] == '-segprot' and i + 3 < len(sys.argv):
segment = sys.argv[i + 1]
maxprot = sys.argv[i + 2]
segprots[segment] = parse_rwx(maxprot)
i += 4
else:
i += 1
apply_maxprots(output_file, segprots)

14
.github/macos/macports.yaml vendored Normal file
View File

@ -0,0 +1,14 @@
version: '2.9.3'
prefix: '/opt/local'
variants:
select:
- aqua
- metal
deselect: x11
ports:
- name: clang-18
- name: llvm-18
- name: libsdl2
select: universal
- name: freetype
select: universal

View File

@ -78,16 +78,16 @@ jobs:
- name: Hotpatch DXC into RT64's contrib
run: |
# check if dxc was updated before we replace it, to detect changes
echo ${{ inputs.DXC_CHECKSUM }} ./lib/rt64/src/contrib/dxc/bin/x64/dxc | sha256sum --status -c -
echo ${{ inputs.DXC_CHECKSUM }} ./lib/rt64/src/contrib/dxc/bin/x64/dxc-linux | sha256sum --status -c -
cp -v /usr/local/lib/libdxcompiler.so ./lib/rt64/src/contrib/dxc/lib/x64/libdxcompiler.so
cp -v /usr/local/bin/dxc ./lib/rt64/src/contrib/dxc/bin/x64/dxc
cp -v /usr/local/bin/dxc ./lib/rt64/src/contrib/dxc/bin/x64/dxc-linux
- name: Build ZeldaRecomp
run: |-
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang++-17 -DCMAKE_C_COMPILER=clang-17 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DPATCHES_C_COMPILER=clang-17 -DPATCHES_LD=ld.lld-17 -DPATCHES_OBJCOPY=llvm-objcopy-17
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang++-17 -DCMAKE_C_COMPILER=clang-17 -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DPATCHES_C_COMPILER=clang-17 -DPATCHES_LD=ld.lld-17
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j $(nproc)
- name: Prepare Archive
run: |
@ -283,3 +283,74 @@ jobs:
name: Zelda64Recompiled-PDB-${{ matrix.type }}
path: |
Zelda64Recompiled.pdb
build-macos:
runs-on: blaze/macos-14
strategy:
matrix:
type: [ Debug, Release ]
name: macos (x64, arm64, ${{ matrix.type }})
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
submodules: recursive
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ runner.os }}-z64re-ccache-${{ matrix.type }}
- name: Homebrew Setup
run: |
brew install ninja
brew uninstall --ignore-dependencies libpng freetype
- name: MacPorts Setup
uses: melusina-org/setup-macports@v1
id: 'macports'
with:
parameters: '.github/macos/macports.yaml'
- name: Prepare Build
run: |-
git clone ${{ secrets.ZRE_REPO_WITH_PAT }}
./zre/process.sh
cp ./zre/mm_shader_cache.bin ./shadercache/
- name: Build N64Recomp & RSPRecomp
run: |
git clone https://github.com/Mr-Wiseguy/N64Recomp.git --recurse-submodules N64RecompSource
cd N64RecompSource
git checkout ${{ inputs.N64RECOMP_COMMIT }}
git submodule update --init --recursive
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
# Build N64Recomp & RSPRecomp
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build
cmake --build cmake-build --config Release --target N64Recomp -j $(sysctl -n hw.ncpu)
cmake --build cmake-build --config Release --target RSPRecomp -j $(sysctl -n hw.ncpu)
# Copy N64Recomp & RSPRecomp to root directory
cp cmake-build/N64Recomp ..
cp cmake-build/RSPRecomp ..
- name: Run N64Recomp & RSPRecomp
run: |
./N64Recomp us.rev1.toml
./RSPRecomp aspMain.us.rev1.toml
./RSPRecomp njpgdspMain.us.rev1.toml
- name: Build ZeldaRecomp
run: |-
# enable ccache
export PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH"
cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build \
-DPATCHES_LD=/opt/local/bin/ld.lld-mp-18 -DCMAKE_AR=/opt/local/bin/llvm-ar-mp-18 -DPATCHES_C_COMPILER=/opt/local/bin/clang-mp-18 \
-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"
cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j $(sysctl -n hw.ncpu)
- name: Prepare Archive
run: |
mv cmake-build/Zelda64Recompiled.app Zelda64Recompiled.app
zip -r -y Zelda64Recompiled.zip Zelda64Recompiled.app
- name: Archive Zelda64Recomp
uses: actions/upload-artifact@v4
with:
name: Zelda64Recompiled-${{ runner.os }}-${{ matrix.type }}
path: Zelda64Recompiled.zip

7
.gitignore vendored
View File

@ -2,6 +2,7 @@
.vscode/settings.json
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/tasks.json
# Input elf and rom files
*.elf
@ -62,3 +63,9 @@ RSPRecomp
# Controller mappings file
gamecontrollerdb.txt
# Cmake build directory
.cache
.idea
build-*
cmake-build-*

View File

@ -1,5 +1,10 @@
cmake_minimum_required(VERSION 3.20)
project(Zelda64Recompiled)
if (APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version")
endif()
project(Zelda64Recompiled LANGUAGES C CXX)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
@ -15,6 +20,10 @@ if (WIN32)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR")
endif()
if (APPLE)
enable_language(OBJC OBJCXX)
endif()
# Avoid warning about DOWNLOAD_EXTRACT_TIMESTAMP in CMake 3.24:
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
cmake_policy(SET CMP0135 NEW)
@ -107,7 +116,7 @@ add_custom_target(PatchesBin
# Generate patches_bin.c from patches.bin
add_custom_command(OUTPUT ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin mm_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/patches/patches.bin mm_patches_bin ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.c ${CMAKE_SOURCE_DIR}/RecompiledPatches/patches_bin.h
DEPENDS ${CMAKE_SOURCE_DIR}/patches/patches.bin
)
@ -139,6 +148,7 @@ add_dependencies(Zelda64Recompiled DownloadGameControllerDB)
set (SOURCES
${CMAKE_SOURCE_DIR}/src/main/main.cpp
${CMAKE_SOURCE_DIR}/src/main/support.cpp
${CMAKE_SOURCE_DIR}/src/main/register_overlays.cpp
${CMAKE_SOURCE_DIR}/src/main/register_patches.cpp
${CMAKE_SOURCE_DIR}/src/main/rt64_render_context.cpp
@ -164,6 +174,10 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/lib/RmlUi/Backends/RmlUi_Platform_SDL.cpp
)
if (APPLE)
list(APPEND SOURCES ${CMAKE_SOURCE_DIR}/src/main/support_apple.mm)
endif()
target_include_directories(Zelda64Recompiled PRIVATE
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/lib/N64ModernRuntime/N64Recomp/include
@ -199,6 +213,12 @@ endif()
if (MSVC)
# Disable identical code folding, since this breaks mod function patching as multiple functions can get merged into one.
target_link_options(Zelda64Recompiled PRIVATE /OPT:NOICF)
elseif (APPLE)
# Use a wrapper around ld64 that respects segprot's `max_prot` value in order
# to make our executable memory writable (required for mod function patching)
target_link_options(Zelda64Recompiled PRIVATE
"-fuse-ld=${CMAKE_SOURCE_DIR}/.github/macos/ld64"
)
endif()
if (WIN32)
@ -241,6 +261,20 @@ if (WIN32)
)
target_sources(Zelda64Recompiled PRIVATE ${CMAKE_SOURCE_DIR}/icons/app.rc)
target_link_libraries(Zelda64Recompiled PRIVATE SDL2)
endif()
if (APPLE)
find_package(SDL2 REQUIRED)
target_include_directories(Zelda64Recompiled PRIVATE ${SDL2_INCLUDE_DIRS})
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
find_package(Threads REQUIRED)
target_link_libraries(Zelda64Recompiled PRIVATE ${CMAKE_DL_LIBS} Threads::Threads SDL2::SDL2)
include(${CMAKE_SOURCE_DIR}/.github/macos/apple_bundle.cmake)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
@ -251,7 +285,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
# Generate icon_bytes.c from the app icon PNG.
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
COMMAND file_to_c ${CMAKE_SOURCE_DIR}/icons/512.png icon_bytes ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.h
DEPENDS ${CMAKE_SOURCE_DIR}/icons/512.png
)
target_sources(Zelda64Recompiled PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/icon_bytes.c)
@ -276,7 +310,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Linux")
message(STATUS "FREETYPE_LIBRARIES = ${FREETYPE_LIBRARIES}")
include_directories(${FREETYPE_LIBRARIES})
target_link_libraries(Zelda64Recompiled PRIVATE ${FREETYPE_LIBRARIES})
target_link_libraries(Zelda64Recompiled PRIVATE ${FREETYPE_LIBRARIES} SDL2::SDL2)
set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
@ -288,7 +322,6 @@ endif()
target_link_libraries(Zelda64Recompiled PRIVATE
PatchesLib
RecompiledFuncs
SDL2
librecomp
ultramodern
rt64
@ -316,9 +349,14 @@ else()
if (APPLE)
# Apple's binary is universal, so it'll work on both x86_64 and arm64
set (DXC "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-macos")
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
else()
set(SPIRVCROSS "DYLD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/spirv-cross/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64//src/contrib/spirv-cross/bin/x64/spirv-cross")
endif()
else()
if(CMAKE_SIZEOF_VOID_P EQUAL 8 AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64")
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc")
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/x64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/x64/dxc-linux")
else()
set (DXC "LD_LIBRARY_PATH=${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/lib/arm64" "${PROJECT_SOURCE_DIR}/lib/rt64/src/contrib/dxc/bin/arm64/dxc-linux")
endif()
@ -331,4 +369,3 @@ build_pixel_shader(Zelda64Recompiled "shaders/InterfacePS.hlsl" "shaders/Interfa
target_sources(Zelda64Recompiled PRIVATE ${SOURCES})
set_property(TARGET Zelda64Recompiled PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")

View File

@ -1,7 +1,7 @@
#ifndef __ZELDA_RENDER_H__
#define __ZELDA_RENDER_H__
#include <unordered_set>
#include <set>
#include <filesystem>
#include "common/rt64_user_configuration.h"
@ -32,7 +32,7 @@ namespace zelda64 {
protected:
std::unique_ptr<RT64::Application> app;
std::unordered_set<std::filesystem::path> enabled_texture_packs;
std::set<std::filesystem::path> enabled_texture_packs;
};
std::unique_ptr<ultramodern::renderer::RendererContext> create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);

19
include/zelda_support.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef __ZELDA_SUPPORT_H__
#define __ZELDA_SUPPORT_H__
#include <functional>
#include <filesystem>
namespace zelda64 {
std::filesystem::path get_asset_path(const char* asset);
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback);
void show_error_message_box(const char *title, const char *message);
// Apple specific methods that usually require Objective-C. Implemented in support_apple.mm.
#ifdef __APPLE__
void dispatch_on_ui_thread(std::function<void()> func);
const char* get_bundle_resource_directory();
#endif
}
#endif

@ -1 +1 @@
Subproject commit 0afeb089a55cb391c24352f23b7683ab3c2ca854
Subproject commit ec56fb39b0d295d9636cb60e252088d7b23c7ac9

@ -1 +1 @@
Subproject commit 0ca92eeb6c2f58ce3581c65f87f7261b8ac0fea0
Subproject commit 8efb6cc8168e746fb22d08c2dd766b2a176e1a51

View File

@ -13,6 +13,8 @@
#elif defined(__linux__)
#include <unistd.h>
#include <pwd.h>
#elif defined(__APPLE__)
#include "apple/rt64_apple.h"
#endif
constexpr std::u8string_view general_filename = u8"general.json";
@ -71,7 +73,7 @@ T from_or_default(const json& j, const std::string& key, T default_value) {
else {
ret = default_value;
}
return ret;
}
@ -129,7 +131,7 @@ namespace recomp {
}
std::filesystem::path zelda64::get_app_folder_path() {
// directly check for portable.txt (windows and native linux binary)
// directly check for portable.txt (windows and native linux binary)
if (std::filesystem::exists("portable.txt")) {
return std::filesystem::current_path();
}
@ -145,8 +147,8 @@ std::filesystem::path zelda64::get_app_folder_path() {
}
CoTaskMemFree(known_path);
#elif defined(__linux__)
// check for APP_FOLDER_PATH env var used by AppImage
#elif defined(__linux__) || defined(__APPLE__)
// check for APP_FOLDER_PATH env var
if (getenv("APP_FOLDER_PATH") != nullptr) {
return std::filesystem::path{getenv("APP_FOLDER_PATH")};
}
@ -154,7 +156,11 @@ std::filesystem::path zelda64::get_app_folder_path() {
const char *homedir;
if ((homedir = getenv("HOME")) == nullptr) {
#if defined(__linux__)
homedir = getpwuid(getuid())->pw_dir;
#elif defined(__APPLE__)
homedir = GetHomeDirectory();
#endif
}
if (homedir != nullptr) {
@ -206,7 +212,7 @@ bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::j
return recomp::finalize_output_file_with_backup(path);
}
bool save_general_config(const std::filesystem::path& path) {
bool save_general_config(const std::filesystem::path& path) {
nlohmann::json config_json{};
zelda64::to_json(config_json["targeting_mode"], zelda64::get_targeting_mode());
@ -220,7 +226,7 @@ bool save_general_config(const std::filesystem::path& path) {
config_json["analog_cam_mode"] = zelda64::get_analog_cam_mode();
config_json["analog_camera_invert_mode"] = zelda64::get_analog_camera_invert_mode();
config_json["debug_mode"] = zelda64::get_debug_mode_enabled();
return save_json_with_backups(path, config_json);
}
@ -438,7 +444,7 @@ bool save_sound_config(const std::filesystem::path& path) {
config_json["main_volume"] = zelda64::get_main_volume();
config_json["bgm_volume"] = zelda64::get_bgm_volume();
config_json["low_health_beeps"] = zelda64::get_low_health_beeps_enabled();
return save_json_with_backups(path, config_json);
}
@ -500,7 +506,7 @@ void zelda64::save_config() {
}
std::filesystem::create_directories(recomp_dir);
// TODO error handling for failing to save config files.
save_general_config(recomp_dir / general_filename);

View File

@ -25,6 +25,7 @@
#include "zelda_config.h"
#include "zelda_sound.h"
#include "zelda_render.h"
#include "zelda_support.h"
#include "zelda_game.h"
#include "ovl_patches.hpp"
#include "librecomp/game.hpp"
@ -51,7 +52,8 @@ void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
std::quick_exit(EXIT_FAILURE);
ultramodern::error_handling::quick_exit(__FILE__, __LINE__, __FUNCTION__);
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
@ -125,7 +127,9 @@ SDL_Window* window;
ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) {
uint32_t flags = SDL_WINDOW_RESIZABLE;
#if defined(RT64_SDL_WINDOW_VULKAN)
#if defined(__APPLE__)
flags |= SDL_WINDOW_METAL;
#elif defined(RT64_SDL_WINDOW_VULKAN)
flags |= SDL_WINDOW_VULKAN;
#endif
@ -151,6 +155,9 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::
return ultramodern::renderer::WindowHandle{ wmInfo.info.win.window, GetCurrentThreadId() };
#elif defined(__linux__) || defined(__ANDROID__)
return ultramodern::renderer::WindowHandle{ window };
#elif defined(__APPLE__)
SDL_MetalView view = SDL_Metal_CreateView(window);
return ultramodern::renderer::WindowHandle{ wmInfo.info.cocoa.window, SDL_Metal_GetLayer(view) };
#else
static_assert(false && "Unimplemented");
#endif

View File

@ -263,6 +263,9 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere
case ultramodern::renderer::GraphicsApi::Vulkan:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Vulkan;
break;
case ultramodern::renderer::GraphicsApi::Metal:
app->userConfig.graphicsAPI = RT64::UserConfiguration::GraphicsAPI::Metal;
break;
default:
case ultramodern::renderer::GraphicsApi::Auto:
// Don't override if auto is selected.

58
src/main/support.cpp Normal file
View File

@ -0,0 +1,58 @@
#include "zelda_support.h"
#include <SDL.h>
#include "nfd.h"
#include "RmlUi/Core.h"
namespace zelda64 {
// MARK: - Internal Helpers
void perform_file_dialog_operation(const std::function<void(bool, const std::filesystem::path&)>& callback) {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
bool success = (result == NFD_OKAY);
std::filesystem::path path;
if (success) {
path = std::filesystem::path{native_path};
NFD_FreePathN(native_path);
}
callback(success, path);
}
// MARK: - Public API
std::filesystem::path get_asset_path(const char* asset) {
std::filesystem::path base_path = "";
#if defined(__APPLE__)
const char* resource_dir = get_bundle_resource_directory();
base_path = resource_dir;
free((void*)resource_dir);
#endif
return base_path / "assets" / asset;
}
void open_file_dialog(std::function<void(bool success, const std::filesystem::path& path)> callback) {
#ifdef __APPLE__
dispatch_on_ui_thread([callback]() {
perform_file_dialog_operation(callback);
});
#else
perform_file_dialog_operation(callback);
#endif
}
void show_error_message_box(const char *title, const char *message) {
#ifdef __APPLE__
std::string title_copy(title);
std::string message_copy(message);
dispatch_on_ui_thread([title_copy, message_copy] {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title_copy.c_str(), message_copy.c_str(), nullptr);
});
#else
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, message, nullptr);
#endif
}
}

54
src/main/support_apple.mm Normal file
View File

@ -0,0 +1,54 @@
#include "zelda_support.h"
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>
#include <SDL.h>
#include "nfd.h"
namespace zelda64 {
void dispatch_on_ui_thread(std::function<void()> func) {
dispatch_async(dispatch_get_main_queue(), ^{
func();
});
}
const char* get_bundle_resource_directory() {
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
return strdup([bundlePath UTF8String]);
}
}
// Used to swizzle the updateDrawableSize method in SDL_cocoametalview to not
// automatically resize the underlying CAMetalLayer when the window size changes.
static void MySwizzleSDLMetalView(void) {
Class cls = objc_getClass("SDL_cocoametalview");
if (!cls) {
// Probably means SDL is using a different name, or the symbol is still hidden.
return;
}
SEL originalSelector = sel_registerName("updateDrawableSize");
SEL swizzledSelector = sel_registerName("my_updateDrawableSize");
Method originalMethod = class_getInstanceMethod(cls, originalSelector);
if (!originalMethod) {
// The method might not exist or might get inlined in some SDL builds.
return;
}
// Implementation of our replacement method
IMP swizzledIMP = imp_implementationWithBlock(^void(id selfObj) {
// (no-op)
});
// Swizzle method
class_addMethod(cls, swizzledSelector, swizzledIMP, method_getTypeEncoding(originalMethod));
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
__attribute__((constructor))
static void PatchSDLMetalViewConstructor() {
// This runs as soon as the dynamic library/executable is loaded, before main().
MySwizzleSDLMetalView();
}

View File

@ -4,6 +4,7 @@
#include "zelda_config.h"
#include "zelda_debug.h"
#include "zelda_render.h"
#include "zelda_support.h"
#include "promptfont.h"
#include "ultramodern/config.hpp"
#include "ultramodern/ultramodern.hpp"
@ -519,7 +520,8 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/config_menu.rml");
const std::filesystem::path asset = zelda64::get_asset_path("config_menu.rml");
return context->LoadDocument(asset.string());
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "apply_options",
@ -725,7 +727,7 @@ public:
throw std::runtime_error("Failed to make RmlUi data model for the controls config menu");
}
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = recomp::get_num_inputs(); } );
constructor.BindFunc("input_count", [](Rml::Variant& out) { out = static_cast<uint64_t>(recomp::get_num_inputs()); } );
constructor.BindFunc("input_device_is_keyboard", [](Rml::Variant& out) { out = cur_device == recomp::InputDevice::Keyboard; } );
constructor.RegisterTransformFunc("get_input_name", [](const Rml::VariantList& inputs) {

View File

@ -1,5 +1,6 @@
#include "recomp_ui.h"
#include "zelda_config.h"
#include "zelda_support.h"
#include "librecomp/game.hpp"
#include "ultramodern/ultramodern.hpp"
#include "RmlUi/Core.h"
@ -15,41 +16,36 @@ extern std::vector<recomp::GameEntry> supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
if (result == NFD_OKAY) {
std::filesystem::path path{native_path};
NFD_FreePathN(native_path);
native_path = nullptr;
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
zelda64::open_file_dialog([](bool success, const std::filesystem::path& path) {
if (success) {
recomp::RomValidationError rom_error = recomp::select_rom(path, supported_games[0].game_id);
switch (rom_error) {
case recomp::RomValidationError::Good:
mm_rom_valid = true;
model_handle.DirtyVariable("mm_rom_valid");
break;
case recomp::RomValidationError::FailedToOpen:
recompui::message_box("Failed to open ROM file.");
break;
case recomp::RomValidationError::NotARom:
recompui::message_box("This is not a valid ROM file.");
break;
case recomp::RomValidationError::IncorrectRom:
recompui::message_box("This ROM is not the correct game.");
break;
case recomp::RomValidationError::NotYet:
recompui::message_box("This game isn't supported yet.");
break;
case recomp::RomValidationError::IncorrectVersion:
recompui::message_box(
"This ROM is the correct game, but the wrong version.\nThis project requires the NTSC-U N64 version of the game.");
break;
case recomp::RomValidationError::OtherError:
recompui::message_box("An unknown error has occurred.");
break;
}
}
});
}
class LauncherMenu : public recompui::MenuController {
@ -61,7 +57,8 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
return context->LoadDocument("assets/launcher.rml");
const std::filesystem::path asset = zelda64::get_asset_path("launcher.rml");
return context->LoadDocument(asset.string());
}
void register_events(recompui::UiEventListenerInstancer& listener) override {
recompui::register_event(listener, "select_rom",

View File

@ -15,6 +15,7 @@
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "zelda_config.h"
#include "zelda_support.h"
#include "ui_rml_hacks.hpp"
#include "concurrentqueue.h"
@ -34,6 +35,9 @@
#ifdef _WIN32
# include "InterfaceVS.hlsl.dxil.h"
# include "InterfacePS.hlsl.dxil.h"
#elif defined(__APPLE__)
# include "InterfaceVS.hlsl.metal.h"
# include "InterfacePS.hlsl.metal.h"
#endif
#ifdef _WIN32
@ -43,6 +47,13 @@
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::DXIL ? std::size(name##BlobDXIL) : 0)
#elif defined(__APPLE__)
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : \
(format) == RT64::RenderShaderFormat::METAL ? name##BlobMSL : nullptr)
# define GET_SHADER_SIZE(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? std::size(name##BlobSPIRV) : \
(format) == RT64::RenderShaderFormat::METAL ? std::size(name##BlobMSL) : 0)
#else
# define GET_SHADER_BLOB(name, format) \
((format) == RT64::RenderShaderFormat::SPIRV ? name##BlobSPIRV : nullptr)
@ -1144,8 +1155,6 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
Rml::Debugger::Initialise(ui_context->rml.context);
{
const Rml::String directory = "assets/";
struct FontFace {
const char* filename;
bool fallback_face;
@ -1162,7 +1171,8 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
};
for (const FontFace& face : font_faces) {
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
auto font = zelda64::get_asset_path(face.filename);
Rml::LoadFontFace(font.string(), face.fallback_face);
}
}