aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMako <137927095+Takoroka@users.noreply.github.com>2026-11-01 10:18:27 -0500
committerGitHub <noreply@github.com>2026-11-01 10:18:27 -0500
commit89dd760cc44aec0a9401809f3837917645d8af25 (patch)
tree41d6ea03c7be56feb0b2573ab199a43c7c9ab5e0
parentadae7635708e155d967e84d7ad84bf63c5203409 (diff)
parent76adfd3e9508bf685b0dc5354d6821148139cef1 (diff)
Merge pull request #143 from Sssnowyyy/macos
macOS (Apple Silicon) build fixes and release builds
-rw-r--r--.github/workflows/release-binaries.yml100
-rw-r--r--dist/eglantine.icnsbin0 -> 620877 bytes
-rw-r--r--externals/cmake-modules/DetectArchitecture.cmake13
-rw-r--r--src/common/CMakeLists.txt2
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp4
-rw-r--r--src/video_core/texture_cache/texture_cache.h4
-rw-r--r--src/yuzu/Info.plist2
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.cpp14
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.h6
-rw-r--r--src/yuzu_cmd/yuzu.cpp3
10 files changed, 140 insertions, 8 deletions
diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index 8835844060..20aa57f7aa 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -67,6 +67,8 @@ jobs:
- platform: linux-aarch64-pgo
runner: ubuntu-24.04-arm
pgo: true
+ - platform: macos-arm64
+ runner: macos-15
steps:
- name: Check out source
@@ -87,6 +89,17 @@ jobs:
libboost-dev libboost-fiber-dev libboost-context-dev \
libopus-dev libasound2-dev ccache
+ # The packages docs/Deps.md lists for macOS, plus ccache.
+ - name: Install macOS dependencies
+ if: startsWith(matrix.platform, 'macos')
+ shell: bash
+ env:
+ HOMEBREW_NO_INSTALL_CLEANUP: 1
+ run: |
+ brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja \
+ nlohmann-json openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake catch2 molten-vk \
+ vulkan-loader spirv-tools vulkan-utility-libraries ccache
+
- name: Install Qt on Linux x86_64
if: startsWith(matrix.platform, 'linux') && runner.arch != 'ARM64'
uses: jurplel/install-qt-action@v4
@@ -266,6 +279,27 @@ jobs:
-DCMAKE_C_COMPILER_LAUNCHER=sccache
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
+ - name: Configure macOS
+ if: startsWith(matrix.platform, 'macos')
+ shell: bash
+ run: |
+ set -euo pipefail
+ cmake -S . -B build/release -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBUILD_TESTING=OFF \
+ -DYUZU_TESTS=OFF \
+ -DYUZU_CMD=ON \
+ -DYUZU_ROOM=OFF \
+ -DYUZU_ROOM_STANDALONE=OFF \
+ -DYUZU_USE_BUNDLED_QT=OFF \
+ "-DCMAKE_PREFIX_PATH=$(brew --prefix qt@6);$(brew --prefix zlib)" \
+ "-DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" \
+ -DVulkanHeaders_FORCE_BUNDLED=ON \
+ -DVulkanUtilityLibraries_FORCE_BUNDLED=ON \
+ -DENABLE_UPDATE_CHECKER=OFF \
+ -DUSE_DISCORD_PRESENCE=OFF \
+ -DUSE_CCACHE=ON
+
# MinGW build: system Qt and libraries from MSYS2, clang plus Eden's PGO profile. The profile
# path is converted to its Windows form because clang runs with the MSYS mount underneath.
- name: Configure MinGW PGO
@@ -411,6 +445,58 @@ jobs:
deps "$dll"
done
+ # A self-contained app bundle: macdeployqt copies Qt and every other Homebrew library both
+ # programs load into the bundle, then the leftover Homebrew search paths are removed so a
+ # machine with Homebrew still uses the bundled copies. Ad-hoc signed, not notarized.
+ - name: Package macOS app
+ if: startsWith(matrix.platform, 'macos')
+ shell: bash
+ run: |
+ set -euo pipefail
+ app=build/release/bin/eglantine.app
+ test -x "$app/Contents/MacOS/eglantine"
+ test -x build/release/bin/eden-cli
+ cp build/release/bin/eden-cli "$app/Contents/MacOS/eden-cli"
+ "$(brew --prefix qt@6)/bin/macdeployqt" "$app" \
+ -executable="$app/Contents/MacOS/eden-cli" -libpath="$(brew --prefix)/lib" \
+ -always-overwrite -verbose=1
+ # Qt's virtual keyboard input plugin pulls in the QML stack, which the emulator doesn't use.
+ rm -f "$app/Contents/PlugIns/platforminputcontexts/libqtvirtualkeyboardplugin.dylib"
+ binaries=$(find "$app/Contents" -type f \( -perm -u+x -o -name '*.dylib' \) \
+ -exec sh -c 'file -b "$1" | grep -q Mach-O' _ {} \; -print)
+ for binary in $binaries; do
+ otool -l "$binary" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' |
+ { grep -E '^/(opt/homebrew|usr/local)/' || true; } | while read -r rpath; do
+ install_name_tool -delete_rpath "$rpath" "$binary" 2> /dev/null
+ done
+ done
+ # Libraries that load their dependencies through @rpath find them in the bundle.
+ for program in eglantine eden-cli; do
+ install_name_tool -add_rpath @executable_path/../Frameworks \
+ "$app/Contents/MacOS/$program" 2> /dev/null
+ done
+ leaks=$(for binary in $binaries; do
+ otool -L "$binary" | grep -E '^\s+/(opt/homebrew|usr/local)/'
+ otool -l "$binary" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' |
+ grep -E '^/(opt/homebrew|usr/local)/'
+ done || true)
+ if [ -n "$leaks" ]; then
+ echo "References still pointing outside the bundle:"
+ echo "$leaks" | sort -u
+ exit 1
+ fi
+ # macdeployqt invalidates the signatures. The programs get the entitlements when the
+ # tree has them (the Hypervisor CPU backend needs com.apple.security.hypervisor).
+ codesign --force --deep --sign - "$app"
+ if [ -f dist/eglantine.entitlements ]; then
+ codesign --force --sign - --entitlements dist/eglantine.entitlements \
+ "$app/Contents/MacOS/eden-cli"
+ codesign --force --sign - --entitlements dist/eglantine.entitlements "$app"
+ fi
+ codesign --verify --deep --strict "$app"
+ ditto -c -k --keepParent "$app" "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.zip"
+ ls -la "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.zip"
+
# The artifact is the bin folder itself: GitHub zips artifacts on download, and uploading a
# zip gave a zip inside a zip. The release job zips it once for the release.
- name: Upload Windows binary
@@ -444,6 +530,17 @@ jobs:
compression-level: 0
retention-days: 7
+ # Zipped with ditto beforehand: the bundle's framework symlinks don't survive artifact zipping.
+ - name: Upload macOS app
+ if: startsWith(matrix.platform, 'macos')
+ uses: actions/upload-artifact@v4
+ with:
+ name: eglantine-${{ matrix.platform }}-${{ github.sha }}
+ path: eglantine-${{ github.sha }}-${{ matrix.platform }}.zip
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 7
+
# Publishes every binary as a GitHub pre-release once every platform built: on pushes to nx2
# and nx2-experimental, and on manual runs unless "Publish the binaries as a GitHub release" is
# unticked.
@@ -471,6 +568,8 @@ jobs:
if compgen -G "$artifact*.AppImage" > /dev/null; then
# Linux: ship the AppImage and the tar.gz as they are.
cp "$artifact"/*.AppImage "$artifact"/*.tar.gz dist/
+ elif compgen -G "$artifact*.zip" > /dev/null; then
+ cp "$artifact"/*.zip dist/
else
(cd "$artifact" && zip -qr "${GITHUB_WORKSPACE}/dist/${name}.zip" .)
fi
@@ -492,6 +591,7 @@ jobs:
Windows: extract the zip and run eglantine.exe, or eden-cli.exe from a terminal.
Linux (AppImage): make it executable (chmod +x) and run it; it bundles its libraries and needs glibc 2.39 or newer (Ubuntu 24.04, Fedora 40, Debian 13, Arch).
Linux (tar.gz): extract it and run ./eglantine or ./eden-cli; it uses your system libraries, so Qt 6 and the usual libraries must be installed.
+ macOS: Apple Silicon and macOS 15 or newer. Unzip and move eglantine.app to Applications; eden-cli is inside it, in Contents/MacOS. The app is not notarized, so the first time right-click it and choose Open (or run xattr -dr com.apple.quarantine eglantine.app).
The -pgo builds are the same programs compiled with profile-guided optimization (clang plus Eden's profile); prefer them when your CPU supports it. The Windows PGO build is a MinGW build, so it carries its own DLLs and needs no Visual C++ redistributable."
- name: Ping Discord
diff --git a/dist/eglantine.icns b/dist/eglantine.icns
new file mode 100644
index 0000000000..812334cd28
--- /dev/null
+++ b/dist/eglantine.icns
Binary files differ
diff --git a/externals/cmake-modules/DetectArchitecture.cmake b/externals/cmake-modules/DetectArchitecture.cmake
index 105963c8c2..be84109994 100644
--- a/externals/cmake-modules/DetectArchitecture.cmake
+++ b/externals/cmake-modules/DetectArchitecture.cmake
@@ -36,12 +36,17 @@ This file is based off of Yuzu and Dynarmic.
# should NOT be too dependent on the architecture
# otherwise, you may end up with duplicate code
if (CMAKE_OSX_ARCHITECTURES)
- set(MULTIARCH_BUILD 1)
+ list(LENGTH CMAKE_OSX_ARCHITECTURES architecture_count)
+ if (architecture_count GREATER 1)
+ set(MULTIARCH_BUILD 1)
+ else()
+ set(MULTIARCH_BUILD 0)
+ endif()
set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}")
# hope and pray the architecture names match
- foreach(ARCH IN ${CMAKE_OSX_ARCHITECTURES})
- set(ARCHITECTURE_${ARCH} 1 PARENT_SCOPE)
+ foreach(ARCH IN LISTS CMAKE_OSX_ARCHITECTURES)
+ set(ARCHITECTURE_${ARCH} 1)
add_definitions(-DARCHITECTURE_${ARCH}=1)
endforeach()
@@ -218,4 +223,4 @@ if (NOT DEFINED ARCHITECTURE)
add_definitions(-DARCHITECTURE_GENERIC=1)
endif()
-message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}") \ No newline at end of file
+message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index d3e7b03cd2..76c5019788 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -152,7 +152,7 @@ add_library(
set_source_files_properties(zbic_compression.cpp PROPERTIES
INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}"
- COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>")
+ COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:AppleClang,Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>")
if(WIN32)
target_sources(common PRIVATE windows/timer_resolution.cpp
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 2c64f3375b..05511c10e1 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -256,6 +256,10 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
if (device.IsMoltenVK()) {
for (size_t i = 0; i < 8; ++i) {
const auto format = static_cast<Tegra::RenderTargetFormat>(key.state.color_formats[i]);
+ if (format == Tegra::RenderTargetFormat::NONE) {
+ info.color_output_types[i] = Shader::AttributeType::Float;
+ continue;
+ }
const auto pixel_format = VideoCore::Surface::PixelFormatFromRenderTargetFormat(format);
if (VideoCore::Surface::IsPixelFormatInteger(pixel_format)) {
if (VideoCore::Surface::IsPixelFormatSignedInteger(pixel_format)) {
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index c8aaf8dff5..3b783fc6c6 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -3063,6 +3063,10 @@ template <class P>
void TextureCache<P>::CopyImage(ImageId dst_id, ImageId src_id, std::vector<ImageCopy> copies) {
Image& dst = slot_images[dst_id];
Image& src = slot_images[src_id];
+ if (True(dst.flags & ImageFlagBits::UnwrittenSparse) ||
+ True(src.flags & ImageFlagBits::UnwrittenSparse)) {
+ return;
+ }
if (dst.info.astc_opaque != src.info.astc_opaque) {
// BC1 and BC3 blocks differ in size, so the host can't copy between them; both hold the
// same guest data, which is uploaded from guest memory instead.
diff --git a/src/yuzu/Info.plist b/src/yuzu/Info.plist
index cf6f597e99..9b6d49b87a 100644
--- a/src/yuzu/Info.plist
+++ b/src/yuzu/Info.plist
@@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
- <string>${EXECUTABLE_NAME}</string>
+ <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string></string>
<key>CFBundleIconFile</key>
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp
index ecb8171ac7..93bbbfb8fc 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp
@@ -38,8 +38,10 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co
titlebar_timer = SDL_AddTimer(2000, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 {
auto* this_ = (EmuWindow_SDL3*)userdata;
auto const results = this_->system.GetAndResetPerfStats();
- auto const title = fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f);
- SDL_SetWindowTitle(this_->render_window, title.c_str());
+ auto* const title = new std::string(fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f));
+ // Timers run on their own thread and AppKit throws when a title is set off the main
+ // thread: ApplyPendingTitle sets it from the main loop.
+ delete this_->pending_title.exchange(title);
return 2000;
}, this);
mouse_timer = SDL_AddTimer(100, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 {
@@ -55,6 +57,7 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co
EmuWindow_SDL3::~EmuWindow_SDL3() {
SDL_RemoveTimer(titlebar_timer);
SDL_RemoveTimer(mouse_timer);
+ delete pending_title.exchange(nullptr);
system.HIDCore().UnloadInputDevices();
input_subsystem->Shutdown();
SDL_Quit();
@@ -154,6 +157,13 @@ bool EmuWindow_SDL3::IsOpen() const {
return is_open;
}
+void EmuWindow_SDL3::ApplyPendingTitle() {
+ if (std::string* const title = pending_title.exchange(nullptr)) {
+ SDL_SetWindowTitle(render_window, title->c_str());
+ delete title;
+ }
+}
+
bool EmuWindow_SDL3::IsShown() const {
return is_shown;
}
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h
index 0dc2a4983a..a76a2bfd85 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h
@@ -6,7 +6,9 @@
#pragma once
+#include <atomic>
#include <cstdlib>
+#include <string>
#include <tuple>
#include <utility>
@@ -41,6 +43,9 @@ public:
/// Whether the window is still open, and a close request hasn't yet been sent
bool IsOpen() const;
+ /// Sets the title the titlebar timer prepared; call from the main thread.
+ void ApplyPendingTitle();
+
/// Returns if window is shown (not minimized)
bool IsShown() const override;
@@ -92,6 +97,7 @@ protected:
/// Periodic changer of titlebar (independent of event loop)
SDL_TimerID titlebar_timer;
+ std::atomic<std::string*> pending_title{};
// Mouse resetter once it
SDL_TimerID mouse_timer;
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index 22abe6a759..4d71a29993 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -481,6 +481,9 @@ static void RunKeyScript(InputCommon::InputSubsystem& input_subsystem) {
extern "C" SDL_AppResult SDL_AppIterate(void *appstate) {
SdlState *state = (SdlState *)appstate;
RunKeyScript(state->input_subsystem);
+ if (state->emu_window) {
+ state->emu_window->ApplyPendingTitle();
+ }
return state->emu_window && state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS;
}
extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {