From f4cfe6e4b63d81f541174b25c16339995163a0a5 Mon Sep 17 00:00:00 2001 From: David Chavez Date: Tue, 25 Feb 2025 16:54:25 +0100 Subject: [PATCH] Add macOS Support --- .github/macos/Info.plist.in | 33 ++++++++ .github/macos/MoltenVK_icd.json | 8 ++ .github/macos/entitlements.plist | 12 +++ .github/macos/fixup_bundle.cmake | 44 ++++++++++ .github/macos/macports.yaml | 14 ++++ .github/workflows/validate.yml | 71 ++++++++++++++++ .gitignore | 7 ++ CMakeLists.txt | 138 +++++++++++++++++++++++++++++-- include/zelda_render.h | 4 +- include/zelda_support.h | 14 ++++ lib/N64ModernRuntime | 2 +- lib/rt64 | 2 +- src/game/config.cpp | 10 ++- src/main/main.cpp | 12 ++- src/main/rt64_render_context.cpp | 3 + src/main/support.cpp | 10 +++ src/main/support_apple.mm | 13 +++ src/ui/elements/ui_slider.cpp | 16 +++- src/ui/ui_config.cpp | 10 ++- src/ui/ui_launcher.cpp | 73 +++++++++------- src/ui/ui_mod_menu.cpp | 10 +++ src/ui/ui_renderer.cpp | 12 ++- src/ui/ui_state.cpp | 28 +++++-- 23 files changed, 486 insertions(+), 60 deletions(-) create mode 100644 .github/macos/Info.plist.in create mode 100644 .github/macos/MoltenVK_icd.json create mode 100644 .github/macos/entitlements.plist create mode 100644 .github/macos/fixup_bundle.cmake create mode 100644 .github/macos/macports.yaml create mode 100644 include/zelda_support.h create mode 100644 src/main/support.cpp create mode 100644 src/main/support_apple.mm 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); }