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..080806f --- /dev/null +++ b/.github/macos/fixup_bundle.cmake @@ -0,0 +1,11 @@ +include(BundleUtilities) + +# Use generator expressions to get the absolute path to the bundle and frameworks +set(APPS "Zelda64Recompiled.app/Contents/MacOS/Zelda64Recompiled") +set(DIRS "Zelda64Recompiled.app/Contents/Frameworks") + +# The fixup_bundle command needs an absolute path +file(REAL_PATH ${APPS} APPS) +file(REAL_PATH ${DIRS} DIRS) + +fixup_bundle("${APPS}" "" "${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 b9f7c96..e40960d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -14,6 +14,14 @@ on: type: string required: false default: '4e6f4e52989aca69739880b40b9f988357f15d10ca03284377b81f1502463ff5' + VULKAN_SDK_VERSION: + type: string + required: false + default: '1.3.296.0' + MOLTENVK_COMMIT: + type: string + required: false + default: '' secrets: ZRE_REPO_WITH_PAT: required: true @@ -95,7 +103,7 @@ jobs: rm -rf assets/scss tar -czf Zelda64Recompiled.tar.gz Zelda64Recompiled assets/ gamecontrollerdb.txt - name: Archive Zelda64Recomp - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Zelda64Recompiled-${{ runner.os }}-X64-${{ matrix.type }} path: Zelda64Recompiled.tar.gz @@ -103,7 +111,7 @@ jobs: run: |- ./.github/linux/appimage.sh - name: Zelda64Recomp AppImage - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Zelda64Recompiled-AppImage-X64-${{ matrix.type }} path: Zelda64Recompiled-*.AppImage @@ -283,3 +291,119 @@ 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: Cache Vulkan SDK + id: cache-vulkan-sdk + uses: actions/cache@v3 + with: + path: /Users/runner/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }} + key: ${{ runner.os }}-${{ matrix.type }}-vulkan-sdk-${{ inputs.VULKAN_SDK_VERSION }} + - name: Install Vulkan SDK + if: steps.cache-vulkan-sdk.outputs.cache-hit != 'true' + run: | + wget https://sdk.lunarg.com/sdk/download/${{ inputs.VULKAN_SDK_VERSION }}/mac/vulkansdk-macos-${{ inputs.VULKAN_SDK_VERSION }}.zip + unzip vulkansdk-macos-${{ inputs.VULKAN_SDK_VERSION }}.zip + sudo InstallVulkan.app/Contents/MacOS/InstallVulkan --root ~/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }} --accept-licenses --default-answer --confirm-command install + - name: Checkout MoltenVK + if: inputs.MOLTENVK_COMMIT != '' + run: | + git clone https://github.com/KhronosGroup/MoltenVK.git + cd MoltenVK + git checkout ${{ inputs.MOLTENVK_COMMIT }} + - name: Cache MoltenVK Dependencies + if: inputs.MOLTENVK_COMMIT != '' + id: cache-mvk-dependencies + uses: actions/cache@v3 + with: + path: | + MoltenVK/External/build + !MoltenVK/External/build/Intermediates + key: ${{ runner.os }}-${{ matrix.type }}-${{ hashFiles('MoltenVK/fetchDependencies','MoltenVK/ExternalRevisions/**','MoltenVK/ExternalDependencies.xcodeproj/**','MoltenVK/Scripts/**') }} + - name: Fetch MVK Dependencies (Use Built Cache) + if: inputs.MOLTENVK_COMMIT != '' && steps.cache-mvk-dependencies.outputs.cache-hit == 'true' + run: | + cd MoltenVK + ./fetchDependencies -v --none + - name: Fetch MVK Dependencies + if: inputs.MOLTENVK_COMMIT != '' && steps.cache-mvk-dependencies.outputs.cache-hit != 'true' + run: | + cd MoltenVK + ./fetchDependencies -v --macos + - name: Build MoltenVK + if: inputs.MOLTENVK_COMMIT != '' + run: | + cd MoltenVK + make macos + sudo cp Package/Latest/MoltenVK/dylib/macOS/libMoltenVK.dylib ~/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }}/macOS/lib/libMoltenVK.dylib + - 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) + env: + VULKAN_SDK: /Users/runner/VulkanSDK/${{ inputs.VULKAN_SDK_VERSION }}/macOS + - 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..e60a33e 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b900bc..bf0903e 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) @@ -15,6 +20,30 @@ if (WIN32) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_DISABLE_CONSTEXPR_MUTEX_CONSTRUCTOR") endif() +if (APPLE) + enable_language(OBJC OBJCXX) + + # Check if VULKAN_SDK environment variable is set + if(NOT DEFINED ENV{VULKAN_SDK}) + message(FATAL_ERROR "VULKAN_SDK environment variable is not set.") + else() + set(VULKAN_SDK $ENV{VULKAN_SDK}) + endif() + + # Define paths to Vulkan loader and MoltenVK libraries + set(VULKAN_LOADER_PATH "${VULKAN_SDK}/lib/libvulkan.dylib") + set(MOLTENVK_PATH "${VULKAN_SDK}/lib/libMoltenVK.dylib") + + # Ensure the Vulkan loader and MoltenVK libraries exist + if(NOT EXISTS "${VULKAN_LOADER_PATH}") + message(FATAL_ERROR "Vulkan loader not found at ${VULKAN_LOADER_PATH}") + endif() + + if(NOT EXISTS "${MOLTENVK_PATH}") + message(FATAL_ERROR "MoltenVK not found at ${MOLTENVK_PATH}") + endif() +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) @@ -135,11 +164,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 @@ -164,6 +194,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/concurrentqueue @@ -241,6 +275,114 @@ 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) + + # 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 .) + + # Copy required frameworks to bundle + target_link_libraries(Zelda64Recompiled PRIVATE ${MOLTENVK_PATH} ${VULKAN_LOADER_PATH}) + + # 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} -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 --sign - --entitlements ${ENTITLEMENTS_FILE} $ + + COMMENT "Performing post-build steps for macOS bundle" + VERBATIM + ) endif() if (CMAKE_SYSTEM_NAME MATCHES "Linux") @@ -274,7 +416,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) @@ -286,7 +428,6 @@ endif() target_link_libraries(Zelda64Recompiled PRIVATE PatchesLib RecompiledFuncs - SDL2 librecomp ultramodern rt64 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 d5c81d0..3e39c2e 160000 --- a/lib/N64ModernRuntime +++ b/lib/N64ModernRuntime @@ -1 +1 @@ -Subproject commit d5c81d0a6bf2e5f36747a095a7a060d7623bbf58 +Subproject commit 3e39c2ec34340e245a5b7e353560b5a793b9990b diff --git a/lib/rt64 b/lib/rt64 index 67422c3..d4bc18b 160000 --- a/lib/rt64 +++ b/lib/rt64 @@ -1 +1 @@ -Subproject commit 67422c3b647058d3d38f2813a2abe79cf1638f13 +Subproject commit d4bc18b84855938ed7949cecbda918bca37591c1 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 bf13a28..6282d69 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -25,6 +25,7 @@ #include "zelda_config.h" #include "zelda_sound.h" #include "zelda_render.h" +#include "zelda_support.h" #include "ovl_patches.hpp" #include "librecomp/game.hpp" #include "librecomp/mods.hpp" @@ -44,7 +45,7 @@ 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_QUICK_EXIT(); } ultramodern::gfx_callbacks_t::gfx_data_t create_gfx() { @@ -118,7 +119,12 @@ bool SetImageAsIcon(const char* filename, SDL_Window* window) SDL_Window* window; ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t::gfx_data_t) { - window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, SDL_WINDOW_RESIZABLE ); + uint32_t window_flags = SDL_WINDOW_RESIZABLE; +#ifdef __APPLE__ + window_flags |= SDL_WINDOW_METAL; +#endif + + window = SDL_CreateWindow("Zelda 64: Recompiled", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1600, 960, window_flags ); #if defined(__linux__) SetImageAsIcon("icons/512.png",window); if (ultramodern::renderer::get_graphics_config().wm_option == ultramodern::renderer::WindowMode::Fullscreen) { // TODO: Remove once RT64 gets native fullscreen support on Linux @@ -146,6 +152,9 @@ ultramodern::renderer::WindowHandle create_window(ultramodern::gfx_callbacks_t:: } return ultramodern::renderer::WindowHandle{ wmInfo.info.x11.display, wmInfo.info.x11.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 @@ -584,8 +593,13 @@ int main(int argc, char** argv) { SDL_InitSubSystem(SDL_INIT_AUDIO); reset_audio(48000); - // Source controller mappings file - if (SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt") < 0) { +#if defined(__APPLE__) + const char* resource_directory = zelda64::get_bundle_resource_directory(); + std::string mapping_file_path = std::string(resource_directory) + "gamecontrollerdb.txt"; +#else + std::string mapping_file_path = "gamecontrollerdb.txt"; +#endif + if (SDL_GameControllerAddMappingsFromFile(mapping_file_path.c_str()) < 0) { fprintf(stderr, "Failed to load controller mappings: %s\n", SDL_GetError()); } diff --git a/src/main/rt64_render_context.cpp b/src/main/rt64_render_context.cpp index e800d99..40d331a 100644 --- a/src/main/rt64_render_context.cpp +++ b/src/main/rt64_render_context.cpp @@ -207,7 +207,7 @@ zelda64::renderer::RT64Context::RT64Context(uint8_t* rdram, ultramodern::rendere appCore.window.window = window_handle.window; #elif defined(__APPLE__) appCore.window.window = window_handle.window; - appCore.window.view = window_handle.view; + appCore.window.layer = window_handle.view; #endif appCore.checkInterrupts = dummy_check_interrupts; 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/ui_config.cpp b/src/ui/ui_config.cpp index 12bb20e..18158cd 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -2,6 +2,7 @@ #include "recomp_input.h" #include "zelda_sound.h" #include "zelda_config.h" +#include "zelda_support.h" #include "zelda_debug.h" #include "zelda_render.h" #include "promptfont.h" @@ -519,6 +520,11 @@ public: } Rml::ElementDocument* load_document(Rml::Context* context) override { +#if defined(__APPLE__) + const Rml::String asset = "/assets/config_menu.rml"; + return context->LoadDocument(zelda64::get_bundle_resource_directory() + asset); +#endif + return context->LoadDocument("assets/config_menu.rml"); } void register_events(recompui::UiEventListenerInstancer& listener) override { @@ -725,7 +731,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 f699eb7..c876674 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,43 @@ 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; + } } - } + }); } class LauncherMenu : public recompui::MenuController { @@ -61,6 +64,11 @@ public: } Rml::ElementDocument* load_document(Rml::Context* context) override { +#if defined(__APPLE__) + const Rml::String asset = "/assets/launcher.rml"; + return context->LoadDocument(zelda64::get_bundle_resource_directory() + asset); +#endif + return context->LoadDocument("assets/launcher.rml"); } void register_events(recompui::UiEventListenerInstancer& listener) override { diff --git a/src/ui/ui_renderer.cpp b/src/ui/ui_renderer.cpp index a234d7c..a93d7ad 100644 --- a/src/ui/ui_renderer.cpp +++ b/src/ui/ui_renderer.cpp @@ -16,6 +16,7 @@ #include "librecomp/game.hpp" #include "zelda_config.h" #include "ui_rml_hacks.hpp" +#include "zelda_support.h" #include "concurrentqueue.h" @@ -1144,8 +1145,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 +1161,13 @@ void init_hook(RT64::RenderInterface* interface, RT64::RenderDevice* device) { }; 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 } } @@ -1506,6 +1511,9 @@ recompui::Menu recompui::get_current_menu() { } 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); }