diff --git a/.github/macos/Info.plist.in b/.github/macos/Info.plist.in
new file mode 100644
index 0000000..44a1a14
--- /dev/null
+++ b/.github/macos/Info.plist.in
@@ -0,0 +1,33 @@
+
+
+
+
+ CFBundleName
+ ${MACOSX_BUNDLE_BUNDLE_NAME}
+ CFBundleIdentifier
+ ${MACOSX_BUNDLE_GUI_IDENTIFIER}
+ CFBundleVersion
+ ${MACOSX_BUNDLE_BUNDLE_VERSION}
+ CFBundleShortVersionString
+ ${MACOSX_BUNDLE_SHORT_VERSION_STRING}
+ CFBundleExecutable
+ Zelda64Recompiled
+ CFBundleIconFile
+ ${MACOSX_BUNDLE_ICON_FILE}
+ LSApplicationCategoryType
+ public.app-category.games
+ CFBundlePackageType
+ APPL
+ LSMinimumSystemVersion
+ 11
+ LSEnvironment
+
+ MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS
+ 1
+ MVK_CONFIG_USE_METAL_PRIVATE_API
+ 1
+ MVK_CONFIG_RESUME_LOST_DEVICE
+ 1
+
+
+
diff --git a/.github/macos/MoltenVK_icd.json b/.github/macos/MoltenVK_icd.json
new file mode 100644
index 0000000..ffcdbd9
--- /dev/null
+++ b/.github/macos/MoltenVK_icd.json
@@ -0,0 +1,8 @@
+{
+ "file_format_version": "1.0.0",
+ "ICD": {
+ "library_path": "../../../Frameworks/libMoltenVK.dylib",
+ "api_version": "1.2.0",
+ "is_portability_driver": true
+ }
+}
\ No newline at end of file
diff --git a/.github/macos/entitlements.plist b/.github/macos/entitlements.plist
new file mode 100644
index 0000000..46f6756
--- /dev/null
+++ b/.github/macos/entitlements.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+
+
diff --git a/.github/macos/fixup_bundle.cmake b/.github/macos/fixup_bundle.cmake
new file mode 100644
index 0000000..e29a1bb
--- /dev/null
+++ b/.github/macos/fixup_bundle.cmake
@@ -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}")
\ No newline at end of file
diff --git a/.github/macos/macports.yaml b/.github/macos/macports.yaml
new file mode 100644
index 0000000..a51ccec
--- /dev/null
+++ b/.github/macos/macports.yaml
@@ -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
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index f9d9c28..70f7702 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -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 -DPATCHES_OBJCOPY=/opt/local/bin/llvm-objcopy-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
diff --git a/.gitignore b/.gitignore
index bb23653..0066fc5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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-*
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 637e8b1..bc61dcb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,10 @@
cmake_minimum_required(VERSION 3.20)
-project(Zelda64Recompiled)
+
+if (APPLE) # has to be set before the first project() or enable_language()
+ 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)
@@ -16,6 +21,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)
@@ -31,7 +40,6 @@ endif()
set(RT64_STATIC TRUE)
set(RT64_SDL_WINDOW_VULKAN TRUE)
add_compile_definitions(HLSL_CPU)
-
add_subdirectory(${CMAKE_SOURCE_DIR}/lib/rt64 ${CMAKE_BINARY_DIR}/rt64)
# set(BUILD_SHARED_LIBS_SAVED "${BUILD_SHARED_LIBS}")
@@ -110,7 +118,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
)
@@ -137,11 +145,12 @@ add_custom_target(DownloadGameControllerDB
DEPENDS ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt)
# Main executable
-add_executable(Zelda64Recompiled)
+add_executable(Zelda64Recompiled MACOSX_BUNDLE)
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
@@ -189,6 +198,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
@@ -267,6 +280,113 @@ 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})
+
+ add_compile_definitions("RT64_SDL_WINDOW_METAL")
+
+ 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)
+
+ # 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
+ )
+
+ 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 .)
+
+ # Define the path to the entitlements file
+ set(ENTITLEMENTS_FILE ${CMAKE_SOURCE_DIR}/.github/macos/entitlements.plist)
+
+ # 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=$ -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 $/Contents/Resources/assets
+ COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/temp_assets
+
+ # Copy Vulkan ICD files
+ COMMAND ${CMAKE_COMMAND} -E make_directory $/Contents/Resources/vulkan/icd.d
+ COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/.github/macOS/MoltenVK_icd.json $/Contents/Resources/vulkan/icd.d/
+
+ # Copy controller database
+ COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt $/Contents/Resources/
+
+ # Set RPATH
+ COMMAND install_name_tool -add_rpath "@executable_path/../Frameworks/" $/Contents/MacOS/Zelda64Recompiled
+
+ # Apply JIT workaround
+ COMMAND ${CMAKE_COMMAND} -E echo "Applying JIT compilation workaround"
+ COMMAND /bin/bash -c "printf '\\x07' | dd of=$ bs=1 seek=160 count=1 conv=notrunc"
+
+ # Finally sign the whole bundle with runtime option and entitlements
+ COMMAND codesign --deep --force --options runtime --sign - --entitlements ${ENTITLEMENTS_FILE} $
+
+ COMMENT "Performing post-build steps for macOS bundle"
+ VERBATIM
+ )
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")
@@ -277,7 +397,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)
@@ -302,7 +422,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)
@@ -314,7 +434,6 @@ endif()
target_link_libraries(Zelda64Recompiled PRIVATE
PatchesLib
RecompiledFuncs
- SDL2
librecomp
ultramodern
rt64
@@ -342,6 +461,11 @@ 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")
diff --git a/include/zelda_render.h b/include/zelda_render.h
index 0143471..e09cdc9 100644
--- a/include/zelda_render.h
+++ b/include/zelda_render.h
@@ -1,7 +1,7 @@
#ifndef __ZELDA_RENDER_H__
#define __ZELDA_RENDER_H__
-#include
+#include
#include
#include "common/rt64_user_configuration.h"
@@ -32,7 +32,7 @@ namespace zelda64 {
protected:
std::unique_ptr app;
- std::unordered_set enabled_texture_packs;
+ std::set enabled_texture_packs;
};
std::unique_ptr create_render_context(uint8_t *rdram, ultramodern::renderer::WindowHandle window_handle, bool developer_mode);
diff --git a/include/zelda_support.h b/include/zelda_support.h
new file mode 100644
index 0000000..42f74c2
--- /dev/null
+++ b/include/zelda_support.h
@@ -0,0 +1,14 @@
+#ifndef __ZELDA_SUPPORT_H__
+#define __ZELDA_SUPPORT_H__
+
+#include
+
+namespace zelda64 {
+ void dispatch_on_main_thread(std::function func);
+
+#ifdef __APPLE__
+ const char* get_bundle_resource_directory();
+#endif
+}
+
+#endif
diff --git a/lib/N64ModernRuntime b/lib/N64ModernRuntime
index 4c7acc6..0fa9698 160000
--- a/lib/N64ModernRuntime
+++ b/lib/N64ModernRuntime
@@ -1 +1 @@
-Subproject commit 4c7acc6eb11a662e6024105ace37242584b977f2
+Subproject commit 0fa969865f1b8cbdbcf2116a000e7ab1e2e50b57
diff --git a/lib/rt64 b/lib/rt64
index 1db8c34..3cb338b 160000
--- a/lib/rt64
+++ b/lib/rt64
@@ -1 +1 @@
-Subproject commit 1db8c347caa9dd356050777ac79a81f1ccfa462b
+Subproject commit 3cb338b321a99fd203fde86ebda9bf635bbd8f9f
diff --git a/src/game/config.cpp b/src/game/config.cpp
index ab24e0a..2a1fdd4 100644
--- a/src/game/config.cpp
+++ b/src/game/config.cpp
@@ -13,6 +13,8 @@
#elif defined(__linux__)
#include
#include
+#elif defined(__APPLE__)
+#include "common/rt64_apple.h"
#endif
constexpr std::u8string_view general_filename = u8"general.json";
@@ -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) {
diff --git a/src/main/main.cpp b/src/main/main.cpp
index b85c949..97485d4 100644
--- a/src/main/main.cpp
+++ b/src/main/main.cpp
@@ -52,7 +52,12 @@ void exit_error(const char* str, Ts ...args) {
// TODO pop up an error
((void)fprintf(stderr, str, args), ...);
assert(false);
+
+#ifdef __APPLE__
+ std::_Exit(EXIT_FAILURE);
+#else
std::quick_exit(EXIT_FAILURE);
+#endif
}
ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() {
@@ -126,7 +131,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
@@ -152,6 +159,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
diff --git a/src/main/rt64_render_context.cpp b/src/main/rt64_render_context.cpp
index 126ab70..d1339a0 100644
--- a/src/main/rt64_render_context.cpp
+++ b/src/main/rt64_render_context.cpp
@@ -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.
diff --git a/src/main/support.cpp b/src/main/support.cpp
new file mode 100644
index 0000000..4c7631d
--- /dev/null
+++ b/src/main/support.cpp
@@ -0,0 +1,10 @@
+#ifndef __APPLE__
+
+#include "zelda_support.h"
+#include
+
+void zelda64::dispatch_on_main_thread(std::function func) {
+ func();
+}
+
+#endif
\ No newline at end of file
diff --git a/src/main/support_apple.mm b/src/main/support_apple.mm
new file mode 100644
index 0000000..ceba22c
--- /dev/null
+++ b/src/main/support_apple.mm
@@ -0,0 +1,13 @@
+#include "zelda_support.h"
+#import
+
+void zelda64::dispatch_on_main_thread(std::function func) {
+ dispatch_async(dispatch_get_main_queue(), ^{
+ func();
+ });
+}
+
+const char* zelda64::get_bundle_resource_directory() {
+ NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
+ return strdup([bundlePath UTF8String]);
+}
diff --git a/src/ui/elements/ui_slider.cpp b/src/ui/elements/ui_slider.cpp
index 4cd096c..64f5907 100644
--- a/src/ui/elements/ui_slider.cpp
+++ b/src/ui/elements/ui_slider.cpp
@@ -48,17 +48,29 @@ namespace recompui {
}
void Slider::update_label_text() {
+ #if defined(__APPLE__)
+ char text_buffer[32];
+ if (type == SliderType::Double) {
+ std::snprintf(text_buffer, sizeof(text_buffer), "%.1f", value);
+ } else if (type == SliderType::Percent) {
+ std::snprintf(text_buffer, sizeof(text_buffer), "%d%%", static_cast(value));
+ } else {
+ std::snprintf(text_buffer, sizeof(text_buffer), "%d", static_cast(value));
+ }
+ value_label->set_text(text_buffer);
+ #else
char text_buffer[32];
int precision = type == SliderType::Double ? 1 : 0;
- auto result = std::to_chars(text_buffer, text_buffer + sizeof(text_buffer) - 1, value, std::chars_format::fixed, precision);
+ auto result = std::to_chars(text_buffer, text_buffer + sizeof(text_buffer) - 1,
+ value, std::chars_format::fixed, precision);
if (result.ec == std::errc()) {
if (type == SliderType::Percent) {
*result.ptr = '%';
result.ptr++;
}
-
value_label->set_text(std::string(text_buffer, result.ptr));
}
+ #endif
}
Slider::Slider(Element *parent, SliderType type) : Element(parent) {
diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp
index 97a61cc..ab6c25d 100644
--- a/src/ui/ui_config.cpp
+++ b/src/ui/ui_config.cpp
@@ -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"
@@ -443,7 +444,12 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
- config_context = recompui::create_context("assets/config_menu.rml");
+#if defined(__APPLE__)
+ const Rml::String asset = "/assets/config_menu.rml";
+ config_context = recompui::create_context(zelda64::get_bundle_resource_directory() + asset);
+#else
+ config_context = recompui::create_context("assets/config_menu.rml");
+#endif
Rml::ElementDocument* ret = config_context.get_document();
return ret;
}
@@ -651,7 +657,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(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) {
diff --git a/src/ui/ui_launcher.cpp b/src/ui/ui_launcher.cpp
index 1366267..9b5677d 100644
--- a/src/ui/ui_launcher.cpp
+++ b/src/ui/ui_launcher.cpp
@@ -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,44 @@ extern std::vector supported_games;
void select_rom() {
nfdnchar_t* native_path = nullptr;
- nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
+ zelda64::dispatch_on_main_thread([&native_path]
+ {
+ nfdresult_t result = NFD_OpenDialogN(&native_path, nullptr, 0, nullptr);
- if (result == NFD_OKAY) {
- std::filesystem::path path{native_path};
+ if (result == NFD_OKAY) {
+ std::filesystem::path path{native_path};
- NFD_FreePathN(native_path);
- native_path = nullptr;
+ 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;
+ 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;
+ }
}
- }
+ });
}
recompui::ContextId launcher_context;
@@ -68,7 +72,12 @@ public:
}
Rml::ElementDocument* load_document(Rml::Context* context) override {
(void)context;
- launcher_context = recompui::create_context("assets/launcher.rml");
+#if defined(__APPLE__)
+ const Rml::String asset = "/assets/launcher.rml";
+ launcher_context = recompui::create_context(zelda64::get_bundle_resource_directory() + asset);
+#else
+ launcher_context = recompui::create_context("assets/launcher.rml");
+#endif
Rml::ElementDocument* ret = launcher_context.get_document();
return ret;
}
diff --git a/src/ui/ui_mod_menu.cpp b/src/ui/ui_mod_menu.cpp
index 701664b..0112fd4 100644
--- a/src/ui/ui_mod_menu.cpp
+++ b/src/ui/ui_mod_menu.cpp
@@ -3,6 +3,8 @@
#include "librecomp/mods.hpp"
+#include "zelda_support.h"
+
#include
#ifdef WIN32
@@ -223,6 +225,9 @@ void ModMenu::open_mods_folder() {
#elif defined(__linux__)
std::string command = "xdg-open " + mods_directory.string() + " &";
std::system(command.c_str());
+#elif defined(__APPLE__)
+ std::string command = "open " + mods_directory.string();
+ std::system(command.c_str());
#else
static_assert(false, "Not implemented for this platform.");
#endif
@@ -540,7 +545,12 @@ ModMenu::ModMenu(Element *parent) : Element(parent) {
context.close();
+#if defined(__APPLE__)
+ const Rml::String asset = "/assets/config_sub_menu.rml";
+ sub_menu_context = recompui::create_context(zelda64::get_bundle_resource_directory() + asset);
+#else
sub_menu_context = recompui::create_context("assets/config_sub_menu.rml");
+#endif
sub_menu_context.open();
Rml::ElementDocument* sub_menu_doc = sub_menu_context.get_document();
Rml::Element* config_sub_menu_generic = sub_menu_doc->GetElementById("config_sub_menu");
diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp
index d5468f6..4c5db32 100644
--- a/src/ui/ui_renderer.cpp
+++ b/src/ui/ui_renderer.cpp
@@ -22,6 +22,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
@@ -31,6 +34,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)
@@ -236,7 +246,7 @@ public:
}
copy_command_queue_ = device->createCommandQueue(RT64::RenderCommandListType::COPY);
- copy_command_list_ = device->createCommandList(RT64::RenderCommandListType::COPY);
+ copy_command_list_ = copy_command_queue_->createCommandList(RT64::RenderCommandListType::COPY);
copy_command_fence_ = device->createCommandFence();
}
diff --git a/src/ui/ui_state.cpp b/src/ui/ui_state.cpp
index 9a95207..9785685 100644
--- a/src/ui/ui_state.cpp
+++ b/src/ui/ui_state.cpp
@@ -5,6 +5,8 @@
#include
#endif
+#include
+
#include "rt64_render_hooks.h"
#include "concurrentqueue.h"
@@ -19,6 +21,7 @@
#include "recomp_input.h"
#include "librecomp/game.hpp"
#include "zelda_config.h"
+#include "zelda_support.h"
#include "ui_rml_hacks.hpp"
#include "ui_elements.h"
#include "ui_mod_menu.h"
@@ -196,21 +199,19 @@ public:
recompui::register_custom_elements();
Rml::Initialise();
-
+
// Apply the hack to replace RmlUi's default color parser with one that conforms to HTML5 alpha parsing for SASS compatibility
recompui::apply_color_hack();
int width, height;
SDL_GetWindowSizeInPixels(window, &width, &height);
-
+
context = Rml::CreateContext("main", Rml::Vector2i(width, height));
launcher_menu_controller->make_bindings(context);
config_menu_controller->make_bindings(context);
Rml::Debugger::Initialise(context);
{
- const Rml::String directory = "assets/";
-
struct FontFace {
const char* filename;
bool fallback_face;
@@ -227,7 +228,13 @@ public:
};
for (const FontFace& face : font_faces) {
+ #if defined(__APPLE__)
+ const Rml::String directory = "/assets/";
+ Rml::LoadFontFace(zelda64::get_bundle_resource_directory() + directory + face.filename, face.fallback_face);
+ #else
+ const Rml::String directory = "assets/";
Rml::LoadFontFace(directory + face.filename, face.fallback_face);
+ #endif
}
}
}
@@ -370,7 +377,7 @@ public:
context.get_document()->Hide();
}
-
+
void hide_all_contexts() {
for (auto& context : shown_contexts) {
context.document->Hide();
@@ -424,7 +431,7 @@ inline const std::string read_file_to_string(std::filesystem::path path) {
std::ifstream stream = std::ifstream{path};
std::ostringstream ss;
ss << stream.rdbuf();
- return ss.str();
+ return ss.str();
}
void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) {
@@ -462,7 +469,7 @@ int cont_button_to_key(SDL_ControllerButtonEvent& button) {
if ((menuApplyBinding0.input_type != 0 && button.button == menuApplyBinding0.input_id) ||
(menuApplyBinding1.input_type != 0 && button.button == menuApplyBinding1.input_id)) {
return SDLK_f;
- }
+ }
// Allows closing the menu
auto menuToggleBinding0 = recomp::get_input_binding(recomp::GameInput::TOGGLE_MENU, 0, recomp::InputDevice::Controller);
@@ -583,7 +590,7 @@ void draw_hook(RT64::RenderCommandList* command_list, RT64::RenderFramebuffer* s
mouse_moved = true;
mouse_clicked = true;
break;
-
+
case SDL_EventType::SDL_CONTROLLERBUTTONDOWN: {
int rml_key = cont_button_to_key(cur_event.cbutton);
if (context_taking_input && rml_key) {
@@ -718,7 +725,10 @@ void recompui::set_render_hooks() {
}
void recompui::message_box(const char* msg) {
- SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), msg, nullptr);
+ std::string message(msg);
+ zelda64::dispatch_on_main_thread([message] {
+ SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, zelda64::program_name.data(), message.c_str(), nullptr);
+ });
printf("[ERROR] %s\n", msg);
}