Add Apple Silicon macOS build and frontend support (#320)

This commit is contained in:
Bruno 2026-10-03 13:26:39 +02:00 committed by GitHub
parent ea2dcef8b1
commit b9c233adef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
69 changed files with 1480 additions and 211 deletions

47
.github/workflows/macos.yml vendored Normal file
View File

@ -0,0 +1,47 @@
name: macOS
on:
pull_request:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: macos-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-26
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
# act uses the current working tree, including uncommitted changes.
if: ${{ !env.ACT }}
with:
fetch-depth: 0
submodules: recursive
- name: Install build tools
run: |
if ! command -v cmake >/dev/null || ! command -v ninja >/dev/null; then
brew install cmake ninja
fi
- name: Build
working-directory: ${{ env.NXEMU_LOCAL_WORKSPACE || github.workspace }}
run: sh src/script/build_macos.sh Release "${NXEMU_BUILD_DIRECTORY:-build}"
# The disk image preserves the bundle and includes an Applications shortcut.
- name: Package
working-directory: ${{ env.NXEMU_LOCAL_WORKSPACE || github.workspace }}
run: sh src/script/package_dmg_macos.sh NxEmu-macos-arm64.dmg
- uses: actions/upload-artifact@v4
with:
name: NxEmu-macos-arm64
path: ${{ env.NXEMU_LOCAL_WORKSPACE || github.workspace }}/package/NxEmu-macos-arm64.dmg
if-no-files-found: error

2
.gitignore vendored
View File

@ -20,12 +20,14 @@
*.o
Thumbs.db
.DS_Store
/.vs
/bin
/build
/git.properties
/ipch
/modules
/package
/src/nxemu-core/version.h
/src/nxemu-video/version.h
/src/nxemu-cpu/version.h

View File

@ -1,9 +1,26 @@
cmake_minimum_required(VERSION 3.22)
# Match the minimum supported by the bundled MoltenVK runtime, instead of
# silently targeting the macOS version of the developer's machine.
if(CMAKE_HOST_APPLE AND NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum macOS version" FORCE)
endif()
project(nxemu)
if(APPLE)
if(CMAKE_OSX_DEPLOYMENT_TARGET VERSION_LESS "12.0")
message(FATAL_ERROR "The bundled MoltenVK runtime requires macOS 12.0 or newer")
endif()
enable_language(OBJC OBJCXX)
endif()
find_package(Threads REQUIRED)
if(APPLE)
option(ENABLE_SDL2 "Enable SDL game controller input" ON)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
@ -11,18 +28,26 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# ============================================================
# Architecture detection
# ============================================================
if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
set(NXEMU_TARGET_PROCESSOR "${CMAKE_SYSTEM_PROCESSOR}")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _nxemu_arch_count)
if(_nxemu_arch_count GREATER 1)
message(FATAL_ERROR "Build each macOS architecture separately: the JIT backend is architecture-specific")
endif()
list(GET CMAKE_OSX_ARCHITECTURES 0 NXEMU_TARGET_PROCESSOR)
endif()
if(NXEMU_TARGET_PROCESSOR MATCHES "aarch64|arm64|ARM64")
set(ARCHITECTURE_arm64 TRUE)
set(ARCHITECTURE "arm64")
set(NXEMU_PLATFORM_DIR "arm64")
add_compile_definitions(ARCHITECTURE_arm64=1)
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64|amd64")
elseif(NXEMU_TARGET_PROCESSOR MATCHES "x86_64|AMD64|amd64")
add_compile_definitions(ARCHITECTURE_x86_64=1)
set(ARCHITECTURE_x86_64 TRUE)
set(ARCHITECTURE "x86_64")
set(NXEMU_PLATFORM_DIR "x64")
else()
set(NXEMU_PLATFORM_DIR "${CMAKE_SYSTEM_PROCESSOR}")
set(NXEMU_PLATFORM_DIR "${NXEMU_TARGET_PROCESSOR}")
endif()
# CMake modules

View File

@ -3,14 +3,46 @@ if (NOT TARGET fmt::fmt)
add_subdirectory(fmt)
endif()
# AppleClang 21 rejects the consteval path used by the bundled fmt 10.x.
# fmt 10.1.1 controls the constructor qualifier with FMT_CONSTEVAL; the newer
# FMT_USE_CONSTEVAL switch does not exist in this bundled version. Define
# FMT_CONSTEVAL to an empty replacement on the affected toolchain, matching
# the upstream workaround for fmt 10.x on AppleClang 21.
if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" AND
CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 21)
target_compile_definitions(fmt PUBLIC FMT_CONSTEVAL=)
# macOS controller adapters are bundled against the application deployment target.
if(APPLE)
option(ENABLE_LIBUSB "Enable direct GameCube USB adapter input" ON)
if(ENABLE_LIBUSB AND NOT TARGET libusb::usb)
set(_nxemu_usb "${CMAKE_CURRENT_SOURCE_DIR}/libusb/libusb")
add_library(nxemu_libusb STATIC
${_nxemu_usb}/core.c
${_nxemu_usb}/descriptor.c
${_nxemu_usb}/hotplug.c
${_nxemu_usb}/io.c
${_nxemu_usb}/strerror.c
${_nxemu_usb}/sync.c
${_nxemu_usb}/os/events_posix.c
${_nxemu_usb}/os/threads_posix.c
${_nxemu_usb}/os/darwin_usb.c)
target_include_directories(nxemu_libusb PUBLIC "${_nxemu_usb}"
PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/libusb/Xcode")
target_compile_options(nxemu_libusb PRIVATE -fvisibility=hidden)
target_link_libraries(nxemu_libusb PRIVATE Threads::Threads
"-framework IOKit" "-framework CoreFoundation" "-framework Security")
add_library(libusb::usb ALIAS nxemu_libusb)
unset(_nxemu_usb)
endif()
endif()
# Keep macOS controller support self-contained in the application bundle.
if(APPLE AND ENABLE_SDL2 AND NOT TARGET SDL2::SDL2)
set(SDL_SHARED OFF CACHE BOOL "Build shared SDL" FORCE)
set(SDL_STATIC ON CACHE BOOL "Build static SDL" FORCE)
set(SDL_TEST OFF CACHE BOOL "Build SDL test library" FORCE)
set(SDL_TESTS OFF CACHE BOOL "Build SDL tests" FORCE)
set(SDL2_DISABLE_INSTALL ON CACHE BOOL "Disable SDL installation" FORCE)
# SDL's bundled checks use an older minimum; CMake 4 requires at least 3.5.
set(_nxemu_saved_policy_minimum "${CMAKE_POLICY_VERSION_MINIMUM}")
if(NOT CMAKE_POLICY_VERSION_MINIMUM OR CMAKE_POLICY_VERSION_MINIMUM VERSION_LESS "3.5")
set(CMAKE_POLICY_VERSION_MINIMUM "3.5")
endif()
add_subdirectory(sdl)
set(CMAKE_POLICY_VERSION_MINIMUM "${_nxemu_saved_policy_minimum}")
unset(_nxemu_saved_policy_minimum)
endif()
# Boost
@ -184,5 +216,44 @@ if(NOT TARGET tsl::robin_map)
target_include_directories(robin_map INTERFACE "${CMAKE_SOURCE_DIR}/external/robin-map/include")
endif()
# MoltenVK runtime (macOS)
if(APPLE)
include(ExternalProject)
find_program(NXEMU_XCODEBUILD xcodebuild REQUIRED)
execute_process(COMMAND "${NXEMU_XCODEBUILD}" -version
RESULT_VARIABLE _nxemu_xcode_result OUTPUT_QUIET ERROR_VARIABLE _nxemu_xcode_error)
if(NOT _nxemu_xcode_result EQUAL 0)
message(FATAL_ERROR
"Building MoltenVK requires full Xcode, not only Command Line Tools. "
"Select Xcode with xcode-select. ${_nxemu_xcode_error}")
endif()
set(_nxemu_moltenvk_root "${CMAKE_BINARY_DIR}/external/moltenvk")
set(_nxemu_moltenvk_source "${_nxemu_moltenvk_root}/src")
set(_nxemu_moltenvk_runtime
"${_nxemu_moltenvk_source}/MoltenVK/Package/Latest/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib")
ExternalProject_Add(nxemu_moltenvk
GIT_REPOSITORY "https://github.com/V380-Ori/Ryujinx.MoltenVK.git"
GIT_TAG "45f9b641e45f1d150f3521d74808477add9e975b"
GIT_SHALLOW FALSE
GIT_SUBMODULES_RECURSE TRUE
UPDATE_COMMAND ""
SOURCE_DIR "${_nxemu_moltenvk_source}"
CONFIGURE_COMMAND ""
BUILD_IN_SOURCE TRUE
BUILD_COMMAND /bin/sh -c
"cd MoltenVK && (git apply --reverse --check ../000*.patch || git apply --verbose ../000*.patch) && ./fetchDependencies --macos && make macos"
BUILD_BYPRODUCTS "${_nxemu_moltenvk_runtime}"
INSTALL_COMMAND ""
)
set(NXEMU_MOLTENVK_RUNTIME "${_nxemu_moltenvk_runtime}" CACHE INTERNAL "" FORCE)
unset(_nxemu_moltenvk_root)
unset(_nxemu_moltenvk_source)
unset(_nxemu_moltenvk_runtime)
endif()
# sciter
add_subdirectory(sciterui)
add_subdirectory(sciterui)

2
external/ffmpeg vendored

@ -1 +1 @@
Subproject commit 772cea1ac12d5cd11ff17df97dc8a9858d9c1c01
Subproject commit 7bce512dcae670a7349088b564b0e6251c0a487e

2
external/fmt vendored

@ -1 +1 @@
Subproject commit f5e54359df4c26b6230fc61d38aa294581393084
Subproject commit 1be298e1bd68957e4cd352e1f676f00e07dcfb57

2
external/sciterui vendored

@ -1 +1 @@
Subproject commit 303c68557c469a76da5ee8b9d07c1594a8798a9f
Subproject commit dcceefb6dfcb92a0ef5d98ec6f368f82c83fe934

View File

@ -143,3 +143,19 @@ html > header > window-buttons > window-button[role="window-minimize"]
{
foreground-image: url(icon:window-minimize);
}
/* macOS uses the native AppKit title bar instead of Sciter window chrome. */
html[data-platform="macos"]
{
border: none;
}
html[data-platform="macos"] > header
{
display: none;
}
html[data-platform="macos"][window-state="maximized"]
{
border: none;
}

View File

@ -46,7 +46,7 @@
}
#StatusBar #volume:hover,
#StatusBar #renderer:hover,
#StatusBar #renderer.interactive:hover,
#StatusBar #dockedMode:hover,
#StatusBar #gpuCommandSynchronization:hover,
#StatusBar #volumePopupBtn:hover

View File

@ -1,94 +1,8 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png" window-resizable window-min-width="1000" window-min-height="665">
<html window-frame="solid-with-shadow" window-icon="window-icon.png" window-resizable window-min-width="1000" window-min-height="665">
<head>
<link href="base.css" rel="stylesheet" type="text/css">
<link href="main_window.css" rel="stylesheet" type="text/css">
<title>NxEmu</title>
</head>
<header>
<window-caption role=window-caption><img src="window-icon-32.png" /><span>NxEmu</span></window-caption>
<window-buttons>
<window-button role="window-minimize"></window-button>
<window-button role="window-maximize"></window-button>
<window-button role="window-close"></window-button>
</window-buttons>
</header>
<mainmenu id="MainMenu" />
<div id="MainContents">
<div id="RomBrowserPanel" class="main-contents-panel">
<rombrowser />
</div>
<div id="LoadingPanel" class="main-contents-panel">
<div class="loading-container">
<img id="LoadingCornerLogo" />
<div class="loading-main">
<img id="LoadingGameIcon" alt="" />
<div class="loading-text-column">
<div id="loadingText" />
<div class="loading-progress-wrap">
<div class="loading-progress-track">
<div id="LoadingProgressFill" class="loading-progress-fill indeterminate"></div>
</div>
</div>
</div>
</div>
<img id="LoadingCornerBanner" />
</div>
</div>
<div id="PausePanel" class="main-contents-panel">
<div class="pause-panel-container">
<div class="pause-panel-title">Paused</div>
</div>
</div>
</div>
<div id="StatusBar" >
<span id="dockedMode" />
<span id="renderer" />
<span id="gpuCommandSynchronization" tooltip="Toggle guest-requested GPU command synchronization at runtime." />
<span id="volumeAnchor">
<span id="volume" />
<span id="volumePopupBtn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M104.704 685.248a64 64 0 0 0 90.496 0l316.8-316.8 316.8 316.8a64 64 0 0 0 90.496-90.496L557.248 232.704a64 64 0 0 0-90.496 0L104.704 594.752a64 64 0 0 0 0 90.496"/></svg></span>
</span>
<div id="StatusText" />
</div>
<menu.context id="RomCardContextMenu">
<li id="romStartGame">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</span>
<span class="menu-item-label">Start Game</span>
</li>
<hr />
<li id="romOpenSaveData">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>
</span>
<span class="menu-item-label">Open Save Data Location</span>
</li>
<li id="romOpenModData">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>
</span>
<span class="menu-item-label">Open Mod Data Location</span>
</li>
<hr />
<li id="romProperties">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.03 7.03 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 2h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.59.24-1.13.55-1.63.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 8.48a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.83 14.1a.5.5 0 0 0-.12.64l1.92 3.32c.13.23.4.32.64.22l2.39-.96c.5.39 1.04.7 1.63.94l.36 2.54c.05.24.25.42.49.42h3.8c.24 0 .44-.18.49-.42l.36-2.54c.59-.24 1.13-.55 1.63-.94l2.39.96c.24.1.51.01.64-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7z"/>
</svg>
</span>
<span class="menu-item-label">Configure Game</span>
</li>
</menu>
<include src="main_window_content.html" />
</html>
<popup id="VolumePopup">
<div>
<input type="hslider" min="0" max="200" value="100" id="audioVolume" style="width: 19em" />
</div>
</popup>

