Add Apple Silicon macOS build and frontend support (#320)
This commit is contained in:
parent
ea2dcef8b1
commit
b9c233adef
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 772cea1ac12d5cd11ff17df97dc8a9858d9c1c01
|
||||
Subproject commit 7bce512dcae670a7349088b564b0e6251c0a487e
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit f5e54359df4c26b6230fc61d38aa294581393084
|
||||
Subproject commit 1be298e1bd68957e4cd352e1f676f00e07dcfb57
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 303c68557c469a76da5ee8b9d07c1594a8798a9f
|
||||
Subproject commit dcceefb6dfcb92a0ef5d98ec6f368f82c83fe934
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
#StatusBar #volume:hover,
|
||||
#StatusBar #renderer:hover,
|
||||
#StatusBar #renderer.interactive:hover,
|
||||
#StatusBar #dockedMode:hover,
|
||||
#StatusBar #gpuCommandSynchronization:hover,
|
||||
#StatusBar #volumePopupBtn:hover
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#endif
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include "core/core.h"
|
||||
#include "core/hle/kernel/k_event.h"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public:
|
|||
|
||||
void Attach(ISciterUI & sciterUI, SystemModules & modules, SciterElement rootElement, void * parentHwnd);
|
||||
void Detach();
|
||||
void DetachAfterWindowDestroyed();
|
||||
|
||||
// IProfileSelectApplet
|
||||
void Close() override;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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"))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
}});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ namespace Vulkan {
|
|||
// matching CreateInstance's default in vulkan_instance.h.
|
||||
enum class WindowSystemType {
|
||||
Headless,
|
||||
#ifdef __APPLE__
|
||||
Metal,
|
||||
#endif
|
||||
};
|
||||
|
||||
namespace vk {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ public:
|
|||
|
||||
private:
|
||||
AVCodecContext* m_codec_context{};
|
||||
AVPixelFormat m_hw_pix_fmt{AV_PIX_FMT_NONE};
|
||||
};
|
||||
|
||||
// Wraps an AVFilterGraph.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue