diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76a2b313..fa2df2ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -208,34 +208,66 @@ jobs: run: | archive="$RUNNER_TEMP/MoltenVK-macos.tar" package="$RUNNER_TEMP/MoltenVK" + install_dir="_Build/macos/install" + app_dir="$install_dir/KytyPS5.app" + moltenvk="$install_dir/libMoltenVK.dylib" curl --fail --location --retry 3 \ --output "$archive" \ "https://github.com/KhronosGroup/MoltenVK/releases/download/$MOLTENVK_VERSION/MoltenVK-macos.tar" echo "$MOLTENVK_SHA256 $archive" | shasum -a 256 --check tar -xf "$archive" -C "$RUNNER_TEMP" + test -d "$app_dir" install -m 755 \ "$package/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib" \ - _Build/macos/install/libMoltenVK.dylib - install -m 644 "$package/LICENSE" _Build/macos/install/LICENSE.MoltenVK - codesign --force --sign - --timestamp=none _Build/macos/install/libMoltenVK.dylib + "$moltenvk" + install -m 644 "$package/LICENSE" "$install_dir/LICENSE.MoltenVK" + codesign --force --sign - --timestamp=none "$moltenvk" + install -d -m 755 "$app_dir/Contents/Frameworks" "$app_dir/Contents/Resources" + install -m 755 "$moltenvk" "$app_dir/Contents/Frameworks/libMoltenVK.dylib" + install -m 644 "$package/LICENSE" "$app_dir/Contents/Resources/LICENSE.MoltenVK" + codesign --force --sign - --timestamp=none "$app_dir" - name: Verify artifacts shell: bash run: | - file _Build/macos/install/launcher - file _Build/macos/install/kyty_emulator - file _Build/macos/install/libMoltenVK.dylib - lipo _Build/macos/install/launcher -verify_arch x86_64 - lipo _Build/macos/install/kyty_emulator -verify_arch x86_64 - lipo _Build/macos/install/libMoltenVK.dylib -verify_arch x86_64 - codesign --verify --strict _Build/macos/install/kyty_emulator - codesign --verify --strict _Build/macos/install/libMoltenVK.dylib + install_dir="_Build/macos/install" + app_dir="$install_dir/KytyPS5.app" + test -d "$app_dir" + for binary in \ + "$install_dir/kyty_emulator" \ + "$install_dir/libMoltenVK.dylib" \ + "$app_dir/Contents/MacOS/KytyPS5" \ + "$app_dir/Contents/MacOS/kyty_emulator" \ + "$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do + file "$binary" + lipo "$binary" -verify_arch x86_64 + done + for binary in \ + "$install_dir/kyty_emulator" \ + "$install_dir/libMoltenVK.dylib" \ + "$app_dir/Contents/MacOS/KytyPS5" \ + "$app_dir/Contents/MacOS/kyty_emulator" \ + "$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do + codesign --verify --strict "$binary" + done + codesign --verify --deep --strict --verbose=2 "$app_dir" + + - name: Package macOS artifacts + shell: bash + run: | + install_dir="_Build/macos/install" + archive="$RUNNER_TEMP/KytyPS5-macOS-x86_64.zip" + ( + cd "$install_dir" + zip -qry "$archive" . + ) + unzip -t "$archive" - name: Upload macOS artifacts uses: actions/upload-artifact@v4 with: name: KytyPS5-macOS-x86_64 - path: _Build/macos/install/** + path: ${{ runner.temp }}/KytyPS5-macOS-x86_64.zip if-no-files-found: error linux: @@ -424,16 +456,9 @@ jobs: if: needs.macos.result == 'success' shell: bash run: | - macos_dir="artifacts/KytyPS5-macOS-x86_64" - test -d "$macos_dir" - chmod a+x \ - "$macos_dir/launcher" \ - "$macos_dir/kyty_emulator" \ - "$macos_dir/libMoltenVK.dylib" - ( - cd "$macos_dir" - zip -r "$GITHUB_WORKSPACE/$RELEASE_NAME-macOS-x86_64.zip" . - ) + macos_archive="artifacts/KytyPS5-macOS-x86_64/KytyPS5-macOS-x86_64.zip" + test -f "$macos_archive" + cp "$macos_archive" "$RELEASE_NAME-macOS-x86_64.zip" - name: Package Linux build if: needs.linux.result == 'success' diff --git a/README.md b/README.md index 97572d21..22476341 100644 --- a/README.md +++ b/README.md @@ -220,17 +220,23 @@ cmake --install _Build/macos --prefix _Build/macos/install ``` The build re-signs `kyty_emulator` with the JIT entitlements it needs to execute translated -guest code; no manual signing step is required. +guest code; no manual signing step is required. When the launcher is built, the install +also produces `_Build/macos/install/KytyPS5.app` — double-click to launch the GUI. +A flat `kyty_emulator` is kept for CLI usage. Vulkan comes from MoltenVK. Download `MoltenVK-macos.tar` from the [MoltenVK releases](https://github.com/KhronosGroup/MoltenVK/releases), then copy -`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to `kyty_emulator` and ad-hoc sign it: +`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to the flat `kyty_emulator` +(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and ad-hoc sign it: ```bash codesign --force --sign - _Build/macos/install/libMoltenVK.dylib +# For the bundle (if present): +codesign --force --sign - _Build/macos/install/KytyPS5.app/Contents/Frameworks/libMoltenVK.dylib +codesign --force --sign - _Build/macos/install/KytyPS5.app ``` -Release archives already include a signed `libMoltenVK.dylib`. +Release archives already include a signed `libMoltenVK.dylib` (both flat and inside the bundle). ### Regression tests @@ -274,6 +280,10 @@ To use the graphical launcher: ./_Build/linux/install/launcher ``` +```bash +open _Build/macos/install/KytyPS5.app # or double-click in Finder +``` + On first launch, add one or more game folders in the global settings. The launcher searches those folders recursively for game directories containing `eboot.bin`. Select a detected game and run it from the game list. @@ -288,12 +298,17 @@ The emulator can also be started directly with a legally obtained game directory ./_Build/linux/install/kyty_emulator --game "/games/ExampleGame" ``` -On macOS, point SDL at the MoltenVK library explicitly; the hardened runtime prevents it from -being picked up from the executable's directory: +On macOS, the adjacent flat or app-bundled `libMoltenVK.dylib` is found automatically; no +environment variable is required: ```bash -cd _Build/macos/install -SDL_VULKAN_LIBRARY="$PWD/libMoltenVK.dylib" ./kyty_emulator --game "/games/ExampleGame" +./_Build/macos/install/kyty_emulator --game "/games/ExampleGame" +``` + +To override the Vulkan loader, set `SDL_VULKAN_LIBRARY`: + +```bash +SDL_VULKAN_LIBRARY=/path/to/libMoltenVK.dylib ./kyty_emulator --game "/games/ExampleGame" ``` Run `kyty_emulator --help` to see the available graphics, logging, validation, profiling, and diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 6d1e5df6..1ebdc831 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -942,6 +942,18 @@ void WindowContext::CreateVulkan() { inst_info.sType = vk::StructureType::eInstanceCreateInfo; inst_info.pNext = (r.enable_validation_layers ? &dbg_create_info : nullptr); inst_info.flags = {}; +#if defined(__APPLE__) + // MoltenVK requires VK_KHR_portability_enumeration + flag to surface + // portability devices. Without this, enumeratePhysicalDevices hides the + // MoltenVK adapter on some driver versions. + if (HasExtension(r.available_extensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) { + if (!HasExtension(r.required_extensions, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME)) { + r.required_extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + } + inst_info.flags |= vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR; + LOGF("Vulkan instance: enabled %s\n", VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + } +#endif inst_info.pApplicationInfo = &app_info; inst_info.enabledExtensionCount = static_cast(r.required_extensions.size()); inst_info.ppEnabledExtensionNames = r.required_extensions.data(); diff --git a/src/graphics/presentation/window/window.cpp b/src/graphics/presentation/window/window.cpp index 559e3eb9..7378de74 100644 --- a/src/graphics/presentation/window/window.cpp +++ b/src/graphics/presentation/window/window.cpp @@ -3,6 +3,7 @@ #include "SDL.h" #include "SDL_error.h" #include "SDL_events.h" +#include "SDL_filesystem.h" #include "SDL_gamecontroller.h" #include "SDL_hints.h" #include "SDL_joystick.h" @@ -40,13 +41,12 @@ #include "loader/systemContent.h" #include +#include #include #include #include #include -#include - #define STB_IMAGE_IMPLEMENTATION #define STBI_NO_SIMD #include "stb_image.h" @@ -812,6 +812,24 @@ static void WindowCreate(WindowContext& context) { uint32_t window_flags = WindowContext::InitialWindowFlags(Config::FullscreenEnabled()); #if defined(__APPLE__) + // SDL loads Vulkan while creating a Vulkan window, so select the bundled + // MoltenVK loader before calling SDL_CreateWindow. Keep an explicit user + // override, and fall back to SDL's normal loader search when no bundle is present. + if (std::getenv("SDL_VULKAN_LIBRARY") == nullptr) { + if (char* base_path = SDL_GetBasePath(); base_path != nullptr) { + const std::string base_path_str = base_path; + SDL_free(base_path); + std::string moltenvk_path = base_path_str + "libMoltenVK.dylib"; + if (!Common::File::IsFileExisting(moltenvk_path)) { + moltenvk_path = base_path_str + "../Frameworks/libMoltenVK.dylib"; + } + if (Common::File::IsFileExisting(moltenvk_path) && + SDL_setenv("SDL_VULKAN_LIBRARY", moltenvk_path.c_str(), 0) == 0) { + LOGF("Vulkan loader: %s\n", moltenvk_path.c_str()); + } + } + } + // macOS 26 window chrome (CoreUI asset decode, SwiftUI titlebar) has been observed // throwing NSExceptions under Rosetta during the first CATransaction commit. A // borderless window skips that machinery entirely. diff --git a/src/launcher/CMakeLists.txt b/src/launcher/CMakeLists.txt index 45b2bbbf..e30ef61a 100644 --- a/src/launcher/CMakeLists.txt +++ b/src/launcher/CMakeLists.txt @@ -66,10 +66,46 @@ if(WIN32) endif() set(launcher_name "launcher") +if(APPLE) + set(launcher_name "KytyPS5") +endif() set_target_properties(launcher PROPERTIES OUTPUT_NAME ${launcher_name}) -install(TARGETS launcher DESTINATION .) +if(APPLE) + # macOS bundle: KytyPS5.app with launcher as main executable and + # kyty_emulator as helper inside Contents/MacOS. MoltenVK is found + # next to a flat executable or in the bundle's Contents/Frameworks; + # no rpath fixup is needed because SDL loads it by absolute path. + # Qt deployment (Frameworks + plugins) is handled by macdeployqt. + set_target_properties(launcher PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in" + INSTALL_RPATH "@executable_path/../Frameworks" + BUILD_WITH_INSTALL_RPATH TRUE + ) +endif() + +install(TARGETS launcher + BUNDLE DESTINATION . + RUNTIME DESTINATION .) + +if(APPLE) + install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS") + # Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle. + find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH) + if(NOT KYTY_MACDEPLOYQT) + message(FATAL_ERROR "Could not find macdeployqt") + endif() + install(CODE " + execute_process(COMMAND \"${KYTY_MACDEPLOYQT}\" \"\${CMAKE_INSTALL_PREFIX}/KytyPS5.app\" + -codesign=- + RESULT_VARIABLE _kyty_deploy_result) + if(NOT _kyty_deploy_result EQUAL 0) + message(FATAL_ERROR \"macdeployqt failed with \${_kyty_deploy_result}\") + endif() + ") +endif() if(WIN32) install(CODE " diff --git a/src/launcher/Info.plist.in b/src/launcher/Info.plist.in new file mode 100644 index 00000000..74f2e05a --- /dev/null +++ b/src/launcher/Info.plist.in @@ -0,0 +1,17 @@ + + + + + CFBundleNameKytyPS5 + CFBundleDisplayNameKytyPS5 + CFBundleIdentifierorg.kytyps5.kytyps5 + CFBundleVersion@PROJECT_VERSION@ + CFBundleShortVersionString@PROJECT_VERSION@ + CFBundlePackageTypeAPPL + CFBundleSignature???? + CFBundleExecutableKytyPS5 + LSMinimumSystemVersion13.0 + NSHighResolutionCapable + NSRequiresAquaSystemAppearance + +