View File

@ -0,0 +1,87 @@
<header>
<window-caption role=window-caption><img src="window-icon-32.png" /><span>NxEmu</span></window-caption>
<window-buttons>
<window-button role="window-minimize"></window-button>
<window-button role="window-maximize"></window-button>
<window-button role="window-close"></window-button>
</window-buttons>
</header>
<mainmenu id="MainMenu" />
<div id="MainContents">
<div id="RomBrowserPanel" class="main-contents-panel">
<rombrowser />
</div>
<div id="LoadingPanel" class="main-contents-panel">
<div class="loading-container">
<img id="LoadingCornerLogo" />
<div class="loading-main">
<img id="LoadingGameIcon" alt="" />
<div class="loading-text-column">
<div id="loadingText" />
<div class="loading-progress-wrap">
<div class="loading-progress-track">
<div id="LoadingProgressFill" class="loading-progress-fill indeterminate"></div>
</div>
</div>
</div>
</div>
<img id="LoadingCornerBanner" />
</div>
</div>
<div id="PausePanel" class="main-contents-panel">
<div class="pause-panel-container">
<div class="pause-panel-title">Paused</div>
</div>
</div>
</div>
<div id="StatusBar" >
<span id="dockedMode" />
<span id="renderer" />
<span id="gpuCommandSynchronization" tooltip="Toggle guest-requested GPU command synchronization at runtime." />
<span id="volumeAnchor">
<span id="volume" />
<span id="volumePopupBtn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path d="M104.704 685.248a64 64 0 0 0 90.496 0l316.8-316.8 316.8 316.8a64 64 0 0 0 90.496-90.496L557.248 232.704a64 64 0 0 0-90.496 0L104.704 594.752a64 64 0 0 0 0 90.496"/></svg></span>
</span>
<div id="StatusText" />
</div>
<menu.context id="RomCardContextMenu">
<li id="romStartGame">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z"/>
</svg>
</span>
<span class="menu-item-label">Start Game</span>
</li>
<hr />
<li id="romOpenSaveData">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>
</span>
<span class="menu-item-label">Open Save Data Location</span>
</li>
<li id="romOpenModData">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>
</svg>
</span>
<span class="menu-item-label">Open Mod Data Location</span>
</li>
<hr />
<li id="romProperties">
<span class="menu-item-glyph menu-item-icon-svg" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19.14 12.94c.04-.31.06-.63.06-.94s-.02-.63-.06-.94l2.03-1.58a.5.5 0 0 0 .12-.64l-1.92-3.32a.5.5 0 0 0-.6-.22l-2.39.96a7.03 7.03 0 0 0-1.63-.94l-.36-2.54A.5.5 0 0 0 13.9 2h-3.8a.5.5 0 0 0-.49.42l-.36 2.54c-.59.24-1.13.55-1.63.94l-2.39-.96a.5.5 0 0 0-.6.22L2.71 8.48a.5.5 0 0 0 .12.64l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94L2.83 14.1a.5.5 0 0 0-.12.64l1.92 3.32c.13.23.4.32.64.22l2.39-.96c.5.39 1.04.7 1.63.94l.36 2.54c.05.24.25.42.49.42h3.8c.24 0 .44-.18.49-.42l.36-2.54c.59-.24 1.13-.55 1.63-.94l2.39.96c.24.1.51.01.64-.22l1.92-3.32a.5.5 0 0 0-.12-.64l-2.03-1.58zM12 15.5A3.5 3.5 0 1 1 12 8.5a3.5 3.5 0 0 1 0 7z"/>
</svg>
</span>
<span class="menu-item-label">Configure Game</span>
</li>
</menu>
<popup id="VolumePopup">
<div>
<input type="hslider" min="0" max="200" value="100" id="audioVolume" style="width: 19em" />
</div>
</popup>

View File

@ -0,0 +1,8 @@
<html window-frame="standard" data-platform="macos" window-icon="window-icon.png" window-resizable window-min-width="1000" window-min-height="665">
<head>
<link href="base.css" rel="stylesheet" type="text/css">
<link href="main_window.css" rel="stylesheet" type="text/css">
<title>NxEmu</title>
</head>
<include src="main_window_content.html" />
</html>

View File

@ -0,0 +1,45 @@
# Invoked at link time and by nxemu_bundle, so resources never depend on a relink.
foreach(required BUNDLE LOADER CPU VIDEO OS MOLTENVK SCITER LANG CODESIGN INFO_PLIST ICON EXECUTABLE_NAME SHORT_VERSION BUILD_VERSION MINIMUM_MACOS)
if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "")
message(FATAL_ERROR "Missing bundle input: ${required}")
endif()
endforeach()
set(runtime "${BUNDLE}/Contents/MacOS")
set(frameworks "${BUNDLE}/Contents/Frameworks")
file(MAKE_DIRECTORY "${frameworks}" "${BUNDLE}/Contents/Resources")
# CMake normally generates Info.plist only during configuration. Restore it during
# packaging too, including incremental builds after bundle metadata was removed.
set(MACOSX_BUNDLE_EXECUTABLE_NAME "${EXECUTABLE_NAME}")
set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${SHORT_VERSION}")
set(MACOSX_BUNDLE_BUNDLE_VERSION "${BUILD_VERSION}")
set(CMAKE_OSX_DEPLOYMENT_TARGET "${MINIMUM_MACOS}")
configure_file("${INFO_PLIST}" "${BUNDLE}/Contents/Info.plist")
file(COPY_FILE "${ICON}" "${BUNDLE}/Contents/Resources/nxemu.icns" ONLY_IF_DIFFERENT)
foreach(pair "LOADER|loader" "CPU|cpu" "VIDEO|video" "OS|operating_system")
string(REPLACE "|" ";" parts "${pair}")
list(GET parts 0 input)
list(GET parts 1 folder)
get_filename_component(filename "${${input}}" NAME)
file(MAKE_DIRECTORY "${frameworks}/modules/${folder}")
file(COPY_FILE "${${input}}" "${frameworks}/modules/${folder}/${filename}" ONLY_IF_DIFFERENT)
endforeach()
file(COPY_FILE "${MOLTENVK}" "${frameworks}/libMoltenVK.dylib" ONLY_IF_DIFFERENT)
file(COPY_FILE "${SCITER}" "${runtime}/libsciter.dylib" ONLY_IF_DIFFERENT)
file(COPY "${LANG}" DESTINATION "${BUNDLE}/Contents/Resources")
# Remove only locations generated by the older bundle layout, before sealing.
file(REMOVE_RECURSE "${runtime}/lang" "${runtime}/modules")
# Ad-hoc signing enables local execution; distribution/notarization needs a
# developer identity. Sign nested code before sealing the application resources.
file(GLOB_RECURSE libraries "${runtime}/*.dylib" "${frameworks}/*.dylib")
foreach(library IN LISTS libraries)
execute_process(COMMAND "${CODESIGN}" --force --sign - "${library}"
RESULT_VARIABLE result OUTPUT_QUIET ERROR_VARIABLE error)
if(NOT result EQUAL 0)
message(FATAL_ERROR "Cannot sign ${library}: ${error}")
endif()
endforeach()
execute_process(COMMAND "${CODESIGN}" --force --sign - "${BUNDLE}"
RESULT_VARIABLE result OUTPUT_QUIET ERROR_VARIABLE error)
if(NOT result EQUAL 0)
message(FATAL_ERROR "Cannot sign bundle: ${error}")
endif()

View File

