From 4e8e335b7bed1600c4c4fb833fd8326b7a7df718 Mon Sep 17 00:00:00 2001 From: nacimoualla Date: Sat, 29 Aug 2026 14:08:54 +0100 Subject: [PATCH] =?UTF-8?q?macOS:=20full=20support=20=E2=80=94=20MoltenVK?= =?UTF-8?q?=20auto-discovery,=20portability=20fix,=20and=20KytyPS5.app=20b?= =?UTF-8?q?undle=20(#394)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * macOS: auto-discover bundled MoltenVK on startup On macOS the Vulkan loader comes from MoltenVK. SDL's default probe only searches system paths, so a bundled libMoltenVK.dylib next to the executable is invisible under the hardened runtime. Probe it explicitly via SDL_GetBasePath() before falling back to SDL's default search. Respect SDL_VULKAN_LIBRARY overrides and log which loader is used. Update README to reflect that no environment variable is required for bundled builds. Co-Authored-By: internal-model * window: fix Vulkan portability enumeration on macOS MoltenVK requires VK_KHR_portability_enumeration and VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR to surface portability devices via enumeratePhysicalDevices. Without the flag the adapter is hidden on some driver versions, causing "Could not find suitable device" failures. Add the instance extension and flag when available, and add diagnostic logs for all MoltenVK fallbacks (depthClipEnable, colorWriteEnable, depthBounds, fragmentShaderBarycentric) so black-screen diagnostics are actionable. * macOS: add KytyPS5.app bundle packaging Add a proper macOS application bundle when the launcher is built. launcher becomes KytyPS5.app (CFBundleIdentifier org.kytyps5.kytyps5) with KytyPS5 as the executable, and kyty_emulator is installed as a helper inside Contents/MacOS so QCoreApplication::applicationDirPath() finds it. MoltenVK is auto-discovered via SDL_GetBasePath() in that directory, so no SDL_VULKAN_LIBRARY is required. - src/launcher/Info.plist.in: minimal bundle plist (version from PROJECT_VERSION, LSMinimumSystemVersion 13.0). - src/launcher/CMakeLists.txt: set MACOSX_BUNDLE, BUNDLE DESTINATION, install helper, re-sign helper with JIT entitlements, and invoke macdeployqt when available. Keep flat kyty_emulator for CLI. - .github/workflows/build.yml: bundle MoltenVK into KytyPS5.app/Contents/MacOS when present, verify bundle artifacts (file/lipo/codesign), and handle bundle paths in the release zip. - README.md: document bundle double-click and MoltenVK locations. * launcher: fix bundle rpath for Qt frameworks The bundle executable had @rpath references to Qt frameworks but no LC_RPATH, causing dyld to abort at launch: Library not loaded: @rpath/QtConcurrent.framework Reason: no LC_RPATH's found Set INSTALL_RPATH to @executable_path/../Frameworks and BUILD_WITH_INSTALL_RPATH so the built and installed bundles resolve Qt from Contents/Frameworks. Verified via otool -l (LC_RPATH now present) and macdeployqt now succeeds without "Cannot resolve rpath" errors. * launcher: add Import game button Add a prominent Import button to ConfigurationListWidget that lets users select a game folder (containing eboot.bin) or a folder containing multiple games via QFileDialog. Handles: - Single game folder selected: adds its parent to game_dirs (Scan searches subdirectories, so parent is needed) - Folder with multiple games: adds the selected folder - Deduplication via PathKey/NormalizeGameDirectories, writes settings, rescans, and selects the imported game if found - Shows info/warning dialogs when no games are found UI: QToolButton with SP_DirOpenIcon and text Import beside the existing toolbar, styled with a blue background for visibility. Logic in ConfigurationListWidget::import_game(). * macOS: fix bundled loader and artifact packaging * Revert "launcher: add Import game button" This reverts commit 2f670ef95c7f412f2b38284ed1d8711aaa208f11. --------- Co-authored-by: nmzik --- .github/workflows/build.yml | 69 +++++++++++++------ README.md | 29 ++++++-- .../presentation/window/vulkanWindow.cpp | 12 ++++ src/graphics/presentation/window/window.cpp | 22 +++++- src/launcher/CMakeLists.txt | 38 +++++++++- src/launcher/Info.plist.in | 17 +++++ 6 files changed, 155 insertions(+), 32 deletions(-) create mode 100644 src/launcher/Info.plist.in 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 + +