@ -8,7 +8,13 @@ function(nxemu_set_app_output target)
endfunction()
function(nxemu_set_module_output target module_name)
set(_out "${CMAKE_SOURCE_DIR}/modules/${NXEMU_PLATFORM_DIR}/${module_name}")
if(APPLE)
# Keep modules from different build trees/configurations from overwriting
# each other. The bundle receives exactly these target files.
set(_out "${CMAKE_BINARY_DIR}/modules/${NXEMU_PLATFORM_DIR}/$<CONFIG>/${module_name}")
else()
set(_out "${CMAKE_SOURCE_DIR}/modules/${NXEMU_PLATFORM_DIR}/${module_name}")
endif()
set_target_properties(${target} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${_out}"
LIBRARY_OUTPUT_DIRECTORY "${_out}"

View File

@ -10,6 +10,7 @@
#include "dynarmic/backend/arm64/abi.h"
#include "dynarmic/backend/arm64/emit_arm64.h"
#include "dynarmic/backend/arm64/emit_context.h"
#include "dynarmic/backend/arm64/fpsr_manager.h"
#include "dynarmic/backend/arm64/reg_alloc.h"
#include "dynarmic/interface/halt_reason.h"
#include "dynarmic/ir/basic_block.h"
@ -278,6 +279,7 @@ void EmitIR<IR::Opcode::A64GetFPCR>(oaknut::CodeGenerator& code, EmitContext& ct
template<>
void EmitIR<IR::Opcode::A64GetFPSR>(oaknut::CodeGenerator& code, EmitContext& ctx, IR::Inst* inst) {
ctx.fpsr.Spill();
auto Wresult = ctx.reg_alloc.WriteW(inst);
RegAlloc::Realize(Wresult);

View File

@ -22,7 +22,7 @@ struct fmt::formatter<Dynarmic::A32::CoprocReg>
return ctx.begin();
}
template <typename FormatContext>
auto format(const Dynarmic::A32::CoprocReg & reg, FormatContext & ctx)
auto format(const Dynarmic::A32::CoprocReg & reg, FormatContext & ctx) const
{
return fmt::format_to(ctx.out(), "cp{}", static_cast<size_t>(reg));
}

View File

@ -18,6 +18,7 @@
#endif
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "core/core.h"
#include "core/hle/kernel/k_event.h"

View File

@ -167,7 +167,7 @@ constexpr inline Result GetSpanBetweenTimePoints(s64* out_seconds, const SteadyC
template <>
struct fmt::formatter<Service::PSC::Time::TimeType> : fmt::formatter<fmt::string_view> {
template <typename FormatContext>
auto format(Service::PSC::Time::TimeType type, FormatContext& ctx) {
auto format(Service::PSC::Time::TimeType type, FormatContext& ctx) const {
const string_view name = [type] {
using Service::PSC::Time::TimeType;
switch (type) {
@ -270,4 +270,4 @@ struct fmt::formatter<Service::PSC::Time::ContinuousAdjustmentTimePoint>
time_point.rtc_offset, time_point.diff_scale, time_point.shift_amount,
time_point.lower, time_point.upper);
}
};
};

View File

@ -310,10 +310,15 @@ RenderWindow::RenderWindow(IRenderWindow & renderWindow) :
window_info.type = Core::Frontend::WindowSystemType::Windows;
#elif defined(__ANDROID__)
window_info.type = Core::Frontend::WindowSystemType::Android;
#elif defined(__APPLE__)
window_info.type = Core::Frontend::WindowSystemType::Cocoa;
#else
window_info.type = Core::Frontend::WindowSystemType::Headless;
#endif
window_info.render_surface = renderWindow.RenderSurface();
#ifdef __APPLE__
window_info.render_surface_scale = renderWindow.PixelRatio();
#endif
NotifyClientAreaSizeChanged({0, 0});
UpdateCurrentFramebufferLayout(640, 480);
}
@ -351,4 +356,4 @@ std::unique_ptr<Core::Frontend::GraphicsContext> RenderWindow::CreateSharedConte
bool RenderWindow::IsShown() const
{
return true;
}
}

View File

@ -1,3 +1,5 @@
option(NXEMU_MACOS_BUNDLE "Build NxEmu as a macOS application bundle" ON)
add_executable(nxemu
main.cpp
startup_checks.cpp
@ -72,6 +74,21 @@ add_executable(nxemu
applets/web_browser.h
)
if(APPLE)
target_sources(nxemu PRIVATE
user_interface/file_dialogs_macos.mm
user_interface/render_window_macos.mm
)
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
find_library(QUARTZCORE_FRAMEWORK QuartzCore REQUIRED)
find_library(UNIFORMTYPEIDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
target_link_libraries(nxemu PRIVATE
${APPKIT_FRAMEWORK}
${QUARTZCORE_FRAMEWORK}
${UNIFORMTYPEIDENTIFIERS_FRAMEWORK}
)
endif()
target_include_directories(nxemu PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/nxemu
@ -99,8 +116,10 @@ target_link_libraries(nxemu PRIVATE
${CMAKE_DL_LIBS}
)
nxemu_set_app_output(nxemu)
include(nxemu_setup_environment)
nxemu_setup_environment(nxemu)
if(NOT APPLE OR NOT NXEMU_MACOS_BUNDLE)
include(nxemu_setup_environment)
nxemu_setup_environment(nxemu)
endif()
# Runtime needs the Sciter engine beside the executable
if(WIN32)
@ -117,9 +136,92 @@ else()
set(_nxemu_sciter_dest "libsciter.so")
endif()
if(EXISTS "${_nxemu_sciter_lib}")
add_custom_command(TARGET nxemu POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_nxemu_sciter_lib}" "$<TARGET_FILE_DIR:nxemu>/${_nxemu_sciter_dest}"
COMMENT "Copy Sciter engine next to nxemu"
if(APPLE AND NXEMU_MACOS_BUNDLE)
set(_nxemu_icon "${CMAKE_CURRENT_SOURCE_DIR}/macos/nxemu.icns")
set_source_files_properties("${_nxemu_icon}" PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
target_sources(nxemu PRIVATE "${_nxemu_icon}")
set_target_properties(nxemu PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in"
MACOSX_BUNDLE_ICON_FILE "nxemu.icns"
MACOSX_BUNDLE_GUI_IDENTIFIER "org.nxemu"
MACOSX_BUNDLE_BUNDLE_NAME "NxEmu"
MACOSX_BUNDLE_SHORT_VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_REVISION}"
MACOSX_BUNDLE_BUNDLE_VERSION "${VERSION_BUILD}"
OUTPUT_NAME "NxEmu"
)
if(NOT NXEMU_MOLTENVK_RUNTIME OR NOT EXISTS "${_nxemu_sciter_lib}")
message(FATAL_ERROR "The macOS bundle requires MoltenVK and Sciter")
endif()
if(TARGET nxemu_moltenvk)
add_dependencies(nxemu nxemu_moltenvk)
endif()
add_dependencies(nxemu nxemu-loader nxemu-cpu nxemu-video nxemu-os)
find_program(NXEMU_CODESIGN codesign REQUIRED)
set(_nxemu_bundle_script "${CMAKE_SOURCE_DIR}/src/cmake/modules/nxemu_macos_bundle.cmake")
set(_nxemu_bundle_command ${CMAKE_COMMAND}
"-DBUNDLE=$<TARGET_BUNDLE_DIR:nxemu>"
"-DLOADER=$<TARGET_FILE:nxemu-loader>"
"-DCPU=$<TARGET_FILE:nxemu-cpu>"
"-DVIDEO=$<TARGET_FILE:nxemu-video>"
"-DOS=$<TARGET_FILE:nxemu-os>"
"-DMOLTENVK=${NXEMU_MOLTENVK_RUNTIME}"
"-DSCITER=${_nxemu_sciter_lib}"
"-DLANG=${CMAKE_SOURCE_DIR}/lang"
"-DCODESIGN=${NXEMU_CODESIGN}"
"-DINFO_PLIST=${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in"
"-DICON=${_nxemu_icon}"
"-DEXECUTABLE_NAME=$<TARGET_FILE_NAME:nxemu>"
"-DSHORT_VERSION=${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_REVISION}"
"-DBUILD_VERSION=${VERSION_BUILD}"
"-DMINIMUM_MACOS=${CMAKE_OSX_DEPLOYMENT_TARGET}"
-P "${_nxemu_bundle_script}")
# Module-only rebuilds must refresh the app even when its own code did not change.
# LINK_DEPENDS also covers an explicit --target nxemu build.
set_property(TARGET nxemu APPEND PROPERTY LINK_DEPENDS
"$<TARGET_FILE:nxemu-loader>" "$<TARGET_FILE:nxemu-cpu>"
"$<TARGET_FILE:nxemu-video>" "$<TARGET_FILE:nxemu-os>"
"${NXEMU_MOLTENVK_RUNTIME}" "${_nxemu_sciter_lib}" "${_nxemu_bundle_script}"
"${CMAKE_CURRENT_SOURCE_DIR}/macos/Info.plist.in" "${_nxemu_icon}")
add_custom_command(TARGET nxemu POST_BUILD
COMMAND ${_nxemu_bundle_command} VERBATIM
COMMENT "Package and sign NxEmu.app with this build's modules")
add_custom_target(nxemu_bundle ALL
COMMAND ${_nxemu_bundle_command}
DEPENDS nxemu
VERBATIM
COMMENT "Refresh NxEmu.app runtime and language resources")
else()
if(EXISTS "${_nxemu_sciter_lib}")
add_custom_command(TARGET nxemu POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_nxemu_sciter_lib}" "$<TARGET_FILE_DIR:nxemu>/${_nxemu_sciter_dest}"
VERBATIM)
endif()
if(APPLE)
if(NOT NXEMU_MOLTENVK_RUNTIME)
message(FATAL_ERROR "MoltenVK runtime was not configured")
endif()
if(TARGET nxemu_moltenvk)
add_dependencies(nxemu nxemu_moltenvk)
endif()
add_dependencies(nxemu nxemu-loader nxemu-cpu nxemu-video nxemu-os)
set_property(TARGET nxemu APPEND PROPERTY LINK_DEPENDS
"$<TARGET_FILE:nxemu-loader>" "$<TARGET_FILE:nxemu-cpu>"
"$<TARGET_FILE:nxemu-video>" "$<TARGET_FILE:nxemu-os>")
add_custom_command(TARGET nxemu POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${NXEMU_MOLTENVK_RUNTIME}" "$<TARGET_FILE_DIR:nxemu>/libMoltenVK.dylib"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/lang" "$<TARGET_FILE_DIR:nxemu>/lang"
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:nxemu>/modules/loader"
"$<TARGET_FILE_DIR:nxemu>/modules/cpu"
"$<TARGET_FILE_DIR:nxemu>/modules/video"
"$<TARGET_FILE_DIR:nxemu>/modules/operating_system"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:nxemu-loader>" "$<TARGET_FILE_DIR:nxemu>/modules/loader/"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:nxemu-cpu>" "$<TARGET_FILE_DIR:nxemu>/modules/cpu/"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:nxemu-video>" "$<TARGET_FILE_DIR:nxemu>/modules/video/"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:nxemu-os>" "$<TARGET_FILE_DIR:nxemu>/modules/operating_system/"
VERBATIM)
endif()
endif()

View File

@ -52,6 +52,22 @@ void ProfileSelectApplet::Detach()
m_parameters = {};
}
void ProfileSelectApplet::DetachAfterWindowDestroyed()
{
m_sciterUI = nullptr;
m_modules = nullptr;
m_rootElement = {};
m_parentHwnd = nullptr;
m_window = nullptr;
m_dialogOpen = false;
std::lock_guard<std::mutex> lock(m_mutex);
m_pending = false;
m_userData = nullptr;
m_finished = nullptr;
m_parameters = {};
}
void ProfileSelectApplet::Close()
{
ProfileSelectFinishedFn finished = nullptr;

View File

@ -18,6 +18,7 @@ public:
void Attach(ISciterUI & sciterUI, SystemModules & modules, SciterElement rootElement, void * parentHwnd);
void Detach();
void DetachAfterWindowDestroyed();
// IProfileSelectApplet
void Close() override;

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>org.nxemu</string>
<key>CFBundleIconFile</key>
<string>nxemu.icns</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>NxEmu</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
<key>LSMinimumSystemVersion</key>
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSSupportsGameMode</key>
<true/>
<!-- Compatibility with macOS versions using the older Game Mode key. -->
<key>GCSupportsGameMode</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>NxEmu uses the microphone for games that request audio input.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Games running in NxEmu may use the local network to communicate with other devices.</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>

BIN
src/nxemu/macos/nxemu.icns Normal file

Binary file not shown.

View File

@ -4,8 +4,12 @@
#include "user_interface/sciter_main_window.h"
#include <common/path.h>
#include <common/std_string.h>
#include <filesystem>
#include <memory>
#include <nxemu-core/app_init.h>
#ifdef __APPLE__
#include <nxemu-core/settings/core_settings.h>
#endif
#include <nxemu-core/version.h>
#include <yuzu_common/fs/path_util.h>
#include <sciter_ui.h>
@ -97,6 +101,21 @@ static int RunApplication(const char * arg0)
#endif
bool res = AppInit(&Notification::GetInstance(), Path(Path::MODULE_DIRECTORY), Common::FS::GetYuzuPathString(Common::FS::YuzuPath::YuzuDir).c_str());
#ifdef __APPLE__
const auto exe_dir = Common::FS::GetExeDirectory();
const bool is_bundle = exe_dir.filename() == "MacOS" && exe_dir.parent_path().filename() == "Contents";
const bool has_local_runtime = std::filesystem::is_directory(exe_dir / "modules") &&
std::filesystem::is_directory(exe_dir / "lang");
if (res && (is_bundle || has_local_runtime)) {
// A bundle must load its matching modules/resources even when reusing a
// development config. Keep the stored path values unchanged for raw runs.
const auto module_dir = is_bundle ? exe_dir.parent_path() / "Frameworks/modules" : exe_dir / "modules";
const auto language_dir = is_bundle ? exe_dir.parent_path() / "Resources/lang" : exe_dir / "lang";
coreSettings.moduleDir = Path(module_dir.string(), "");
uiSettings.languageDir = Path(language_dir.string(), "");
}
#endif
if (res && StartupChecks(arg0, &has_broken_vulkan, uiSettings.performVulkanCheck)) {
return 0;
}

View File

@ -23,6 +23,8 @@
#include "startup_checks.h"
#include "vulkan_probe.h"
#include "yuzu_common/common_types.h"
#include "yuzu_common/logging/log.h"
#include "yuzu_common/scope_exit.h"
void CheckVulkan() {
// Just start the Vulkan loader, this will crash if something is wrong
@ -483,6 +485,98 @@ void PopulateVulkanRecords(std::vector<VkDeviceRecord> & records, void * renderS
{
FreeLibrary(library);
}
#elif defined(__APPLE__)
records.clear();
try
{
// Use the same bundled MoltenVK as rendering, with portability enumeration enabled.
const auto library = Vulkan::OpenLibrary();
Vulkan::vk::InstanceDispatch dispatch;
const auto instance = Vulkan::CreateInstance(*library, dispatch, VK_API_VERSION_1_1,
renderSurface ? Vulkan::WindowSystemType::Metal : Vulkan::WindowSystemType::Headless);
u32 count = 0;
std::vector<VkPhysicalDevice> devices;
VkResult result;
do
{
Vulkan::vk::Check(dispatch.vkEnumeratePhysicalDevices(*instance, &count, nullptr));
devices.resize(count);
result = dispatch.vkEnumeratePhysicalDevices(*instance, &count, devices.data());
} while (result == VK_INCOMPLETE);
Vulkan::vk::Check(result);
devices.resize(count);
// Match the renderer's device indices: vendor, discrete GPUs, then descending name.
const auto vendor_rank = [](u32 vendor) {
return vendor == 0x10de ? 0 : vendor == 0x1002 ? 1 : vendor == 0x8086 ? 2 : 3;
};
std::stable_sort(devices.begin(), devices.end(), [&](auto lhs, auto rhs) {
VkPhysicalDeviceProperties a{}, b{};
dispatch.vkGetPhysicalDeviceProperties(lhs, &a);
dispatch.vkGetPhysicalDeviceProperties(rhs, &b);
if (vendor_rank(a.vendorID) != vendor_rank(b.vendorID))
return vendor_rank(a.vendorID) < vendor_rank(b.vendorID);
const bool a_discrete = a.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
const bool b_discrete = b.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU;
if (a_discrete != b_discrete)
return a_discrete;
return std::string_view{a.deviceName} > std::string_view{b.deviceName};
});
const auto get_proc = [&](const char* name) {
return dispatch.vkGetInstanceProcAddr(*instance, name);
};
const auto create_surface = reinterpret_cast<PFN_vkCreateMetalSurfaceEXT>(get_proc("vkCreateMetalSurfaceEXT"));
const auto destroy_surface = reinterpret_cast<PFN_vkDestroySurfaceKHR>(get_proc("vkDestroySurfaceKHR"));
const auto get_present_modes = reinterpret_cast<PFN_vkGetPhysicalDeviceSurfacePresentModesKHR>(
get_proc("vkGetPhysicalDeviceSurfacePresentModesKHR"));
VkSurfaceKHR surface = VK_NULL_HANDLE;
SCOPE_EXIT {
if (surface != VK_NULL_HANDLE && destroy_surface)
destroy_surface(*instance, surface, nullptr);
};
if (renderSurface && create_surface && destroy_surface)
{
VkMetalSurfaceCreateInfoEXT info{};
info.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT;
info.pLayer = static_cast<const CAMetalLayer*>(renderSurface);
const auto status = create_surface(*instance, &info, nullptr, &surface);
if (status != VK_SUCCESS)
LOG_WARNING(Render_Vulkan, "Device probe could not create a Metal surface: {}", static_cast<int>(status));
}
records.reserve(devices.size());
for (const auto device : devices)
{
VkPhysicalDeviceProperties properties{};
dispatch.vkGetPhysicalDeviceProperties(device, &properties);
std::vector<uint32_t> present_modes;
if (surface != VK_NULL_HANDLE && get_present_modes)
{
u32 mode_count = 0;
std::vector<VkPresentModeKHR> modes;
VkResult status;
do
{
status = get_present_modes(device, surface, &mode_count, nullptr);
if (status != VK_SUCCESS) break;
modes.resize(mode_count);
status = get_present_modes(device, surface, &mode_count, modes.data());
} while (status == VK_INCOMPLETE);
if (status == VK_SUCCESS)
present_modes.assign(modes.begin(), modes.begin() + mode_count);
}
records.emplace_back(properties.deviceName, present_modes, false);
LOG_INFO(Render_Vulkan, "Graphics device available: {}", properties.deviceName);
}
if (records.empty())
LOG_WARNING(Render_Vulkan, "MoltenVK reported no physical devices");
}
catch (const std::exception& e)
{
records.clear();
LOG_ERROR(Render_Vulkan, "Failed to enumerate graphics devices: {}", e.what());
}
#else
(void)renderSurface;
records.clear();

View File

@ -15,7 +15,14 @@ AboutDialog::AboutDialog(ISciterUI & sciterUI) :
{
}
AboutDialog::~AboutDialog() = default;
AboutDialog::~AboutDialog()
{
if (m_window != nullptr)
{
m_window->OnDestroySinkRemove(this);
m_window->Destroy();
}
}
void AboutDialog::Display(void * parentWindow)
{

View File

@ -21,6 +21,14 @@
#include <vector>
#endif
#ifdef __APPLE__
namespace MacOSFileDialogs
{
bool FileSelect(const char * initialDir, const char * fileFilter, Path & selected);
Path BrowseForDirectory(const char * title);
} // namespace MacOSFileDialogs
#endif
bool FileSelect(void * hwndOwner, const char * initialDir, const char * fileFilter, bool fileMustExist, Path & selected)
{
#ifdef _WIN32
@ -59,6 +67,10 @@ bool FileSelect(void * hwndOwner, const char * initialDir, const char * fileFilt
}
selected = Path(stdstr().FromUTF16(fileName.data()).c_str());
return true;
#elif defined(__APPLE__)
(void)hwndOwner;
(void)fileMustExist;
return MacOSFileDialogs::FileSelect(initialDir, fileFilter, selected);
#else
return false;
#endif
@ -444,6 +456,9 @@ Path BrowseForDirectory(void * parentWindow, const char * title)
{
CoUninitialize();
}
#elif defined(__APPLE__)
(void)parentWindow;
selected = MacOSFileDialogs::BrowseForDirectory(title);
#elif defined(__linux__)
const std::string folder = PickDirectory(parentWindow, title);
if (!folder.empty())

View File

@ -0,0 +1,167 @@
#include "file_dialogs.h"
#include <common/path.h>
#import <AppKit/AppKit.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
#include <cstring>
#include <string>
namespace
{
NSString * ToNSString(const char * text)
{
if (text == nullptr || *text == '\0')
{
return nil;
}
return [NSString stringWithUTF8String:text];
}
void SetInitialDirectory(NSOpenPanel * panel, const char * initialDir)
{
NSString * path = ToNSString(initialDir);
if (path == nil)
{
return;
}
BOOL isDirectory = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDirectory] && isDirectory)
{
panel.directoryURL = [NSURL fileURLWithPath:path isDirectory:YES];
}
}
NSArray<UTType *> * ContentTypesFromFilter(const char * fileFilter)
{
if (fileFilter == nullptr || *fileFilter == '\0')
{
return nil;
}
const char * pattern = fileFilter + std::strlen(fileFilter) + 1;
if (*pattern == '\0')
{
return nil;
}
NSMutableArray<UTType *> * types = [NSMutableArray array];
std::string patterns(pattern);
size_t start = 0;
while (start <= patterns.size())
{
const size_t end = patterns.find(';', start);
std::string entry = patterns.substr(start, end == std::string::npos ? std::string::npos : end - start);
if (entry == "*" || entry == "*.*")
{
return nil;
}
if (entry.rfind("*.", 0) == 0)
{
entry.erase(0, 2);
}
else if (!entry.empty() && entry.front() == '.')
{
entry.erase(0, 1);
}
if (!entry.empty() && entry.find_first_of("*?") == std::string::npos)
{
NSString * extension = [NSString stringWithUTF8String:entry.c_str()];
if (extension != nil)
{
UTType * type = [UTType typeWithFilenameExtension:extension];
if (type != nil && ![types containsObject:type])
{
[types addObject:type];
}
}
}
if (end == std::string::npos)
{
break;
}
start = end + 1;
}
return types.count == 0 ? nil : types;
}
} // namespace
namespace MacOSFileDialogs
{
bool FileSelect(const char * initialDir, const char * fileFilter, Path & selected)
{
@autoreleasepool
{
NSOpenPanel * panel = [NSOpenPanel openPanel];
panel.canChooseFiles = YES;
panel.canChooseDirectories = NO;
panel.allowsMultipleSelection = NO;
panel.resolvesAliases = YES;
SetInitialDirectory(panel, initialDir);
NSArray<UTType *> * contentTypes = ContentTypesFromFilter(fileFilter);
if (contentTypes != nil)
{
panel.allowedContentTypes = contentTypes;
}
if ([panel runModal] != NSModalResponseOK)
{
return false;
}
NSURL * url = panel.URL;
if (url == nil || !url.fileURL)
{
return false;
}
selected = Path(url.path.fileSystemRepresentation);
return true;
}
}
Path BrowseForDirectory(const char * title)
{
@autoreleasepool
{
NSOpenPanel * panel = [NSOpenPanel openPanel];
panel.canChooseFiles = NO;
panel.canChooseDirectories = YES;
panel.allowsMultipleSelection = NO;
panel.canCreateDirectories = YES;
panel.resolvesAliases = YES;
NSString * message = ToNSString(title);
if (message != nil)
{
panel.message = message;
}
if ([panel runModal] != NSModalResponseOK)
{
return {};
}
NSURL * url = panel.URL;
if (url == nil || !url.fileURL)
{
return {};
}
return Path(url.path.fileSystemRepresentation, "");
}
}
} // namespace MacOSFileDialogs

View File

@ -0,0 +1,9 @@
#pragma once
void NxEmuMacOSApplyWindowStyle(const void * nativeWindow);
void * NxEmuMacOSCreateRenderView(const void * nativeWindow);
void NxEmuMacOSDestroyRenderView(void * renderView);
void * NxEmuMacOSGetRenderSurface(void * renderView);
void NxEmuMacOSLayoutRenderView(void * renderView, int x, int y, int width, int height);
void NxEmuMacOSSetRenderViewVisible(void * renderView, bool visible);
float NxEmuMacOSWindowScale(const void * nativeWindow);

View File

@ -0,0 +1,171 @@
#import <AppKit/AppKit.h>
#import <QuartzCore/CAMetalLayer.h>
#include "render_window_macos.h"
@interface NxEmuMetalRenderView : NSView
- (void)updateDrawableSize;
@end
@implementation NxEmuMetalRenderView
- (BOOL)wantsUpdateLayer
{
return YES;
}
- (CALayer *)makeBackingLayer
{
return [CAMetalLayer layer];
}
- (void)updateDrawableSize
{
CAMetalLayer * metalLayer = (CAMetalLayer *)self.layer;
if (metalLayer == nil)
{
return;
}
const CGFloat scale = self.window != nil ? self.window.backingScaleFactor : 1.0;
// Retina sizes the presentation surface only. Guest render targets are
// scaled separately by the video resolution setting; do not multiply that
// setting by backingScaleFactor or force the layer to 1x.
metalLayer.contentsScale = scale;
metalLayer.drawableSize = [self convertRectToBacking:self.bounds].size;
}
- (void)viewDidMoveToWindow
{
[super viewDidMoveToWindow];
[self updateDrawableSize];
}
- (void)viewDidChangeBackingProperties
{
[super viewDidChangeBackingProperties];
[self updateDrawableSize];
}
- (void)setFrameSize:(NSSize)newSize
{
[super setFrameSize:newSize];
[self updateDrawableSize];
}
- (NSView *)hitTest:(NSPoint)point
{
(void)point;
return nil;
}
@end
static NSWindow * NxEmuGetSciterWindow(const void * nativeWindowHandle)
{
return (NSWindow *)const_cast<void *>(nativeWindowHandle);
}
void NxEmuMacOSApplyWindowStyle(const void * nativeWindowHandle)
{
NSWindow * window = NxEmuGetSciterWindow(nativeWindowHandle);
if (window == nil)
{
return;
}
window.titleVisibility = NSWindowTitleVisible;
window.titlebarAppearsTransparent = NO;
window.movableByWindowBackground = NO;
window.hasShadow = YES;
// A standard main window must have an opaque background, including the
// status bar. A clear background can retain per-pixel click-through regions.
window.opaque = YES;
window.backgroundColor = [NSColor windowBackgroundColor];
window.tabbingMode = NSWindowTabbingModeDisallowed;
window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
if (@available(macOS 14.0, *))
{
[NSApp activate];
}
else
{
[NSApp activateIgnoringOtherApps:YES];
}
[window makeKeyAndOrderFront:nil];
}
void * NxEmuMacOSCreateRenderView(const void * nativeWindowHandle)
{
NSWindow * window = NxEmuGetSciterWindow(nativeWindowHandle);
NSView * parentView = window != nil ? window.contentView : nil;
if (parentView == nil)
{
return nullptr;
}
NxEmuMetalRenderView * renderView = [[NxEmuMetalRenderView alloc] initWithFrame:NSZeroRect];
renderView.wantsLayer = YES;
CAMetalLayer * metalLayer = (CAMetalLayer *)renderView.layer;
if (metalLayer == nil)
{
[renderView release];
return nullptr;
}
metalLayer.opaque = YES;
renderView.hidden = YES;
[parentView addSubview:renderView positioned:NSWindowAbove relativeTo:nil];
[renderView updateDrawableSize];
return renderView;
}
void NxEmuMacOSDestroyRenderView(void * renderViewHandle)
{
NSView * renderView = (NSView *)renderViewHandle;
if (renderView == nil)
{
return;
}
[renderView removeFromSuperview];
[renderView release];
}
void * NxEmuMacOSGetRenderSurface(void * renderViewHandle)
{
NSView * renderView = (NSView *)renderViewHandle;
return renderView != nil ? (void *)renderView.layer : nullptr;
}
void NxEmuMacOSLayoutRenderView(void * renderViewHandle, int x, int y, int width, int height)
{
NxEmuMetalRenderView * renderView = (NxEmuMetalRenderView *)renderViewHandle;
if (renderView == nil || renderView.superview == nil || width <= 0 || height <= 0)
{
return;
}
const NSRect parentBounds = renderView.superview.bounds;
const CGFloat originY = renderView.superview.isFlipped ? y : parentBounds.size.height - y - height;
renderView.frame = NSMakeRect(x, originY, width, height);
[renderView updateDrawableSize];
}
void NxEmuMacOSSetRenderViewVisible(void * renderViewHandle, bool visible)
{
NSView * renderView = (NSView *)renderViewHandle;
if (renderView != nil)
{
renderView.hidden = !visible;
}
}
float NxEmuMacOSWindowScale(const void * nativeWindowHandle)
{
NSWindow * window = NxEmuGetSciterWindow(nativeWindowHandle);
return window != nil ? (float)window.backingScaleFactor : 1.0f;
}

View File

@ -10,6 +10,9 @@
#include "user_interface/html_utils.h"
#include "user_interface/key_mappings.h"
#include "user_interface/notification.h"
#ifdef __APPLE__
#include "user_interface/render_window_macos.h"
#endif
#include <common/path.h>
#include <common/shell_open.h>
#include <common/std_string.h>
@ -364,6 +367,16 @@ const std::string * SciterMainWindow::MenuIconSvg(GuiAction action)
SciterMainWindow::~SciterMainWindow()
{
if (m_window != nullptr)
{
m_window->OnCloseSinkRemove(this);
m_window->OnDestroySinkRemove(this);
if (m_menuBar)
{
m_menuBar->RemoveSink(this);
}
}
Notification::GetInstance().ClearSciterContext();
m_ProfileSelect.Detach();
m_WebBrowser.DetachWindow();
@ -388,19 +401,45 @@ SciterMainWindow::~SciterMainWindow()
settings.UnregisterCallback(NXUISetting::HideMouseOnInactivity, SciterMainWindow::SettingChanged, this);
settings.UnregisterCallback(NXUISetting::EnableDiscordPresence, SciterMainWindow::SettingChanged, this);
if (m_rootElement.IsValid())
{
m_rootElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE);
}
if (m_firmwareInstallThread.joinable())
{
m_firmwareInstallThread.join();
}
m_systemConfig.reset(nullptr);
m_inputConfig.reset(nullptr);
m_gameConfig.reset(nullptr);
m_aboutDialog.reset(nullptr);
settings.SetBool(NXCoreSetting::ShuttingDown, true);
if (m_romBrowser)
{
// Stop launches a fresh library scan. A native close has already
// cleared m_rootElement, but that worker must still be joined before
// its loader module is destroyed.
m_romBrowser->StopScan();
m_romBrowser->SetMainWindow(nullptr, nullptr);
}
if (m_romBrowser && m_rootElement.IsValid())
{
m_romBrowser->ClearItems();
}
m_modules.ShutDown();
#ifdef __APPLE__
NxEmuMacOSDestroyRenderView(m_renderWindow);
m_renderWindow = nullptr;
#endif
if (m_window != nullptr)
{
// File -> Exit stops the loop without a native close notification. Close
// while this owner is alive, so shutdown cannot dispatch into dead sinks.
m_window->Destroy();
m_window = nullptr;
m_rootElement = {};
}
}
void SciterMainWindow::RegisterApplets()
@ -615,11 +654,20 @@ bool SciterMainWindow::Show()
WINDOW_WIDTH = 760,
};
if (!m_sciterUI.WindowCreate(nullptr, "main_window.html", 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SUIW_MAIN | SUIW_HIDDEN, m_window))
#ifdef __APPLE__
constexpr const char * mainDocument = "main_window_macos.html";
#else
constexpr const char * mainDocument = "main_window.html";
#endif
if (!m_sciterUI.WindowCreate(nullptr, mainDocument, 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SUIW_MAIN | SUIW_HIDDEN, m_window))
{
return false;
}
m_rootElement = m_window->GetRootElement();
#ifdef __APPLE__
NxEmuMacOSApplyWindowStyle(m_window->GetHandle());
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
#endif
m_sciterUI.AttachHandler(m_rootElement, IID_IKEYSINK, (IKeySink *)this);
m_sciterUI.AttachHandler(m_rootElement, IID_EVENTSINK, (IEventSink *)this);
m_window->OnCloseSinkAdd(this);
@ -654,7 +702,12 @@ bool SciterMainWindow::Show()
UpdateDiscordPresence();
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("dockedMode"), IID_ICLICKSINK, (IClickSink *)this);
#ifdef _WIN32
// NXEmu's OpenGL context implementation uses WGL. Other platforms
// currently provide Vulkan contexts only.
SciterElement(m_rootElement.GetElementByID("renderer")).AddClassName("interactive");
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("renderer"), IID_ICLICKSINK, (IClickSink *)this);
#endif
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("gpuCommandSynchronization"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("volume"), IID_ICLICKSINK, (IClickSink *)this);
m_sciterUI.AttachHandler(m_rootElement.GetElementByID("volumePopupBtn"), IID_ICLICKSINK, (IClickSink *)this);
@ -734,6 +787,12 @@ void SciterMainWindow::LoadGame(const char * path, int32_t program_index, Applic
m_romBrowser->SetMainWindow(this, nullptr);
}
// Configuration pages retain controller and loader interfaces from the
// current modules. Retire them before Setup replaces those modules, even
// when a game is launched while a child window is still open.
m_inputConfig.reset();
m_systemConfig.reset();
m_gameConfig.reset();
m_modules.Setup(*this);
if (!m_modules.IsValid())
{
@ -893,6 +952,9 @@ void SciterMainWindow::CreateRenderWindow()
m_renderWindow = CreateWindowExW(0, L"Static", L"", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
rect.left, rect.top, width, height, (HWND)m_window->GetHandle(), nullptr, GetModuleHandle(nullptr), nullptr);
ShowWindow((HWND)m_renderWindow, SW_HIDE);
#elif defined(__APPLE__)
m_renderWindow = NxEmuMacOSCreateRenderView(m_window->GetHandle());
NxEmuMacOSLayoutRenderView(m_renderWindow, rect.left, rect.top, width, height);
#else
(void)width;
(void)height;
@ -1031,6 +1093,8 @@ void SciterMainWindow::EmulationRunning(const char * /*setting*/, void * userDat
{
#ifdef _WIN32
DestroyWindow((HWND)impl->m_renderWindow);
#elif defined(__APPLE__)
NxEmuMacOSDestroyRenderView(impl->m_renderWindow);
#endif
impl->m_renderWindow = nullptr;
}
@ -1071,6 +1135,8 @@ void SciterMainWindow::ShowPanel(Panel panel)
}
#ifdef _WIN32
ShowWindow((HWND)m_renderWindow, panel == Panel::Renderer ? SW_SHOW : SW_HIDE);
#elif defined(__APPLE__)
NxEmuMacOSSetRenderViewVisible(m_renderWindow, panel == Panel::Renderer);
#endif
m_sciterUI.UpdateWindow(m_rootElement.GetElementHwnd(true));
}
@ -1849,12 +1915,13 @@ void SciterMainWindow::OnRecetGame(uint32_t fileIndex)
void SciterMainWindow::OnWindowDestroy(HWINDOW /*hWnd*/)
{
m_ProfileSelect.Detach();
m_ProfileSelect.DetachAfterWindowDestroyed();
m_WebBrowser.DetachWindow();
if (m_rootElement.IsValid())
{
m_rootElement.SetTimer(0, (uint32_t *)TIMER_UPDATE_INSTALL_FIRMWARE);
}
m_window = nullptr;
m_rootElement = {};
m_sciterUI.Stop();
}
@ -1961,7 +2028,11 @@ void SciterMainWindow::OnGuiAction(GuiAction action)
void * SciterMainWindow::RenderSurface() const
{
#ifdef __APPLE__
return NxEmuMacOSGetRenderSurface(m_renderWindow);
#else
return m_renderWindow;
#endif
}
float SciterMainWindow::PixelRatio() const
@ -2000,6 +2071,9 @@ float SciterMainWindow::PixelRatio() const
return static_cast<float>(dpi) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
}
}
#endif
#ifdef __APPLE__
return NxEmuMacOSWindowScale(m_window != nullptr ? m_window->GetHandle() : nullptr);
#endif
return 1.0f;
}
@ -2118,6 +2192,8 @@ void SciterMainWindow::LayoutRenderWindow()
uint32_t height = rect.bottom - rect.top;
#ifdef _WIN32
MoveWindow((HWND)m_renderWindow, rect.left, rect.top, width, height, false);
#elif defined(__APPLE__)
NxEmuMacOSLayoutRenderView(m_renderWindow, rect.left, rect.top, width, height);
#else
(void)width;
(void)height;
@ -2400,6 +2476,7 @@ bool SciterMainWindow::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, ui
}
else if (element == rootElement.GetElementByID("renderer"))
{
#ifdef _WIN32
SettingsStore & settings = SettingsStore::GetInstance();
RendererBackend graphicsAPI = (RendererBackend)settings.GetInt(NXVideoSetting::GraphicsAPI);
if (graphicsAPI == RendererBackend::Vulkan)
@ -2417,6 +2494,7 @@ bool SciterMainWindow::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, ui
{
m_modules.FlushSettings();
}
#endif
}
else if (source == rootElement.GetElementByID("gpuCommandSynchronization"))
{

View File

@ -58,7 +58,19 @@ GameConfig::GameConfig(ISciterUI & sciterUI, SystemModules & modules) :
{
}
GameConfig::~GameConfig() = default;
GameConfig::~GameConfig()
{
if (m_window != nullptr)
{
// Replacing a dialog must retire its callbacks before its owner dies.
if (m_revertAddonsOnClose && m_gameConfigAddons)
{
m_gameConfigAddons->RevertSetting();
}
m_window->OnDestroySinkRemove(this);
m_window->Destroy();
}
}
void GameConfig::Display(void * parentWindow, const char * gamePath)
{
@ -273,7 +285,6 @@ bool GameConfig::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/, uint
if (m_window != nullptr)
{
m_window->Destroy();
m_window = nullptr;
}
}
return true;

View File

@ -24,6 +24,17 @@ InputConfig::InputConfig(ISciterUI & SciterUI, SystemModules & modules) :
InputConfig::~InputConfig()
{
if (m_pageNav)
{
m_pageNav->RemoveSink(this);
}
if (m_window != nullptr)
{
m_window->OnCloseSinkRemove(this);
m_window->OnDestroySinkRemove(this);
m_window->Destroy();
m_window = nullptr;
}
ShutdownPlayers();
if (m_inputDeviceList != nullptr)
{
@ -157,6 +168,11 @@ void InputConfig::OnWindowDestroy(HWINDOW hWnd)
{
return;
}
if (m_pageNav)
{
m_pageNav->RemoveSink(this);
m_pageNav.reset();
}
ShutdownPlayers();
if (m_inputDeviceList != nullptr)
{

View File

@ -34,6 +34,35 @@ SystemConfig::SystemConfig(ISciterUI & SciterUI, SystemModules & modules, std::v
SystemConfig::~SystemConfig()
{
if (m_pageNav)
{
m_pageNav->RemoveSink(this);
}
if (m_window != nullptr)
{
m_window->OnDestroySinkRemove(this);
m_window->Destroy();
}
}
void SystemConfig::OnWindowDestroy(HWINDOW hWnd)
{
if (m_window == nullptr || m_window->GetHandle() != hWnd)
{
return;
}
m_window = nullptr;
if (m_pageNav)
{
m_pageNav->RemoveSink(this);
m_pageNav.reset();
}
m_systemConfigGraphics.reset();
m_systemConfigAudio.reset();
m_systemConfigGeneral.reset();
m_systemConfigGameBrowser.reset();
m_systemConfigProfiles.reset();
m_systemConfigSystemTab.reset();
}
void SystemConfig::Display(void * parentWindow, const char * startPage)
@ -57,6 +86,7 @@ void SystemConfig::Display(void * parentWindow, const char * startPage)
{
return;
}
m_window->OnDestroySinkAdd(this);
SciterElement root(m_window->GetRootElement());
if (root.IsValid())
{
@ -408,7 +438,9 @@ void SystemConfig::InitializeTranslations()
}
m_settingTranslations.insert({ TranslationType::RendererBackend, {
#ifdef _WIN32
{(uint32_t)RendererBackend::OpenGL, "OpenGL"},
#endif
{(uint32_t)RendererBackend::Vulkan, "Vulkan"},
{(uint32_t)RendererBackend::Null, "Null"},
}});

View File

@ -21,7 +21,8 @@ class SystemModules;
class SystemConfig :
public IPagesSink,
public IClickSink
public IClickSink,
public IWindowDestroySink
{
public:
enum class TranslationType : uint32_t
@ -70,6 +71,7 @@ public:
//IClickSink
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
void OnWindowDestroy(HWINDOW hWnd) override;
private:
SystemConfig() = delete;

View File

@ -71,6 +71,7 @@ class WidgetRomBrowser :
public ITimerSink,
public IClickSink,
public IDoubleClickSink,
public IContextMenuSink,
public IRomBrowser
{
friend class RomListWorker;
@ -89,6 +90,8 @@ public:
// IClickSink
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
bool OnContextMenu(SCITER_ELEMENT element, SCITER_ELEMENT source, int32_t x, int32_t y) override;
// IDoubleClickSink
bool OnDoubleClick(SCITER_ELEMENT element, SCITER_ELEMENT source) override;
@ -276,16 +279,32 @@ bool WidgetRomBrowser::OnEvent(SCITER_ELEMENT element, SCITER_ELEMENT source, ui
if (event_code == (uint32_t)SciterBehaviorEvent::ContextMenuRequest)
{
SciterElement card = FindRomCard(source);
if (!card.IsValid()) card = FindRomCard(element);
if (card.IsValid())
{
SelectRomCard(card);
m_contextMenuPath = card.GetAttribute("data-path");
const auto bounds = card.GetLocation(SciterElement::VIEW_RELATIVE | SciterElement::BORDER_BOX);
return OnContextMenu(element, source, bounds.left, bounds.bottom);
}
return false;
}
return false;
}
bool WidgetRomBrowser::OnContextMenu(SCITER_ELEMENT element, SCITER_ELEMENT source, int32_t x, int32_t y)
{
SciterElement card = FindRomCard(source);
if (!card.IsValid()) card = FindRomCard(element);
if (!card.IsValid()) return false;
SciterElement menu(m_rootElement.GetRoot().GetElementByID("RomCardContextMenu"));
if (!menu.IsValid()) return false;
SelectRomCard(card);
m_contextMenuPath = card.GetAttribute("data-path");
// Placement 7 anchors the top-left at a point in view coordinates. The
// macOS backend uses Cocoa's context menu and native cursor coordinates.
m_sciterUI.PopupShowAt(menu, {x, y}, 7);
return true;
}
bool WidgetRomBrowser::OnTimer(SCITER_ELEMENT /*element*/, uint32_t * timerId)
{
if (timerId == (uint32_t *)TIMER_UPDATE_UI)
@ -491,6 +510,7 @@ void WidgetRomBrowser::Attached(SCITER_ELEMENT element, IBaseElement * baseEleme
m_rootElement = element;
m_sciterUI.AttachHandler(m_rootElement, IID_EVENTSINK, (IEventSink *)this);
m_sciterUI.AttachHandler(m_rootElement, IID_ITIMERSINK, (ITimerSink *)this);
m_sciterUI.AttachHandler(m_rootElement, IID_ICONTEXTMENUSINK, (IContextMenuSink *)this);
AttachContextMenuHandlers();
SettingsStore & settings = SettingsStore::GetInstance();
@ -713,4 +733,4 @@ bool RomListWorker::HasSupportedFileExtension(const std::string & file_name)
bool Register_WidgetRomBrowser(ISciterUI & sciterUI)
{
return WidgetRomBrowser::Register(sciterUI);
}
}

View File

@ -5,8 +5,8 @@
// sandbox. Enough for CheckVulkan (headless OpenLibrary + CreateInstance).
#include <array>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <future>
#include <optional>
#include <span>
@ -22,6 +22,20 @@
#include <vulkan/vk_enum_string_helper.h>
namespace Vulkan {
namespace {
#ifdef __APPLE__
std::filesystem::path AppleVulkanLibraryPath() {
const auto exe_dir = Common::FS::GetExeDirectory();
if (exe_dir.filename() == "MacOS" && exe_dir.parent_path().filename() == "Contents") {
return exe_dir.parent_path() / "Frameworks/libMoltenVK.dylib";
}
return exe_dir.empty() ? std::filesystem::path{} : exe_dir / "libMoltenVK.dylib";
}
#endif
} // namespace
namespace vk {
namespace {
@ -174,6 +188,11 @@ namespace {
switch (window_type) {
case WindowSystemType::Headless:
break;
#ifdef __APPLE__
case WindowSystemType::Metal:
extensions.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
break;
#endif
}
if (window_type != WindowSystemType::Headless) {
extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
@ -223,15 +242,12 @@ std::shared_ptr<Common::DynamicLibrary> OpenLibrary() {
LOG_DEBUG(Render_Vulkan, "Looking for a Vulkan library");
auto library = std::make_shared<Common::DynamicLibrary>();
#ifdef __APPLE__
const auto libvulkan_filename =
Common::FS::GetBundleDirectory() / "Contents/Frameworks/libvulkan.1.dylib";
const auto libmoltenvk_filename =
Common::FS::GetBundleDirectory() / "Contents/Frameworks/libMoltenVK.dylib";
const char* library_paths[] = {std::getenv("LIBVULKAN_PATH"), libvulkan_filename.c_str(),
libmoltenvk_filename.c_str()};
for (const auto& library_path : library_paths) {
if (library_path && library->Open(library_path)) {
break;
const auto library_path = AppleVulkanLibraryPath();
if (!library_path.empty()) {
const std::string path = library_path.string();
LOG_DEBUG(Render_Vulkan, "Trying Vulkan library: {}", path);
if (library->Open(path.c_str())) {
LOG_INFO(Render_Vulkan, "Loaded Vulkan library: {}", path);
}
}
#else

View File

@ -38,6 +38,9 @@ namespace Vulkan {
// matching CreateInstance's default in vulkan_instance.h.
enum class WindowSystemType {
Headless,
#ifdef __APPLE__
Metal,
#endif
};
namespace vk {

14
src/script/build_macos.sh Normal file
View File

@ -0,0 +1,14 @@
#!/bin/sh
set -eu
configuration="${1:-Release}"
build_directory="${2:-build}"
jobs="${NXEMU_BUILD_JOBS:-2}"
cd "$(dirname "$0")/../.."
cmake -S . -B "$build_directory" -G Ninja \
-DCMAKE_BUILD_TYPE="$configuration" \
-DCMAKE_OSX_ARCHITECTURES=arm64 \
-DNXEMU_MACOS_BUNDLE=ON
cmake --build "$build_directory" --target nxemu_bundle --parallel "$jobs"

View File

@ -0,0 +1,21 @@
#!/bin/sh
set -eu
dmg_file="${1:-NxEmu-macos-arm64.dmg}"
configuration="${2:-Release}"
cd "$(dirname "$0")/../.."
app="bin/arm64/$configuration/NxEmu.app"
codesign --verify --deep --strict "$app"
staging_directory="$(mktemp -d "${TMPDIR:-/tmp}/nxemu-dmg.XXXXXX")"
trap 'rm -rf "$staging_directory"' EXIT
trap 'exit 1' HUP INT TERM
ditto "$app" "$staging_directory/NxEmu.app"
ln -s /Applications "$staging_directory/Applications"
mkdir -p package
hdiutil create -volname NxEmu -srcfolder "$staging_directory" \
-fs HFS+ -format UDZO -ov "package/$dmg_file"

View File

@ -18,6 +18,12 @@ list(FILTER YUZU_AUDIO_CORE_SOURCES EXCLUDE REGEX ".*/oboe_sink\\.cpp$")
add_library(yuzu_audio_core STATIC ${YUZU_AUDIO_CORE_SOURCES})
if(APPLE AND ENABLE_SDL2)
target_sources(yuzu_audio_core PRIVATE sink/sdl2_sink.cpp)
target_compile_definitions(yuzu_audio_core PRIVATE HAVE_SDL2)
target_link_libraries(yuzu_audio_core PRIVATE SDL2::SDL2)
endif()
target_include_directories(yuzu_audio_core PUBLIC
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/nxemu-os

View File

@ -106,6 +106,13 @@ public:
#else
const fs::path exe_dir = GetExeDirectory();
yuzu_path = exe_dir.empty() ? fs::path{} : exe_dir / PORTABLE_DIR;
#ifdef __APPLE__
// A portable user directory belongs beside the .app, outside its signed
// contents. This also preserves data from a previous raw executable.
if (exe_dir.filename() == "MacOS" && exe_dir.parent_path().filename() == "Contents") {
yuzu_path = exe_dir.parent_path().parent_path().parent_path() / PORTABLE_DIR;
}
#endif
if (!IsDir(yuzu_path)) {
yuzu_path = GetCurrentDir() / PORTABLE_DIR;
}

View File

@ -9,7 +9,7 @@
#include <windows.h>
#include "yuzu_common/dynamic_library.h"
#elif defined(__linux__) || defined(__FreeBSD__) // ^^^ Windows ^^^ vvv Linux vvv
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) // ^^^ Windows ^^^ vvv POSIX vvv
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
@ -17,8 +17,13 @@
#include <boost/icl/interval_set.hpp>
#include <fcntl.h>
#include <sys/mman.h>
#if defined(__linux__)
#include <sys/random.h>
#endif
#include <unistd.h>
#if defined(__APPLE__)
#include <stdlib.h>
#endif
#if defined(__ANDROID__)
#include <sys/syscall.h>
#endif
@ -27,9 +32,13 @@
#ifndef MAP_NORESERVE
#define MAP_NORESERVE 0
#endif
#if defined(__APPLE__) && !defined(MAP_ANONYMOUS)
#define MAP_ANONYMOUS MAP_ANON
#endif
#endif // ^^^ Linux ^^^
#endif // ^^^ POSIX ^^^
#include <cstdio>
#include <mutex>
#include <random>
@ -41,9 +50,22 @@
namespace Common {
constexpr size_t PageAlignment = 0x1000;
constexpr size_t GuestPageSize = 0x1000;
constexpr size_t HugePageSize = 0x200000;
static size_t GetHostPageSize() {
#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__)
static const size_t host_page_size = [] {
const long value = sysconf(_SC_PAGESIZE);
ASSERT_MSG(value > 0, "sysconf(_SC_PAGESIZE) failed");
return static_cast<size_t>(value);
}();
return host_page_size;
#else
return GuestPageSize;
#endif
}
#ifdef _WIN32
// Manually imported for MinGW compatibility
@ -367,9 +389,16 @@ private:
std::unordered_map<size_t, size_t> placeholder_host_pointers; ///< Placeholder backing offset
};
#elif defined(__linux__) || defined(__FreeBSD__) // ^^^ Windows ^^^ vvv Linux vvv
#elif defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) // ^^^ Windows ^^^ vvv POSIX vvv
#if defined(_M_ARM64) || defined(ARCHITECTURE_arm64)
#if defined(__APPLE__)
static void* ChooseVirtualBase(size_t virtual_size) {
return mmap(nullptr, virtual_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
}
#elif defined(_M_ARM64) || defined(ARCHITECTURE_arm64)
static void* ChooseVirtualBase(size_t virtual_size) {
constexpr uintptr_t Map39BitSize = (1ULL << 39);
@ -444,21 +473,39 @@ public:
}
};
long page_size = sysconf(_SC_PAGESIZE);
if (page_size != 0x1000) {
LOG_CRITICAL(HW_Memory, "page size {:#x} is incompatible with 4K paging", page_size);
#if !defined(__APPLE__)
if (host_page_size != GuestPageSize) {
LOG_CRITICAL(HW_Memory, "page size {:#x} is incompatible with 4K paging",
host_page_size);
throw std::bad_alloc{};
}
#endif
// Backing memory initialization
#if defined(__FreeBSD__) && __FreeBSD__ < 13
// XXX Drop after FreeBSD 12.* reaches EOL on 2024-06-30
fd = shm_open(SHM_ANON, O_RDWR, 0600);
#elif defined(__APPLE__)
// Keep a shared backing store for the page-table path without exposing
// a 4 KiB fastmem arena on 16 KiB hosts.
for (int attempt = 0; attempt < 16; ++attempt) {
char name[32];
std::snprintf(name, sizeof(name), "/nxemu-%08x%08x", arc4random(), arc4random());
fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600);
if (fd >= 0) {
shm_unlink(name);
break;
}
if (errno != EEXIST) {
break;
}
}
#else
fd = memfd_create("HostMemory", 0);
#endif
if (fd < 0) {
LOG_CRITICAL(HW_Memory, "memfd_create failed: {}", strerror(errno));
LOG_CRITICAL(HW_Memory, "Failed to create HostMemory backing file: {}",
strerror(errno));
throw std::bad_alloc{};
}
@ -477,7 +524,20 @@ public:
throw std::bad_alloc{};
}
// Virtual memory initialization
#if defined(__APPLE__)
if (host_page_size != GuestPageSize) {
LOG_INFO(HW_Memory,
"Fastmem disabled: host page size {:#x} does not match guest page size {:#x}; "
"using the page-table memory path",
host_page_size, GuestPageSize);
virtual_base = nullptr;
good = true;
return;
}
#endif
// Virtual memory initialization. This path is only used when the host can represent
// the guest's 4 KiB mappings directly.
virtual_base = virtual_map_base = static_cast<u8*>(ChooseVirtualBase(virtual_size));
if (virtual_base == MAP_FAILED) {
LOG_CRITICAL(HW_Memory, "mmap failed: {}", strerror(errno));
@ -498,6 +558,9 @@ public:
void Map(size_t virtual_offset, size_t host_offset, size_t length, MemoryPermission perms) {
// Intersect the range with our address space.
AdjustMap(&virtual_offset, &length);
if (length == 0) {
return;
}
// We are removing a placeholder.
free_manager.AllocateBlock(virtual_base + virtual_offset, length);
@ -527,6 +590,9 @@ public:
// Intersect the range with our address space.
AdjustMap(&virtual_offset, &length);
if (length == 0) {
return;
}
// Merge with any adjacent placeholder mappings.
auto [merged_pointer, merged_size] =
@ -540,6 +606,9 @@ public:
void Protect(size_t virtual_offset, size_t length, bool read, bool write, bool execute) {
// Intersect the range with our address space.
AdjustMap(&virtual_offset, &length);
if (length == 0) {
return;
}
int flags = PROT_NONE;
if (read) {
@ -553,6 +622,7 @@ public:
flags |= PROT_EXEC;
}
#endif
int ret = mprotect(virtual_base + virtual_offset, length, flags);
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
}
@ -576,6 +646,7 @@ public:
const size_t backing_size; ///< Size of the backing memory in bytes
const size_t virtual_size; ///< Size of the virtual address placeholder in bytes
const size_t host_page_size{GetHostPageSize()};
u8* backing_base{reinterpret_cast<u8*>(MAP_FAILED)};
u8* virtual_base{reinterpret_cast<u8*>(MAP_FAILED)};
@ -621,11 +692,11 @@ private:
}
}
int fd{-1}; // memfd file descriptor, -1 is the error value of memfd_create
int fd{-1}; // Shared backing file descriptor; -1 means no file.
FreeRegionManager free_manager{};
};
#else // ^^^ Linux ^^^ vvv Generic vvv
#else // ^^^ POSIX ^^^ vvv Generic vvv
class HostMemory::Impl {
public:
@ -659,8 +730,9 @@ HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_)
// Try to allocate a fastmem arena.
// The implementation will fail with std::bad_alloc on errors.
impl =
std::make_unique<HostMemory::Impl>(AlignUp(backing_size, PageAlignment),
AlignUp(virtual_size, PageAlignment) + HugePageSize);
std::make_unique<HostMemory::Impl>(AlignUp(backing_size, GetHostPageSize()),
AlignUp(virtual_size, GetHostPageSize()) +
HugePageSize);
backing_base = impl->backing_base;
virtual_base = impl->virtual_base;
@ -688,9 +760,9 @@ HostMemory& HostMemory::operator=(HostMemory&&) noexcept = default;
void HostMemory::Map(size_t virtual_offset, size_t host_offset, size_t length,
MemoryPermission perms, bool separate_heap) {
ASSERT(virtual_offset % PageAlignment == 0);
ASSERT(host_offset % PageAlignment == 0);
ASSERT(length % PageAlignment == 0);
ASSERT(virtual_offset % GuestPageSize == 0);
ASSERT(host_offset % GuestPageSize == 0);
ASSERT(length % GuestPageSize == 0);
ASSERT(virtual_offset + length <= virtual_size);
ASSERT(host_offset + length <= backing_size);
if (length == 0 || !virtual_base || !impl) {
@ -700,8 +772,8 @@ void HostMemory::Map(size_t virtual_offset, size_t host_offset, size_t length,
}
void HostMemory::Unmap(size_t virtual_offset, size_t length, bool separate_heap) {
ASSERT(virtual_offset % PageAlignment == 0);
ASSERT(length % PageAlignment == 0);
ASSERT(virtual_offset % GuestPageSize == 0);
ASSERT(length % GuestPageSize == 0);
ASSERT(virtual_offset + length <= virtual_size);
if (length == 0 || !virtual_base || !impl) {
return;
@ -710,8 +782,8 @@ void HostMemory::Unmap(size_t virtual_offset, size_t length, bool separate_heap)
}
void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission perm) {
ASSERT(virtual_offset % PageAlignment == 0);
ASSERT(length % PageAlignment == 0);
ASSERT(virtual_offset % GuestPageSize == 0);
ASSERT(length % GuestPageSize == 0);
ASSERT(virtual_offset + length <= virtual_size);
if (length == 0 || !virtual_base || !impl) {
return;

View File

@ -66,7 +66,8 @@ public:
}
bool IsInVirtualRange(void* address) const noexcept {
return address >= virtual_base && address < virtual_base + virtual_size;
return virtual_base != nullptr && address >= virtual_base &&
address < virtual_base + virtual_size;
}
private:

View File

@ -14,7 +14,7 @@ template <typename T>
struct fmt::formatter<T, std::enable_if_t<std::is_enum_v<T>, char>>
: formatter<std::underlying_type_t<T>> {
template <typename FormatContext>
auto format(const T& value, FormatContext& ctx) -> decltype(ctx.out()) {
auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
return fmt::formatter<std::underlying_type_t<T>>::format(
static_cast<std::underlying_type_t<T>>(value), ctx);
}

View File

@ -10,7 +10,7 @@
#include <string>
#include <typeindex>
#include <typeinfo>
#include <fmt/core.h>
#include <fmt/format.h>
#include "yuzu_common/common_types.h"
#include "yuzu_common/settings_common.h"

View File

@ -9,6 +9,7 @@
#include "yuzu_common/thread.h"
#ifdef __APPLE__
#include <mach/mach.h>
#include <pthread/qos.h>
#elif defined(_WIN32)
#include <windows.h>
#include "yuzu_common/string_util.h"
@ -58,6 +59,31 @@ void SetCurrentThreadPriority(ThreadPriority new_priority) {
SetThreadPriority(handle, windows_priority);
}
#elif defined(__APPLE__)
void SetCurrentThreadPriority(ThreadPriority new_priority) {
qos_class_t qos_class = QOS_CLASS_DEFAULT;
switch (new_priority) {
case ThreadPriority::Low:
qos_class = QOS_CLASS_UTILITY;
break;
case ThreadPriority::Normal:
qos_class = QOS_CLASS_DEFAULT;
break;
case ThreadPriority::High:
qos_class = QOS_CLASS_USER_INITIATED;
break;
case ThreadPriority::VeryHigh:
case ThreadPriority::Critical:
qos_class = QOS_CLASS_USER_INTERACTIVE;
break;
}
if (const int result = pthread_set_qos_class_self_np(qos_class, 0); result != 0) {
LOG_ERROR(Common, "Failed to set thread QoS class: {}", result);
}
}
#else
void SetCurrentThreadPriority(ThreadPriority new_priority) {

View File

@ -257,7 +257,7 @@ struct fmt::formatter<Common::PhysicalAddress> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Common::PhysicalAddress& addr, FormatContext& ctx) {
auto format(const Common::PhysicalAddress& addr, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{:#x}", static_cast<u64>(addr.GetValue()));
}
};
@ -268,7 +268,7 @@ struct fmt::formatter<Common::ProcessAddress> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Common::ProcessAddress& addr, FormatContext& ctx) {
auto format(const Common::ProcessAddress& addr, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{:#x}", static_cast<u64>(addr.GetValue()));
}
};
@ -279,7 +279,7 @@ struct fmt::formatter<Common::VirtualAddress> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Common::VirtualAddress& addr, FormatContext& ctx) {
auto format(const Common::VirtualAddress& addr, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{:#x}", static_cast<u64>(addr.GetValue()));
}
};

View File

@ -491,7 +491,16 @@ void SDLDriver::PumpEvents() const
{
if (initialized)
{
#ifdef __APPLE__
// Sciter owns the Cocoa event loop; this backend only needs controller
// events. Do not let SDL pump native window events in a host frontend.
SDL_JoystickUpdate();
#else
SDL_PumpEvents();
#endif
// The event watcher already delivered these inputs. Keep motion events
// from accumulating in SDL's queue during long sessions.
SDL_FlushEvents(SDL_JOYAXISMOTION, SDL_CONTROLLERSENSORUPDATE);
}
}
@ -622,9 +631,11 @@ SDLDriver::SDLDriver(std::string input_engine_) :
// Share the same button mapping with non-Nintendo controllers
SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0");
// Disable hidapi driver for xbox. Already default on Windows, this causes conflict with native
// driver on Linux.
// Windows and Linux have native Xbox backends. macOS needs HIDAPI for
// supported USB Xbox controllers, so retain SDL's platform default there.
#ifndef __APPLE__
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_XBOX, "0");
#endif
// If the frontend is going to manage the event loop, then we don't start one here
start_thread = SDL_WasInit(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER) == 0;
@ -665,7 +676,10 @@ SDLDriver::~SDLDriver()
initialized = false;
if (start_thread)
{
vibration_thread.join();
if (vibration_thread.joinable())
{
vibration_thread.join();
}
SDL_QuitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_GAMECONTROLLER);
}
}

View File

@ -184,7 +184,7 @@ struct fmt::formatter<Shader::Backend::GLASM::Id> {
return ctx.begin();
}
template <typename FormatContext>
auto format(Shader::Backend::GLASM::Id id, FormatContext& ctx) {
auto format(Shader::Backend::GLASM::Id id, FormatContext& ctx) const {
return Shader::Backend::GLASM::FormatTo<true>(ctx, id);
}
};
@ -195,7 +195,7 @@ struct fmt::formatter<Shader::Backend::GLASM::Register> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Backend::GLASM::Register& value, FormatContext& ctx) {
auto format(const Shader::Backend::GLASM::Register& value, FormatContext& ctx) const {
if (value.type != Shader::Backend::GLASM::Type::Register) {
throw Shader::InvalidArgument("Register value type is not register");
}
@ -209,7 +209,7 @@ struct fmt::formatter<Shader::Backend::GLASM::ScalarRegister> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Backend::GLASM::ScalarRegister& value, FormatContext& ctx) {
auto format(const Shader::Backend::GLASM::ScalarRegister& value, FormatContext& ctx) const {
if (value.type != Shader::Backend::GLASM::Type::Register) {
throw Shader::InvalidArgument("Register value type is not register");
}
@ -223,7 +223,7 @@ struct fmt::formatter<Shader::Backend::GLASM::ScalarU32> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Backend::GLASM::ScalarU32& value, FormatContext& ctx) {
auto format(const Shader::Backend::GLASM::ScalarU32& value, FormatContext& ctx) const {
switch (value.type) {
case Shader::Backend::GLASM::Type::Void:
break;
@ -244,7 +244,7 @@ struct fmt::formatter<Shader::Backend::GLASM::ScalarS32> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Backend::GLASM::ScalarS32& value, FormatContext& ctx) {
auto format(const Shader::Backend::GLASM::ScalarS32& value, FormatContext& ctx) const {
switch (value.type) {
case Shader::Backend::GLASM::Type::Void:
break;
@ -265,7 +265,7 @@ struct fmt::formatter<Shader::Backend::GLASM::ScalarF32> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Backend::GLASM::ScalarF32& value, FormatContext& ctx) {
auto format(const Shader::Backend::GLASM::ScalarF32& value, FormatContext& ctx) const {
switch (value.type) {
case Shader::Backend::GLASM::Type::Void:
break;
@ -286,7 +286,7 @@ struct fmt::formatter<Shader::Backend::GLASM::ScalarF64> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Backend::GLASM::ScalarF64& value, FormatContext& ctx) {
auto format(const Shader::Backend::GLASM::ScalarF64& value, FormatContext& ctx) const {
switch (value.type) {
case Shader::Backend::GLASM::Type::Void:
break;

View File

@ -250,7 +250,7 @@ struct fmt::formatter<Shader::IR::Attribute> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::Attribute& attribute, FormatContext& ctx) {
auto format(const Shader::IR::Attribute& attribute, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{}", Shader::IR::NameOf(attribute));
}
};

View File

@ -52,7 +52,7 @@ struct fmt::formatter<Shader::IR::Condition> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::Condition& cond, FormatContext& ctx) {
auto format(const Shader::IR::Condition& cond, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{}", Shader::IR::NameOf(cond));
}
};

View File

@ -55,7 +55,7 @@ struct fmt::formatter<Shader::IR::FlowTest> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::FlowTest& flow_test, FormatContext& ctx) {
auto format(const Shader::IR::FlowTest& flow_test, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{}", Shader::IR::NameOf(flow_test));
}
};

View File

@ -103,7 +103,7 @@ struct fmt::formatter<Shader::IR::Opcode> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::Opcode& op, FormatContext& ctx) {
auto format(const Shader::IR::Opcode& op, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{}", Shader::IR::NameOf(op));
}
};

View File

@ -33,7 +33,7 @@ struct fmt::formatter<Shader::IR::Pred> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::Pred& pred, FormatContext& ctx) {
auto format(const Shader::IR::Pred& pred, FormatContext& ctx) const {
if (pred == Shader::IR::Pred::PT) {
return fmt::format_to(ctx.out(), "PT");
} else {

View File

@ -319,7 +319,7 @@ struct fmt::formatter<Shader::IR::Reg> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::Reg& reg, FormatContext& ctx) {
auto format(const Shader::IR::Reg& reg, FormatContext& ctx) const {
if (reg == Shader::IR::Reg::RZ) {
return fmt::format_to(ctx.out(), "RZ");
} else if (static_cast<int>(reg) >= 0 && static_cast<int>(reg) < 255) {

View File

@ -54,7 +54,7 @@ struct fmt::formatter<Shader::IR::Type> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::IR::Type& type, FormatContext& ctx) {
auto format(const Shader::IR::Type& type, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{}", NameOf(type));
}
};

View File

@ -102,7 +102,7 @@ struct fmt::formatter<Shader::Maxwell::Location> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Maxwell::Location& location, FormatContext& ctx) {
auto format(const Shader::Maxwell::Location& location, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{:04x}", location.Offset());
}
};

View File

@ -23,7 +23,7 @@ struct fmt::formatter<Shader::Maxwell::Opcode> {
return ctx.begin();
}
template <typename FormatContext>
auto format(const Shader::Maxwell::Opcode& opcode, FormatContext& ctx) {
auto format(const Shader::Maxwell::Opcode& opcode, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{}", NameOf(opcode));
}
};

View File

@ -334,6 +334,12 @@ if (YUZU_USE_BUNDLED_FFMPEG AND NOT (WIN32 OR ANDROID OR APPLE))
endif()
if(APPLE AND NOT FFmpeg_LIBRARIES)
if(NOT ARCHITECTURE_arm64)
message(FATAL_ERROR
"Bundled macOS FFmpeg libraries are arm64-only. "
"Build with -DCMAKE_OSX_ARCHITECTURES=arm64, or provide "
"FFmpeg_INCLUDE_DIR and FFmpeg_LIBRARIES for the target architecture.")
endif()
set(_ffmpeg_macos_root "${CMAKE_SOURCE_DIR}/external/ffmpeg/lib/macos-arm64")
set(_ffmpeg_macos_libs
"${_ffmpeg_macos_root}/libavfilter.a"
@ -389,6 +395,12 @@ endif()
target_include_directories(video_core PRIVATE ${FFmpeg_INCLUDE_DIR})
target_link_libraries(video_core PRIVATE ${FFmpeg_LIBRARIES})
if(APPLE)
foreach(framework CoreFoundation CoreMedia CoreVideo VideoToolbox)
find_library(NXEMU_${framework}_FRAMEWORK ${framework} REQUIRED)
target_link_libraries(video_core PRIVATE ${NXEMU_${framework}_FRAMEWORK})
endforeach()
endif()
target_link_options(video_core PRIVATE ${FFmpeg_LDFLAGS})
add_dependencies(video_core host_shaders)

View File

@ -25,11 +25,14 @@ namespace
constexpr AVPixelFormat PreferredGpuFormat = AV_PIX_FMT_NV12;
constexpr AVPixelFormat PreferredCpuFormat = AV_PIX_FMT_YUV420P;
constexpr std::array PreferredGpuDecoders = {
#ifdef __APPLE__
AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
#endif
AV_HWDEVICE_TYPE_CUDA,
#ifdef _WIN32
AV_HWDEVICE_TYPE_D3D11VA,
AV_HWDEVICE_TYPE_DXVA2,
#elif defined(__unix__)
#elif defined(__unix__) && !defined(__APPLE__)
AV_HWDEVICE_TYPE_VAAPI,
AV_HWDEVICE_TYPE_VDPAU,
#endif
@ -39,19 +42,28 @@ constexpr std::array PreferredGpuDecoders = {
AVPixelFormat GetGpuFormat(AVCodecContext * codec_context, const AVPixelFormat * pix_fmts)
{
// FFmpeg can change pix_fmt before invoking get_format; retain our selection separately.
auto & hw_pix_fmt = *static_cast<AVPixelFormat *>(codec_context->opaque);
for (const AVPixelFormat * p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
{
if (*p == codec_context->pix_fmt)
if (*p == hw_pix_fmt)
{
return codec_context->pix_fmt;
return hw_pix_fmt;
}
}
LOG_INFO(HW_GPU, "Could not find compatible GPU AV format, falling back to CPU");
av_buffer_unref(&codec_context->hw_device_ctx);
hw_pix_fmt = AV_PIX_FMT_NONE;
codec_context->pix_fmt = PreferredCpuFormat;
return codec_context->pix_fmt;
for (const AVPixelFormat * p = pix_fmts; *p != AV_PIX_FMT_NONE; ++p)
{
if (*p == PreferredCpuFormat)
{
return *p;
}
}
return AV_PIX_FMT_NONE;
}
std::string AVError(int errnum)
@ -227,6 +239,8 @@ DecoderContext::~DecoderContext()
void DecoderContext::InitializeHardwareDecoder(const HardwareContext & context, AVPixelFormat hw_pix_fmt)
{
m_codec_context->hw_device_ctx = av_buffer_ref(context.GetBufferRef());
m_hw_pix_fmt = hw_pix_fmt;
m_codec_context->opaque = &m_hw_pix_fmt;
m_codec_context->get_format = GetGpuFormat;
m_codec_context->pix_fmt = hw_pix_fmt;
}
@ -278,29 +292,25 @@ std::unique_ptr<Frame> DecoderContext::ReceiveFrame(bool * out_is_interlaced)
return true;
};
if (m_codec_context->hw_device_ctx)
if (!ReceiveImpl(dst_frame->GetFrame()))
{
// If we have a hardware context, make a separate frame here to receive the
// hardware result before sending it to the output.
Frame intermediate_frame;
if (!ReceiveImpl(intermediate_frame.GetFrame()))
{
return {};
}
return {};
}
// get_format may have fallen back to software while receiving this frame.
// Transfer only frames that actually own a hardware surface.
if (dst_frame->GetFrame()->hw_frames_ctx)
{
auto hardware_frame = std::move(dst_frame);
dst_frame = std::make_unique<Frame>();
dst_frame->SetFormat(PreferredGpuFormat);
const int ret = av_hwframe_transfer_data(dst_frame->GetFrame(), intermediate_frame.GetFrame(), 0);
const int ret = av_hwframe_transfer_data(dst_frame->GetFrame(), hardware_frame->GetFrame(), 0);
if (ret < 0)
{
LOG_ERROR(HW_GPU, "av_hwframe_transfer_data error: {}", AVError(ret));
return {};
}
}
else
{
// Otherwise, decode the frame as normal.
if (!ReceiveImpl(dst_frame->GetFrame()))
if (av_frame_copy_props(dst_frame->GetFrame(), hardware_frame->GetFrame()) < 0)
{
return {};
}
@ -414,6 +424,12 @@ bool DecodeApi::Initialize(Tegra::Host1x::NvdecCommon::VideoCodec codec)
{
this->Reset();
m_decoder.emplace(codec);
if (!m_decoder->GetCodec())
{
LOG_ERROR(HW_GPU, "FFmpeg build has no decoder for codec {}", codec);
this->Reset();
return false;
}
m_decoder_context.emplace(*m_decoder);
// Enable GPU decoding if requested.

View File

@ -172,6 +172,7 @@ public:
private:
AVCodecContext* m_codec_context{};
AVPixelFormat m_hw_pix_fmt{AV_PIX_FMT_NONE};
};
// Wraps an AVFilterGraph.

View File

@ -10,6 +10,7 @@
#include <vector>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include "frontend/graphics_context.h"
#include "yuzu_common/logging/log.h"

View File

@ -144,6 +144,9 @@ bool Scheduler::UpdateRescaling(bool is_rescaling) {
void Scheduler::WorkerThread(std::stop_token stop_token) {
Common::SetCurrentThreadName("VulkanWorker");
#ifdef __APPLE__
Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
#endif
const auto TryPopQueue{[this](auto& work) -> bool {
if (work_queue.empty()) {

View File

@ -13,7 +13,7 @@
template <>
struct fmt::formatter<VideoCore::Surface::PixelFormat> : fmt::formatter<fmt::string_view> {
template <typename FormatContext>
auto format(VideoCore::Surface::PixelFormat format, FormatContext& ctx) {
auto format(VideoCore::Surface::PixelFormat format, FormatContext& ctx) const {
using VideoCore::Surface::PixelFormat;
const string_view name = [format] {
switch (format) {
@ -234,7 +234,7 @@ struct fmt::formatter<VideoCore::Surface::PixelFormat> : fmt::formatter<fmt::str
template <>
struct fmt::formatter<VideoCommon::ImageType> : fmt::formatter<fmt::string_view> {
template <typename FormatContext>
auto format(VideoCommon::ImageType type, FormatContext& ctx) {
auto format(VideoCommon::ImageType type, FormatContext& ctx) const {
const string_view name = [type] {
using VideoCommon::ImageType;
switch (type) {
@ -262,7 +262,7 @@ struct fmt::formatter<VideoCommon::Extent3D> {
}
template <typename FormatContext>
auto format(const VideoCommon::Extent3D& extent, FormatContext& ctx) {
auto format(const VideoCommon::Extent3D& extent, FormatContext& ctx) const {
return fmt::format_to(ctx.out(), "{{{}, {}, {}}}", extent.width, extent.height,
extent.depth);
}

View File

@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <filesystem>
#include <string>
#include "yuzu_common/dynamic_library.h"
@ -11,6 +12,21 @@
namespace Vulkan
{
#ifdef __APPLE__
namespace {
std::filesystem::path AppleVulkanLibraryPath()
{
const auto exe_dir = Common::FS::GetExeDirectory();
if (exe_dir.filename() == "MacOS" && exe_dir.parent_path().filename() == "Contents") {
return exe_dir.parent_path() / "Frameworks/libMoltenVK.dylib";
}
return exe_dir.empty() ? std::filesystem::path{} : exe_dir / "libMoltenVK.dylib";
}
} // namespace
#endif
std::shared_ptr<Common::DynamicLibrary> OpenLibrary([[maybe_unused]] Core::Frontend::GraphicsContext * context)
{
LOG_DEBUG(Render_Vulkan, "Looking for a Vulkan library");
@ -30,15 +46,14 @@ std::shared_ptr<Common::DynamicLibrary> OpenLibrary([[maybe_unused]] Core::Front
#else
std::shared_ptr<Common::DynamicLibrary> library = std::make_shared<Common::DynamicLibrary>();
#ifdef __APPLE__
const std::filesystem::path libvulkan_filename = Common::FS::GetBundleDirectory() / "Contents/Frameworks/libvulkan.1.dylib";
const std::filesystem::path libmoltenvk_filename = Common::FS::GetBundleDirectory() / "Contents/Frameworks/libMoltenVK.dylib";
const char * library_paths[] = {std::getenv("LIBVULKAN_PATH"), libvulkan_filename.c_str(), libmoltenvk_filename.c_str()};
// Check if a path to a specific Vulkan library has been specified.
for (const char * library_path : library_paths)
const auto library_path = AppleVulkanLibraryPath();
if (!library_path.empty())
{
if (library_path && library->Open(library_path))
const std::string path = library_path.string();
LOG_DEBUG(Render_Vulkan, "Trying Vulkan library: {}", path);
if (library->Open(path.c_str()))
{
break;
LOG_INFO(Render_Vulkan, "Loaded Vulkan library: {}", path);
}
}
#else

View File

@ -29,7 +29,14 @@ vk::SurfaceKHR CreateSurface(
}
#elif defined(__APPLE__)
if (window_info.type == Core::Frontend::WindowSystemType::Cocoa) {
if (window_info.render_surface == nullptr) {
LOG_ERROR(Render_Vulkan, "No CAMetalLayer available for the render window");
throw vk::Exception(VK_ERROR_INITIALIZATION_FAILED);
}
const VkMetalSurfaceCreateInfoEXT macos_ci = {
.sType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT,
.pNext = nullptr,
.flags = 0,
.pLayer = static_cast<const CAMetalLayer*>(window_info.render_surface),
};
const auto vkCreateMetalSurfaceEXT = reinterpret_cast<PFN_vkCreateMetalSurfaceEXT>(

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstring>
#include <memory>
#include <optional>
#include <utility>
@ -423,7 +424,12 @@ VkResult Free(VkDevice device, VkCommandPool handle, Span<VkCommandBuffer> buffe
Instance Instance::Create(u32 version, Span<const char*> layers, Span<const char*> extensions,
InstanceDispatch& dispatch) {
#ifdef __APPLE__
constexpr VkFlags ci_flags{VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR};
const bool enumerate_portability =
std::find_if(extensions.begin(), extensions.end(), [](const char* extension) {
return std::strcmp(extension, VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME) == 0;
}) != extensions.end();
const VkFlags ci_flags =
enumerate_portability ? VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR : 0;
#else
constexpr VkFlags ci_flags{};
#endif