Compare commits

..

59 Commits

Author SHA1 Message Date
lizzie 08d212e582 swift reworks 2026-08-14 21:14:57 +00:00
lizzie e669d3cf04 fix 2026-08-14 21:14:57 +00:00
lizzie 959ef253c6 fix shit 2 2026-08-14 21:14:57 +00:00
lizzie 61e4584ce3 swiftuijoystick 2026-08-14 21:14:57 +00:00
lizzie 38a8c84134 merge more 2026-08-14 21:14:57 +00:00
lizzie 1c5da6a941 add YUZU_USE_EXTERNAL_SDL2 2026-08-14 21:14:57 +00:00
lizzie 4752dd6a17 fix? 2026-08-14 21:14:57 +00:00
lizzie cc7f784910 pomelo scraps 2026-08-14 21:14:57 +00:00
lizzie 1054283e2b remove toolchain file + remove multibuild thingy 2026-08-14 21:14:57 +00:00
crueter 422d99cab5 Fix build script
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-08-14 21:14:57 +00:00
lizzie be5f99e150 AAAAAA 2026-08-14 21:14:57 +00:00
crueter c718e84c15 Build fixes
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-08-14 21:14:56 +00:00
lizzie 918d40c8c7 add basic ios shit 2026-08-14 21:14:34 +00:00
crueter 270dbd646d Revert stbi
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-08-14 21:14:34 +00:00
crueter 45ef72272a fix libs
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-08-14 21:14:34 +00:00
crueter 40b8d56b17 Fix most build errors
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-08-14 21:14:34 +00:00
crueter b2acce5ac3 Update CPMUtil, and fix script
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-08-14 21:14:33 +00:00
lizzie caa2e1fc21 DISABLE BY DEFAULT ON IOS FFS 2026-08-14 21:14:10 +00:00
lizzie 77230a64c6 fix life 2026-08-14 21:14:10 +00:00
lizzie 623bb81a6f fix sirit i think, add ios-aarch64 2026-08-14 21:14:10 +00:00
lizzie 4f2f3f8523 changes? 2026-08-14 21:14:10 +00:00
lizzie dc8b22d6ac fix shit? 2026-08-14 21:14:10 +00:00
lizzie cd2345b6e8 fix #include "common/logging.h" 2026-08-14 21:14:10 +00:00
lizzie ffd7ac17d5 fix IOS again fucking objc bridge 2026-08-14 21:14:10 +00:00
lizzie 3e9d8eebf9 fix? 2026-08-14 21:14:10 +00:00
lizzie c24965011f properly use bridging header, fix headers 2026-08-14 21:14:10 +00:00
lizzie a67a484077 fix swift driver I HOPE 2026-08-14 21:14:10 +00:00
lizzie 14dc272c4b license 2026-08-14 21:14:10 +00:00
lizzie 90b24d9336 [temporary c++ shit] 2026-08-14 21:14:10 +00:00
lizzie 7684e50d5e bit of cmake fuckery 2026-08-14 21:14:10 +00:00
lizzie 5690312587 use language generator exprs 2026-08-14 21:14:10 +00:00
lizzie 2396a0f859 $<$<COMPILE_LANGUAGE:C,CXX>:-Werror=missing-declarations> 2026-08-14 21:14:08 +00:00
lizzie 8f17093671 proper linkings?! 2026-08-14 21:13:48 +00:00
lizzie 385b819d07 bridge changes/fixes 2026-08-14 21:13:48 +00:00
lizzie f8244bb19e fix license 2026-08-14 21:13:48 +00:00
lizzie 62d0619c21 fix xcode 2? 2026-08-14 21:13:48 +00:00
lizzie 98062c95f5 fix xcode paths? 2026-08-14 21:13:48 +00:00
lizzie 4be6d7bef0 fx 2026-08-14 21:13:48 +00:00
lizzie 5034954a4f fx 2026-08-14 21:13:48 +00:00
lizzie c9059d602d fix boost 2026-08-14 21:13:48 +00:00
lizzie 152cc4b0f7 fx 2026-08-14 21:13:48 +00:00
lizzie 413d6b249e fix stuff 2026-08-14 21:13:48 +00:00
lizzie e94231cad0 stupid macos 2026-08-14 21:13:48 +00:00
lizzie 0399f3f4ba fix1 2026-08-14 21:13:46 +00:00
lizzie f03094cd34 fx 2026-08-14 21:13:19 +00:00
lizzie 85b0c96333 fix spirv-tools 2026-08-14 21:13:19 +00:00
lizzie 26e5d989d9 fix license 2026-08-14 21:13:19 +00:00
lizzie b686ddc471 fix ffmpeg 2026-08-14 21:13:19 +00:00
lizzie ceea1b4e20 fx 2026-08-14 21:13:19 +00:00
lizzie a2aa482107 fx 2026-08-14 21:13:19 +00:00
lizzie ce66ad9973 license 2026-08-14 21:13:19 +00:00
lizzie 2d83aa4a86 ios toolchain cmake 2026-08-14 21:13:19 +00:00
lizzie 9b8a152f07 license 2026-08-14 21:13:19 +00:00
lizzie 607d3eeab0 license headers 2026-08-14 21:13:19 +00:00
lizzie 0c20200264 flatten + cmake 2026-08-14 21:13:19 +00:00
lizzie 84b95e5cab flatten 2026-08-14 21:13:19 +00:00
lizzie fd227aa67e loicense 2026-08-14 21:13:19 +00:00
lizzie fdd7bc5419 modernize #1 2026-08-14 21:13:19 +00:00
lizzie 25a413d2ab sudachi ios stuff 2026-08-14 21:13:19 +00:00
199 changed files with 5112 additions and 8203 deletions
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh -ex
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
WORK_DIR="$PWD"
xcrun --sdk iphoneos --show-sdk-path
# TODO: support iphonesimulator sdk
cmake -G Xcode -B build/ios \
-DCMAKE_OSX_DEPLOYMENT_TARGET=16.0 \
-DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_SYSTEM_NAME=iOS \
-DCMAKE_OSX_ARCHITECTURES="arm64" \
-DCMAKE_BUILD_TYPE=Release \
-DYUZU_USE_EXTERNAL_SDL2=ON \
"$@"
cmake --build build/ios -t eden-ios --config Release
+1 -1
View File
@@ -115,7 +115,7 @@ for file in $FILES; do
*.cmake|*.sh|*.ps1|*.py|*.rb|*.perl|*.pl|*.nix|*CMakeLists.txt)
begin="#"
;;
*.kt|*.kts|*.cpp|*.h|*.qml|*.c|*.hpp|*.hxx|*.cxx|*.h.in|*.inc)
*.kt|*.kts|*.cpp|*.h|*.qml|*.c|*.hpp|*.hxx|*.cxx|*.h.in|*.inc|*.mm)
begin="//"
;;
*)
+31
View File
@@ -0,0 +1,31 @@
diff --git a/libs/process/src/shell.cpp b/libs/process/src/shell.cpp
index bf4bbfd8..bc4aae89 100644
--- a/libs/process/src/shell.cpp
+++ b/libs/process/src/shell.cpp
@@ -19,7 +19,7 @@
#if defined(BOOST_PROCESS_V2_WINDOWS)
#include <windows.h>
#include <shellapi.h>
-#elif !defined(__OpenBSD__) && !defined(__ANDROID__)
+#elif !defined(__OpenBSD__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
#include <wordexp.h>
#endif
@@ -30,7 +30,7 @@ BOOST_PROCESS_V2_DECL const error_category& get_shell_category()
{
return system_category();
}
-#elif !defined(__OpenBSD__) && !defined(__ANDROID__)
+#elif !defined(__OpenBSD__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
struct shell_category_t final : public error_category
{
@@ -99,7 +99,7 @@ auto shell::args() const-> args_type
return input_.c_str();
}
-#elif !defined(__OpenBSD__) && !defined(__ANDROID__)
+#elif !defined(__OpenBSD__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
void shell::parse_()
{
+57 -18
View File
@@ -23,6 +23,29 @@ include(CMakeDependentOption)
include(CTest)
include(CPMUtil)
# TODO(crueter): Make this more automatic.
if (IOS)
list(APPEND CMAKE_FIND_ROOT_PATH "${CMAKE_OSX_SYSROOT_INT}" CACHE INTERNAL "")
list(APPEND CMAKE_PROGRAM_PATH "/opt/homebrew/bin" CACHE INTERNAL "")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH CACHE INTERNAL "")
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH CACHE INTERNAL "")
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH CACHE INTERNAL "")
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH CACHE INTERNAL "")
list(LENGTH CMAKE_OSX_ARCHITECTURES _arch_len)
if (NOT _arch_len EQUAL 1)
message(FATAL_ERROR "CMAKE_OSX_ARCHITECTURES must contain exactly one architecture.")
endif()
# TODO(crueter): Proper handling for this.
if (CMAKE_OSX_ARCHITECTURES STREQUAL arm64)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
else()
set(CMAKE_SYSTEM_PROCESSOR ${CMAKE_OSX_ARCHITECTURES})
endif()
endif()
if (NOT DEFINED ARCHITECTURE)
message(FATAL_ERROR "Architecture didn't make it out of scope, did you delete DetectArchitecture.cmake?")
endif()
@@ -44,7 +67,7 @@ if (NETBSD)
set(ENV{PKG_CONFIG_PATH} "${PKG_CONFIG_PATH}:${CMAKE_SYSROOT}/usr/pkg/lib/ffmpeg7/pkgconfig")
endif()
cmake_dependent_option(YUZU_STATIC_ROOM "Build a static room executable only (CI only)" OFF "LINUX" OFF)
cmake_dependent_option(YUZU_STATIC_ROOM "Build a static room executable only (CI only)" OFF "LINUX OR (APPLE AND NOT IOS)" OFF)
if (YUZU_STATIC_ROOM)
set(YUZU_ROOM ON)
set(YUZU_ROOM_STANDALONE ON)
@@ -70,9 +93,15 @@ if (YUZU_STATIC_ROOM)
endif()
# qt stuff
option(ENABLE_QT "Enable the Qt frontend" ON)
if (IOS OR ANDROID)
set(_default_qt OFF)
else()
set(_default_qt ON)
endif()
option(ENABLE_QT "Enable the Qt frontend" ${_default_qt})
option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
option(ENABLE_UPDATE_CHECKER "Enable update checker (for Qt and Android)" OFF)
cmake_dependent_option(ENABLE_UPDATE_CHECKER "Enable update checker (for Qt and Android)" OFF "ENABLE_QT OR ANDROID" OFF)
cmake_dependent_option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF "NOT YUZU_USE_BUNDLED_QT" OFF)
cmake_dependent_option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF "NOT YUZU_USE_BUNDLED_QT" OFF)
set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundled Qt libraries")
@@ -81,9 +110,6 @@ cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSV
option(ENABLE_DEBUG_TOOLS "Enable debugging tools (maxwell disassembler, SPIRV translator, etc)" OFF)
option(ENABLE_WERROR "Enable -Werror diagnostics" ON)
# Lossless Scaling frame generation. Only Android.
cmake_dependent_option(ENABLE_LSFG "Enable Lossless Scaling frame generation" ON "ANDROID" OFF)
# non-linux bundled qt are static
if (YUZU_USE_BUNDLED_QT AND (APPLE OR NOT UNIX))
set(YUZU_STATIC_BUILD ON)
@@ -221,24 +247,31 @@ cmake_dependent_option(YUZU_USE_BUNDLED_SDL3 "Download bundled SDL3 build" "${MS
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
set(EXT_DEFAULT OFF)
if (MSVC OR ANDROID)
if (MSVC OR ANDROID OR IOS)
set(EXT_DEFAULT ON)
endif()
# TODO(crueter): Cleanup, each dep that has a bundled option should allow to choose between bundled, external, system
cmake_dependent_option(YUZU_USE_EXTERNAL_SDL2 "Build SDL2 from external source" OFF "NOT MSVC;NOT ANDROID" OFF)
cmake_dependent_option(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 build" "${EXT_DEFAULT}" "NOT ANDROID" OFF)
# TODO(crueter): did not find header 'AudioHardware.h' in framework 'CoreAudio'
cmake_dependent_option(ENABLE_CUBEB "Enables the cubeb audio backend" ON "NOT IOS" OFF)
# ffmpeg
option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT})
cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from external source" "${SOLARIS}" "NOT WIN32 AND NOT ANDROID" OFF)
# sirit
set(BUNDLED_SIRIT_DEFAULT OFF)
if (MSVC AND NOT (CMAKE_BUILD_TYPE MATCHES "Deb") OR ANDROID)
if ((MSVC AND NOT (CMAKE_BUILD_TYPE MATCHES "Deb")) OR ANDROID OR IOS)
set(BUNDLED_SIRIT_DEFAULT ON)
endif()
option(YUZU_USE_BUNDLED_SIRIT "Download bundled sirit" ${BUNDLED_SIRIT_DEFAULT})
# FreeBSD 15+ has libusb, versions below should disable it
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR LINUX OR FREEBSD OR APPLE" OFF)
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR LINUX OR FREEBSD OR (APPLE AND NOT IOS)" OFF)
cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT (WIN32 AND ARCHITECTURE_arm64) AND NOT APPLE" OFF)
mark_as_advanced(FORCE ENABLE_OPENGL)
@@ -260,10 +293,10 @@ option(YUZU_LEGACY "Apply patches that improve compatibility with older GPUs (e.
option(NIGHTLY_BUILD "Use Nightly qualifiers in the update checker and build metadata" OFF)
cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF)
cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID AND NOT IOS" OFF)
cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF)
cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "NOT ANDROID" OFF)
cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "NOT ANDROID AND NOT IOS" OFF)
cmake_dependent_option(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF)
@@ -307,7 +340,7 @@ if (YUZU_ROOM)
add_compile_definitions(YUZU_ROOM)
endif()
if (UNIX AND NOT (LINUX OR WIN32))
if ((UNIX OR IOS) AND NOT (LINUX OR WIN32))
if(CXX_APPLE OR CXX_CLANG)
# libc++ has stop_token and jthread as experimental
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library")
@@ -342,12 +375,11 @@ if (CXX_GCC OR CXX_CLANG)
endif()
elseif(ARCHITECTURE_arm64)
# See https://gcc.gnu.org/onlinedocs/gcc/AArch64-Options.html
set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, optimized, armv9, native")
set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, armv9, native")
set(mtune generic)
if (${YUZU_BUILD_PRESET} STREQUAL "generic")
set(march armv8-a)
elseif (${YUZU_BUILD_PRESET} STREQUAL "optimized")
set(march armv8.2-a+lse+rcpc)
elseif (${YUZU_BUILD_PRESET} STREQUAL "armv9")
set(march armv9-a)
endif()
@@ -380,7 +412,10 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
find_package(RenderDoc MODULE)
find_package(RenderDoc MODULE QUIET)
if (NOT RenderDoc_FOUND)
message(WARNING "RenderDoc not found. Some debugging features may be disabled.")
endif()
# openssl funniness
if (YUZU_USE_BUNDLED_OPENSSL)
@@ -512,9 +547,13 @@ endfunction()
# Platform-specific library requirements
# Put these BEFORE EXTERNALS or Boost WILL die
# =============================================
if (APPLE)
foreach(fw Carbon Metal Cocoa IOKit CoreVideo CoreMedia Security UniformTypeIdentifiers)
if (IOS)
set(_libs Metal IOKit CoreVideo CoreMedia Security UniformTypeIdentifiers)
else()
set(_libs Carbon Metal Cocoa IOKit CoreVideo CoreMedia Security UniformTypeIdentifiers)
endif()
foreach(fw ${_libs})
find_library(${fw}_LIBRARY ${fw} REQUIRED)
list(APPEND PLATFORM_LIBRARIES ${${fw}_LIBRARY})
endforeach()
+2 -1
View File
@@ -12,7 +12,8 @@
"min_version": "1.57",
"package": "Boost",
"patches": [
"0001-clang-cl.patch"
"0001-clang-cl.patch",
"0002-ios-fix.patch"
],
"repo": "boostorg/boost",
"version": "boost-1.90.0"
+11
View File
@@ -6,6 +6,7 @@
- [macOS](#macos)
- [OpenIndiana](#openindiana)
- [OmniOS](#omnios)
- [iOS](#ios)
- [HaikuOS](#haikuos)
- [OpenBSD](#openbsd)
- [FreeBSD](#freebsd)
@@ -92,6 +93,16 @@ You may also need to install `gmake` in order to properly build FFmpeg, this is
If it wasn't obvious already, you require a X11 server to properly run the emulator within OmniOS, [this guide](https://web.archive.org/web/20260424200928/https://geekblood.wordpress.com/2017/10/26/installing-x11-and-a-desktop-environment-on-omnios/) is a great starting point for that, the links to pkgsrc are outdated so follow [this exemplar](https://pkgsrc.smartos.org/install-on-illumos/) as well:
## iOS
iOS has a dedicated build script, we **highly** recommend using that instead of doing anything else, we don't support any other configuration than the one present in said build script.
To build, it's simply as easy as doing
```sh
chmod +x .ci/ios/build.sh
.ci/ios/build.sh
```
## HaikuOS
It's recommended to do a `pkgman full-sync` before installing. See [HaikuOS: Installing applications](https://www.haiku-os.org/guides/daily-tasks/install-applications/). Sometimes the process may be interrupted by an error like "Interrupted syscall". Simply firing the command again fixes the issue. By default `g++` is included on the default installation.
+6
View File
@@ -233,6 +233,12 @@ if (VulkanMemoryAllocator_ADDED)
endif()
endif()
# httplib
if (IOS)
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF)
endif()
AddJsonPackage(httplib)
# cpp-jwt
if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
AddJsonPackage(cpp-jwt)
+14 -9
View File
@@ -35,16 +35,21 @@ This file is based off of Yuzu and Dynarmic.
# Do note that situations where multiple architectures are defined
# should NOT be too dependent on the architecture
# otherwise, you may end up with duplicate code
if (CMAKE_OSX_ARCHITECTURES)
if (DEFINED CMAKE_OSX_ARCHITECTURES)
set(MULTIARCH_BUILD 1)
set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}")
# hope and pray the architecture names match
foreach(ARCH IN ${CMAKE_OSX_ARCHITECTURES})
set(ARCHITECTURE_${ARCH} 1 PARENT_SCOPE)
add_definitions(-DARCHITECTURE_${ARCH}=1)
endforeach()
if (IOS)
# TODO: Right... the toolchain file won't properly accomodate OSX_ARCHITECTURE
# they aren't defining it as a list properly I assume?
set(ARCHITECTURE_arm64 1)
add_definitions(-DARCHITECTURE_arm64=1)
else ()
# hope and pray the architecture names match
foreach(ARCH ${CMAKE_OSX_ARCHITECTURES})
set(ARCHITECTURE_${ARCH} 1)
add_definitions(-DARCHITECTURE_${ARCH}=1)
endforeach()
endif()
return()
endif()
@@ -218,4 +223,4 @@ if (NOT DEFINED ARCHITECTURE)
add_definitions(-DARCHITECTURE_GENERIC=1)
endif()
message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
+6
View File
@@ -54,6 +54,12 @@ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
set(CXX_APPLE ON)
endif()
# This fixes some quirks with xcrun or weird iOS toolchain cmake files
if (IOS)
unset(CXX_CLANG)
set(CXX_APPLE ON)
endif()
# https://gitlab.kitware.com/cmake/cmake/-/merge_requests/11112
# This works totally fine on MinGW64, but not CLANG{,ARM}64
if(MINGW AND CXX_CLANG)
+17 -1
View File
@@ -31,6 +31,22 @@ if (NOT YUZU_USE_BUNDLED_FFMPEG)
--extra-ldflags="-nostdlib"
)
set(FFmpeg_IS_CROSS_COMPILING TRUE)
elseif(IOS)
execute_process(COMMAND xcrun --sdk iphoneos --show-sdk-path OUTPUT_VARIABLE SYSROOT)
# Lovely extra newline apple adds that **we** must remove... thank you apple!
string(STRIP "${SYSROOT}" SYSROOT)
set(FFmpeg_CC xcrun --sdk iphoneos clang -arch arm64)
set(FFmpeg_CXX xcrun --sdk iphoneos clang++ -arch arm64)
list(APPEND FFmpeg_CROSS_COMPILE_FLAGS
--arch=arm64
--enable-cross-compile
--sysroot="${SYSROOT}"
--extra-ldflags="-miphoneos-version-min=16.0"
--install-name-dir='@rpath'
--disable-audiotoolbox
--disable-videotoolbox
)
set(FFmpeg_IS_CROSS_COMPILING TRUE)
# User attempts to do a FFmpeg cross compilation because...
# Here we just quickly test against host/system processors not matching
# TODO: Test for versions not matching as well?
@@ -64,7 +80,7 @@ elseif (ANDROID)
--enable-mediacodec
--enable-jni
)
elseif (UNIX AND NOT DEFINED FFmpeg_IS_CROSS_COMPILING AND NOT ANDROID)
elseif (UNIX AND NOT DEFINED FFmpeg_IS_CROSS_COMPILING AND NOT ANDROID AND NOT IOS)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBVA libva)
pkg_check_modules(CUDA cuda)
+2 -1
View File
@@ -24,7 +24,8 @@ if (MINGW OR LINUX OR APPLE)
message(FATAL_ERROR "Required program `autoconf` not found.")
endif()
find_program(LIBTOOLIZE libtoolize)
find_program(LIBTOOLIZE
NAMES libtoolize glibtoolize)
if ("${LIBTOOLIZE}" STREQUAL "LIBTOOLIZE-NOTFOUND")
message(FATAL_ERROR "Required program `libtoolize` not found.")
endif()
+8
View File
@@ -177,6 +177,9 @@ else()
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-invalid-offsetof>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-unused-parameter>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-missing-field-initializers>)
if (NOT IOS)
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Werror=missing-declarations>)
endif()
if (CXX_CLANG OR CXX_ICC OR CXX_APPLE) # Clang, AppleClang, or Intel C++
if (NOT MSVC)
@@ -295,4 +298,9 @@ if (ANDROID)
target_include_directories(yuzu-android PRIVATE android/app/src/main)
endif()
if (IOS)
add_subdirectory(ios)
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error>)
endif()
include(GenerateDepHashes)
@@ -539,40 +539,6 @@ object NativeLibrary {
*/
external fun installKeys(path: String, ext: String): Int
/**
* @return Whether this GPU can run the Lossless Scaling frame generation shaders,
* which are built against the Vulkan memory model.
*/
external fun supportsFrameGeneration(): Boolean
/**
* @return Path the user-supplied Lossless Scaling library is expected at.
*/
external fun getLosslessDllPath(): String
/**
* Parses the installed Lossless Scaling library and checks that every shader the
* frame generation chain needs is present.
*
* @return The result code, matching the losslessDllResults array.
*/
external fun validateLosslessDll(): Int
/**
* Translates the frame generation shaders out of the installed Lossless Scaling library
* and writes them to the SPIR-V cache. Slow, so call it off the main thread.
*
* @return The result code, matching the losslessDllResults array.
*/
external fun prepareLosslessDll(): Int
/**
* Deletes the installed Lossless Scaling library.
*
* @return Whether the library is gone after the call.
*/
external fun removeLosslessDll(): Boolean
/**
* Checks the PatchManager for any addons that are available
*
@@ -559,15 +559,6 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
private fun enableFullscreenImmersive() {
WindowCompat.setDecorFitsSystemWindows(window, false)
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_FULLSCREEN or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.hide(WindowInsetsCompat.Type.systemBars())
controller.systemBarsBehavior =
@@ -18,6 +18,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
SKIP_CPU_INNER_INVALIDATION("skip_cpu_inner_invalidation"),
FIX_BLOOM_EFFECTS("fix_bloom_effects"),
EMULATE_BGR565("emulate_bgr565"),
RESCALE_HACK("rescale_hack"),
CPUOPT_UNSAFE_HOST_MMU("cpuopt_unsafe_host_mmu"),
USE_DOCKED_MODE("use_docked_mode"),
USE_AUTO_STUB("use_auto_stub"),
@@ -36,10 +37,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_PATCH_OLD_QCOM_DRIVERS("patch_old_qcom_drivers"),
RENDERER_VERTEX_INPUT_DYNAMIC_STATE("vertex_input_dynamic_state"),
RENDERER_SAMPLE_SHADING("sample_shading"),
RENDERER_FRAME_GEN("frame_gen"),
RENDERER_FRAME_GEN_FP16("frame_gen_fp16"),
RENDERER_FRAME_GEN_FLOW_SCALE_AUTO("frame_gen_flow_scale_auto"),
RENDERER_FRAME_GEN_DUMP_FLOW("frame_gen_dump_flow"),
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
@@ -19,10 +19,6 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
RENDERER_ASTC_DECODE_METHOD("accelerate_astc"),
RENDERER_ACCURACY("gpu_accuracy"),
RENDERER_RESOLUTION("resolution_setup"),
RENDERER_FRAME_GEN_MULTIPLIER("frame_gen_multiplier"),
RENDERER_FRAME_GEN_TARGET_RATE("frame_gen_target_rate"),
RENDERER_FRAME_GEN_QUEUE_TARGET("frame_gen_queue_target"),
RENDERER_FRAME_GEN_FLOW_SCALE("frame_gen_flow_scale"),
RENDERER_VSYNC("use_vsync"),
RENDERER_SCALING_FILTER("scaling_filter"),
RENDERER_ANTI_ALIASING("anti_aliasing"),
@@ -11,7 +11,6 @@ object Settings {
SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics),
SECTION_FRAME_GEN(R.string.frame_gen),
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
@@ -21,7 +21,6 @@ import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.network.NetDataValidators
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
/**
@@ -66,19 +65,6 @@ abstract class SettingsItem(
return NativeLibrary.isFirmwareAvailable()
}
if (setting.key in frameGenKeys &&
!(LosslessScalingHelper.isInstalled() && LosslessScalingHelper.isSupportedByGpu())
) {
return false
}
// A frame rate target moves the multiplier on its own
if (setting.key == IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key &&
frameGenTargetRate != 0
) {
return false
}
// Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
@@ -102,31 +88,7 @@ abstract class SettingsItem(
val clearable: Boolean
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
private val frameGenTargetRate: Int
get() {
val key = IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key
val needsGlobal = if (NativeLibrary.isRunning() &&
!NativeConfig.isPerGameConfigLoaded()
) {
!NativeConfig.usingGlobal(key)
} else {
NativeConfig.usingGlobal(key)
}
return IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(needsGlobal)
}
companion object {
private val frameGenKeys = setOf(
BooleanSetting.RENDERER_FRAME_GEN.key,
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key,
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key,
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key,
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key,
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key,
BooleanSetting.RENDERER_FRAME_GEN_FP16.key,
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key
)
const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1
const val TYPE_SINGLE_CHOICE = 2
@@ -624,7 +586,6 @@ abstract class SettingsItem(
IntSetting.FSR_SHARPENING_SLIDER,
titleId = R.string.fsr_sharpness,
descriptionId = R.string.fsr_sharpness_description,
max = 200,
units = "%"
)
)
@@ -646,71 +607,6 @@ abstract class SettingsItem(
valuesId = R.array.rendererAntiAliasingValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FRAME_GEN,
titleId = R.string.frame_gen,
descriptionId = R.string.frame_gen_description
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER,
titleId = R.string.frame_gen_multiplier,
descriptionId = R.string.frame_gen_multiplier_description,
choicesId = R.array.frameGenMultiplierNames,
valuesId = R.array.frameGenMultiplierValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE,
titleId = R.string.frame_gen_target_rate,
descriptionId = R.string.frame_gen_target_rate_description,
choicesId = R.array.frameGenTargetRateNames,
valuesId = R.array.frameGenTargetRateValues
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET,
titleId = R.string.frame_gen_queue_target,
descriptionId = R.string.frame_gen_queue_target_description,
choicesId = R.array.frameGenQueueTargetNames,
valuesId = R.array.frameGenQueueTargetValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO,
titleId = R.string.frame_gen_flow_scale_auto,
descriptionId = R.string.frame_gen_flow_scale_auto_description
)
)
put(
SliderSetting(
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE,
titleId = R.string.frame_gen_flow_scale,
descriptionId = R.string.frame_gen_flow_scale_description,
min = 25,
max = 100,
units = "%"
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FRAME_GEN_FP16,
titleId = R.string.frame_gen_fp16,
descriptionId = R.string.frame_gen_fp16_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW,
titleId = R.string.frame_gen_dump_flow,
descriptionId = R.string.frame_gen_dump_flow_description
)
)
put(
SingleChoiceSetting(
IntSetting.RENDERER_SCREEN_LAYOUT,
@@ -877,6 +773,13 @@ abstract class SettingsItem(
descriptionId = R.string.fix_bloom_effects_description
)
)
put(
SwitchSetting(
BooleanSetting.RESCALE_HACK,
titleId = R.string.rescale_hack,
descriptionId = R.string.rescale_hack_description
)
)
put(
SwitchSetting(
BooleanSetting.EMULATE_BGR565,
@@ -382,9 +382,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
}
scSetting.setSelectedValue(value)
if (scSetting.setting.key == IntSetting.RENDERER_SCALING_FILTER.key ||
scSetting.setting.key == IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key
) {
if (scSetting.setting.key == IntSetting.RENDERER_SCALING_FILTER.key) {
settingsViewModel.setShouldReloadSettingsList(true)
}
@@ -27,7 +27,6 @@ import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.model.view.*
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FullscreenHelper
@@ -77,41 +76,6 @@ class SettingsFragmentPresenter(
}
}
private fun addFrameGenSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
if (!LosslessScalingHelper.isSupportedByGpu()) {
add(
RunnableSetting(
titleId = R.string.frame_gen_unsupported,
descriptionId = R.string.frame_gen_unsupported_description,
isRunnable = false
) {}
)
} else if (!LosslessScalingHelper.isInstalled()) {
add(
RunnableSetting(
titleId = R.string.lossless_scaling_missing,
descriptionId = R.string.lossless_scaling_missing_description,
isRunnable = false
) {}
)
}
add(BooleanSetting.RENDERER_FRAME_GEN.key)
add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
getNeedsGlobalForKey(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
)
) {
add(IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key)
}
add(BooleanSetting.RENDERER_FRAME_GEN_FP16.key)
}
}
private fun isSharpnessScalingFilterSelected(): Boolean {
val needsGlobal = getNeedsGlobalForKey(IntSetting.RENDERER_SCALING_FILTER.key)
val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal)
@@ -156,7 +120,6 @@ class SettingsFragmentPresenter(
MenuTag.SECTION_ROOT -> addConfigSettings(sl)
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
@@ -339,6 +302,7 @@ class SettingsFragmentPresenter(
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
add(BooleanSetting.EMULATE_BGR565.key)
add(BooleanSetting.RESCALE_HACK.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(IntSetting.ANDROID_PIPELINE_WORKERS.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_GPU_EMULATION.key)
@@ -1332,7 +1296,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.DUMP_GUEST_SHADERS.key)
add(BooleanSetting.GPU_LOG_SHADER_DUMPS.key)
add(BooleanSetting.DUMP_MACROS.key)
add(BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key)
add(BooleanSetting.GPU_LOG_MEMORY_TRACKING.key)
add(BooleanSetting.GPU_LOG_DRIVER_DEBUG.key)
add(IntSetting.GPU_LOG_RING_BUFFER_SIZE.key)
@@ -29,7 +29,6 @@ enum class SettingsSubscreen {
DRIVER_MANAGER,
DRIVER_FETCHER,
FREEDRENO_SETTINGS,
LOSSLESS_MANAGER,
APPLET_LAUNCHER,
INSTALLABLE,
GAME_FOLDERS,
@@ -127,7 +126,6 @@ class SettingsSubscreenActivity : AppCompatActivity() {
SettingsSubscreen.DRIVER_MANAGER -> R.id.driverManagerFragment
SettingsSubscreen.DRIVER_FETCHER -> R.id.driverFetcherFragment
SettingsSubscreen.FREEDRENO_SETTINGS -> R.id.freedrenoSettingsFragment
SettingsSubscreen.LOSSLESS_MANAGER -> R.id.losslessManagerFragment
SettingsSubscreen.APPLET_LAUNCHER -> R.id.appletLauncherFragment
SettingsSubscreen.INSTALLABLE -> R.id.installableFragment
SettingsSubscreen.GAME_FOLDERS -> R.id.gameFoldersFragment
@@ -1182,7 +1182,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
container,
IntSetting.FSR_SHARPENING_SLIDER,
minValue = 0,
maxValue = 200,
maxValue = 100,
units = "%"
)
}
@@ -369,21 +369,6 @@ class GamePropertiesFragment : Fragment() {
)
)
}
add(
SubmenuProperty(
R.string.frame_gen,
R.string.frame_gen_per_game_description,
R.drawable.ic_duck,
action = {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
args.game,
Settings.MenuTag.SECTION_FRAME_GEN
)
binding.root.findNavController().navigate(action)
}
)
)
if (GpuDriverHelper.isAdrenoGpu()) {
add(
SubmenuProperty(
@@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
class HomeSettingsFragment : Fragment() {
@@ -171,24 +170,6 @@ class HomeSettingsFragment : Fragment() {
)
)
}
add(
HomeSetting(
R.string.lossless_scaling,
R.string.lossless_scaling_description,
R.drawable.ic_duck,
{
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
SettingsSubscreen.LOSSLESS_MANAGER,
null
)
binding.root.findNavController().navigate(action)
},
{ true },
0,
0,
LosslessScalingHelper.statusText
)
)
add(
HomeSetting(
R.string.multiplayer,
@@ -356,7 +337,6 @@ class HomeSettingsFragment : Fragment() {
override fun onResume() {
super.onResume()
driverViewModel.updateDriverNameForGame(null)
LosslessScalingHelper.refreshStatus()
}
override fun onDestroyView() {
@@ -1,174 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
import org.yuzu.yuzu_emu.databinding.FragmentLosslessManagerBinding
import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
import org.yuzu.yuzu_emu.utils.collect
class LosslessManagerFragment : Fragment() {
private var _binding: FragmentLosslessManagerBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLosslessManagerBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.toolbarLossless.setNavigationOnClickListener {
requireActivity().onBackPressedDispatcher.onBackPressed()
}
binding.losslessOptionsList.apply {
layoutManager =
GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
addItemDecoration(
SpacingItemDecoration(resources.getDimensionPixelSize(R.dimen.spacing_small))
)
}
LosslessScalingHelper.statusText.collect(viewLifecycleOwner) { refreshOptions() }
setInsets()
}
private fun refreshOptions() {
binding.losslessOptionsList.adapter = HomeSettingAdapter(
requireActivity() as AppCompatActivity,
viewLifecycleOwner,
buildOptions()
)
}
private fun buildOptions(): List<HomeSetting> {
val installed = LosslessScalingHelper.isInstalled()
return listOf(
HomeSetting(
if (installed) R.string.lossless_scaling_replace else R.string.lossless_scaling_install,
if (installed) {
R.string.lossless_scaling_replace_description
} else {
R.string.lossless_scaling_install_description
},
R.drawable.ic_install,
{ dllPickerLauncher.launch(arrayOf("*/*")) },
{ !NativeLibrary.isRunning() },
R.string.lossless_scaling_locked,
R.string.lossless_scaling_locked_description,
LosslessScalingHelper.statusText
),
HomeSetting(
R.string.lossless_scaling_remove,
R.string.lossless_scaling_remove_description,
R.drawable.ic_delete,
{ confirmRemoval() },
{ installed && !NativeLibrary.isRunning() },
if (installed) {
R.string.lossless_scaling_locked
} else {
R.string.lossless_scaling_remove_unavailable
},
if (installed) {
R.string.lossless_scaling_locked_description
} else {
R.string.lossless_scaling_remove_unavailable_description
}
)
)
}
private fun confirmRemoval() {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.lossless_scaling_remove,
descriptionId = R.string.lossless_scaling_remove_confirmation,
positiveButtonTitleId = R.string.lossless_scaling_remove,
positiveAction = { LosslessScalingHelper.remove() },
showNegativeButton = true,
negativeAction = {}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
private val dllPickerLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
return@registerForActivityResult
}
val resultStrings = resources.getStringArray(R.array.losslessDllResults)
ProgressDialogFragment.newInstance(
requireActivity(),
R.string.lossless_scaling_installing,
false
) { _, _ ->
val installResult = LosslessScalingHelper.install(result)
if (installResult == LosslessScalingHelper.RESULT_OK) {
getString(R.string.lossless_scaling_install_success)
} else {
MessageDialogFragment.newInstance(
titleId = R.string.lossless_scaling_install_failed,
descriptionString = resultStrings[installResult]
)
}
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
binding.appbarLossless.updateMargins(
left = barInsets.left + cutoutInsets.left,
right = barInsets.right + cutoutInsets.right
)
binding.scrollViewLossless.updatePadding(bottom = barInsets.bottom)
binding.losslessOptionsList.updatePadding(
left = barInsets.left + cutoutInsets.left,
right = barInsets.right + cutoutInsets.right
)
windowInsets
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -42,7 +42,6 @@ import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.PageState
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
@@ -203,24 +202,6 @@ class SetupFragment : Fragment() {
R.string.install_firmware_warning_help,
)
)
add(
PageButton(
R.drawable.ic_duck,
R.string.lossless_scaling,
R.string.lossless_scaling_setup_description,
{
pageButtonCallback = it
getLosslessDll.launch(arrayOf("*/*"))
},
{
if (LosslessScalingHelper.isInstalled()) {
ButtonState.BUTTON_ACTION_COMPLETE
} else {
ButtonState.BUTTON_ACTION_INCOMPLETE
}
}
)
)
add(
PageButton(
R.drawable.ic_controller,
@@ -465,32 +446,6 @@ class SetupFragment : Fragment() {
}
}
val getLosslessDll =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
return@registerForActivityResult
}
val resultStrings = resources.getStringArray(R.array.losslessDllResults)
ProgressDialogFragment.newInstance(
requireActivity(),
R.string.lossless_scaling_installing,
false
) { _, _ ->
val installResult = LosslessScalingHelper.install(result)
if (installResult == LosslessScalingHelper.RESULT_OK) {
getString(R.string.lossless_scaling_install_success)
} else {
MessageDialogFragment.newInstance(
titleId = R.string.lossless_scaling_install_failed,
descriptionString = resultStrings[installResult]
)
}
}.apply {
onDialogComplete = { checkForButtonState.invoke() }
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
}
val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -72,18 +72,6 @@ class SystemInfoDialogFragment : DialogFragment() {
val vulkanDriver = NativeLibrary.getVulkanDriverVersion()
appendLine("${getString(R.string.vulkan_driver_version)}: $vulkanDriver")
val frameGen = NativeLibrary.supportsFrameGeneration()
appendLine(
"${getString(R.string.frame_generation_support)}: " +
getString(
if (frameGen) {
R.string.frame_generation_supported
} else {
R.string.frame_generation_unsupported
}
)
)
} catch (e: Exception) {
appendLine("${getString(R.string.error_getting_emulator_info)}: ${e.message}")
}
@@ -1,77 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.net.Uri
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import java.io.File
object LosslessScalingHelper {
const val RESULT_OK = 0
const val RESULT_NOT_INSTALLED = 1
private val _statusText = MutableStateFlow("")
val statusText: StateFlow<String> = _statusText.asStateFlow()
private var installed: Boolean? = null
private var gpuSupported: Boolean? = null
fun isInstalled(): Boolean = installed ?: refreshStatus()
fun isSupportedByGpu(): Boolean {
val cached = gpuSupported
if (cached != null) {
return cached
}
val result = NativeLibrary.supportsFrameGeneration()
gpuSupported = result
return result
}
fun refreshStatus(): Boolean {
val result = NativeLibrary.validateLosslessDll() == RESULT_OK
installed = result
val context = YuzuApplication.appContext
_statusText.value = if (result) {
context.getString(R.string.lossless_scaling_installed)
} else {
context.getString(R.string.lossless_scaling_not_installed)
}
return result
}
fun install(source: Uri): Int {
val destination = File(NativeLibrary.getLosslessDllPath())
destination.parentFile?.mkdirs()
val copied = FileUtil.copyUriToInternalStorage(
source,
destination.parent!!,
destination.name
)
if (copied == null) {
refreshStatus()
return RESULT_NOT_INSTALLED
}
val result = NativeLibrary.prepareLosslessDll()
if (result != RESULT_OK) {
NativeLibrary.removeLosslessDll()
}
refreshStatus()
return result
}
fun remove(): Boolean {
val removed = NativeLibrary.removeLosslessDll()
refreshStatus()
return removed
}
}
@@ -33,9 +33,8 @@ void AndroidConfig::SaveAllValues() {
}
void AndroidConfig::ReadAndroidValues() {
Settings::values.rescale_hack.SetValue(false);
ReadAndroidUIValues();
if (global) {
ReadAndroidUIValues();
ReadUIValues();
BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));
Settings::values.ext_content_from_game_dirs = ReadBooleanSetting(
@@ -224,8 +223,8 @@ void AndroidConfig::ReadAndroidControlValues() {
}
void AndroidConfig::SaveAndroidValues() {
SaveAndroidUIValues();
if (global) {
SaveAndroidUIValues();
SaveUIValues();
SaveOverlayValues();
}
@@ -147,7 +147,7 @@ namespace AndroidSettings {
&show_performance_overlay};
Settings::SwitchableSetting<s32> pipeline_worker_count{linkage, 2, "pipeline_worker_count",
Settings::Setting<s32> pipeline_worker_count{linkage, 4, "pipeline_worker_count",
Settings::Category::Android,
Settings::Specialization::Default,
true,
@@ -124,18 +124,9 @@ float EmuWindow_Android::GetFrameTimeVerifiedHint() const {
return QuantizeFrameRateHint(verified_rate);
}
float EmuWindow_Android::GetPresentedFrameMultiplier() {
if (!Settings::values.frame_gen.GetValue()) {
return 1.0f;
}
return static_cast<float>(std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(), 2, 4));
}
float EmuWindow_Android::GetFrameRateHint() const {
const float presented_multiplier = GetPresentedFrameMultiplier();
const float observed_rate =
std::clamp(m_smoothed_present_rate * presented_multiplier, 0.0f, 240.0f);
const float frame_time_verified_hint = GetFrameTimeVerifiedHint() * presented_multiplier;
const float observed_rate = std::clamp(m_smoothed_present_rate, 0.0f, 240.0f);
const float frame_time_verified_hint = GetFrameTimeVerifiedHint();
if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) {
const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f);
@@ -159,9 +150,9 @@ float EmuWindow_Android::GetFrameRateHint() const {
return frame_time_verified_hint;
}
const float nominal_rate = 60.0f * presented_multiplier;
constexpr float NominalFrameRate = 60.0f;
if (!Settings::values.use_speed_limit.GetValue()) {
return QuantizeFrameRateHint(nominal_rate);
return NominalFrameRate;
}
const u16 speed_limit = Settings::SpeedLimit();
@@ -170,7 +161,7 @@ float EmuWindow_Android::GetFrameRateHint() const {
}
const float speed_limited_rate =
nominal_rate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
NominalFrameRate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
return QuantizeFrameRateHint(speed_limited_rate);
}
@@ -61,7 +61,6 @@ private:
void UpdateObservedFrameRate();
[[nodiscard]] float GetFrameRateHint() const;
[[nodiscard]] float GetFrameTimeVerifiedHint() const;
[[nodiscard]] static float GetPresentedFrameMultiplier();
[[nodiscard]] static float QuantizeFrameRateHint(float frame_rate);
float m_window_width{};
+1 -92
View File
@@ -44,7 +44,6 @@ extern "C" {
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "common/dynamic_library.h"
#include "common/fs/fs_util.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/scm_rev.h"
@@ -91,7 +90,6 @@ extern "C" {
#include "hid_core/hid_types.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "jni/native.h"
#include "video_core/frame_gen/lossless_dll.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/capture.h"
@@ -1000,7 +998,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
FILE* f = std::fopen(CPUINFO_PATH, "r");
if (!f) return Common::Android::ToJString(env, result);
char buf[4096];
char buf[512];
if (f) {
std::set<std::string> feature_set;
@@ -1031,13 +1029,7 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
bool has_dotprod = feature_set.count("asimddp") || feature_set.count("dotprod");
bool has_i8mm = feature_set.count("i8mm");
bool has_bf16 = feature_set.count("bf16");
bool has_fp16 = feature_set.count("fphp") || feature_set.count("asimdhp");
bool has_atomics = feature_set.count("atomics") || feature_set.count("lse");
bool has_lse2 = feature_set.count("uscat");
bool has_rcpc = feature_set.count("lrcpc");
bool has_rcpc2 = feature_set.count("ilrcpc");
bool has_flagm = feature_set.count("flagm");
bool has_flagm2 = feature_set.count("flagm2");
std::string features;
if (has_neon || has_fp) {
@@ -1045,7 +1037,6 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
if (has_dotprod) features += "+DP";
if (has_i8mm) features += "+I8MM";
if (has_bf16) features += "+BF16";
if (has_fp16) features += "+FP16";
}
if (has_sve) {
@@ -1062,19 +1053,6 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuSummary(JNIEnv* env, jobject
if (has_atomics) {
if (!features.empty()) features += " | ";
features += "LSE";
if (has_lse2) features += "2";
}
if (has_rcpc) {
if (!features.empty()) features += " | ";
features += "RCpc";
if (has_rcpc2) features += "2";
}
if (has_flagm) {
if (!features.empty()) features += " | ";
features += "FlagM";
if (has_flagm2) features += "2";
}
if (!features.empty()) {
@@ -1112,34 +1090,6 @@ VkPhysicalDeviceProperties GetVulkanDeviceProperties() {
const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
return physical_device.GetProperties();
}
bool GetVulkanMemoryModelSupport() {
Common::DynamicLibrary library;
if (!library.Open("libvulkan.so")) {
return false;
}
Vulkan::vk::InstanceDispatch dld;
const auto instance = Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_1);
const auto physical_devices = instance.EnumeratePhysicalDevices();
if (physical_devices.empty()) {
return false;
}
const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
VkPhysicalDeviceVulkanMemoryModelFeatures memory_model{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES,
.pNext = nullptr,
};
VkPhysicalDeviceFeatures2 features{
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
.pNext = &memory_model,
};
physical_device.GetFeatures2(features);
return memory_model.vulkanMemoryModel == VK_TRUE;
}
} // namespace
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanDriverVersion(JNIEnv* env, jobject jobj) {
@@ -1215,14 +1165,6 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanApiVersion(JNIEnv* env, j
}
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_supportsFrameGeneration(JNIEnv* env, jobject jobj) {
try {
return static_cast<jboolean>(GetVulkanMemoryModelSupport());
} catch (...) {
return static_cast<jboolean>(false);
}
}
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) {
const auto props = GetVulkanDeviceProperties();
if (props.deviceID == 0) {
@@ -1448,39 +1390,6 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz
return static_cast<int>(FirmwareManager::InstallKeys(path, ext));
}
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getLosslessDllPath(JNIEnv* env, jclass clazz) {
#ifdef HAS_LSFG
const auto path = VideoCore::FrameGen::GetLosslessDllPath();
return Common::Android::ToJString(env, Common::FS::PathToUTF8String(path));
#else
return Common::Android::ToJString(env, "");
#endif
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_validateLosslessDll(JNIEnv* env, jclass clazz) {
#ifdef HAS_LSFG
return static_cast<jint>(VideoCore::FrameGen::GetInstalledLosslessStatus());
#else
return static_cast<jint>(VideoCore::FrameGen::LosslessStatus::NotInstalled);
#endif
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_prepareLosslessDll(JNIEnv* env, jclass clazz) {
#ifdef HAS_LSFG
return static_cast<jint>(VideoCore::FrameGen::BuildShaderCache());
#else
return static_cast<jint>(VideoCore::FrameGen::LosslessStatus::NotInstalled);
#endif
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_removeLosslessDll(JNIEnv* env, jclass clazz) {
#ifdef HAS_LSFG
return static_cast<jboolean>(VideoCore::FrameGen::RemoveInstalledLosslessDll());
#else
return static_cast<jboolean>(false);
#endif
}
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
jstring jpath,
jstring jprogramId) {
@@ -1,19 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M11,10.6a8,5.2 0 1,0 0,10.4a8,5.2 0 1,0 0,-10.4z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M4.6,12.6L0.8,10.2L3.2,15.5z" />
<path
android:fillColor="?attr/colorControlNormal"
android:fillType="evenOdd"
android:pathData="M15.3,3.5a4,4 0 1,0 0,8a4,4 0 1,0 0,-8zM16.6,4.9a1,1 0 1,0 0,2a1,1 0 1,0 0,-2z" />
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M18.6,6.5L23.2,7.8L18.6,9.3z" />
</vector>
@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_lossless"
style="@style/Widget.Eden.TransparentTopAppBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:touchscreenBlocksFocus="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_lossless"
style="@style/Widget.Eden.TransparentTopToolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:touchscreenBlocksFocus="false"
app:navigationIcon="@drawable/ic_back"
app:title="@string/lossless_scaling" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view_lossless"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/transparent"
android:clipToPadding="false"
android:defaultFocusHighlightEnabled="false"
android:fadeScrollbars="false"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/appbar_lossless">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="16dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/lossless_options_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -47,12 +47,6 @@
android:defaultValue="@null" />
</fragment>
<fragment
android:id="@+id/losslessManagerFragment"
android:name="org.yuzu.yuzu_emu.fragments.LosslessManagerFragment"
android:label="@string/lossless_scaling"
tools:layout="@layout/fragment_lossless_manager" />
<fragment
android:id="@+id/appletLauncherFragment"
android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
@@ -1088,6 +1088,7 @@
<string name="theme_mode_light">فاتح</string>
<string name="theme_mode_dark">داكن</string>
<string name="multiplier_none">لا شيء</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">خلفيات سوداء</string>
@@ -1004,6 +1004,7 @@ Wirklich fortfahren?</string>
<string name="theme_mode_light">Hell</string>
<string name="theme_mode_dark">Dunkel</string>
<string name="multiplier_none">Keine</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Schwarze Hintergründe</string>
@@ -1080,6 +1080,7 @@
<string name="theme_mode_light">Claro</string>
<string name="theme_mode_dark">Oscuro</string>
<string name="multiplier_none">Nada</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Fondos oscuros</string>
@@ -808,6 +808,9 @@
<string name="multiplier_x4">x4</string>
<string name="multiplier_x8">x8</string>
<string name="multiplier_x16">x16</string>
<string name="multiplier_x32">x32</string>
<string name="multiplier_x64">x64</string>
<string name="multiplier_none">None</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">پس‌زمینه مشکی</string>
@@ -1016,6 +1016,7 @@
<string name="theme_mode_light">Lumineux</string>
<string name="theme_mode_dark">Sombre</string>
<string name="multiplier_none">Aucun</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Arrière-plan noir</string>
@@ -948,6 +948,7 @@
<string name="theme_mode_light">Jasny</string>
<string name="theme_mode_dark">Ciemny</string>
<string name="multiplier_none">Brak</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Czarne tła</string>
@@ -897,6 +897,7 @@
<string name="theme_mode_light">Claro</string>
<string name="theme_mode_dark">Escuro</string>
<string name="multiplier_none">Nenhum</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Planos de fundo pretos</string>
@@ -1084,6 +1084,7 @@
<string name="theme_mode_light">Светлая</string>
<string name="theme_mode_dark">Темная</string>
<string name="multiplier_none">Отключено</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Чёрный фон</string>
@@ -1066,6 +1066,7 @@
<string name="theme_mode_light">Світла</string>
<string name="theme_mode_dark">Темна</string>
<string name="multiplier_none">Жодного</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Чорний фон</string>
@@ -1078,6 +1078,7 @@
<string name="theme_mode_light">浅色</string>
<string name="theme_mode_dark">深色</string>
<string name="multiplier_none"></string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">使用黑色背景</string>
@@ -926,6 +926,7 @@
<string name="theme_mode_light">淺色</string>
<string name="theme_mode_dark">深色</string>
<string name="multiplier_none"></string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">黑色背景</string>
+6 -52
View File
@@ -162,48 +162,6 @@
<item>@string/resolution_four</item>
</string-array>
<string-array name="frameGenMultiplierNames">
<item>@string/frame_gen_multiplier_2x</item>
<item>@string/frame_gen_multiplier_3x</item>
<item>@string/frame_gen_multiplier_4x</item>
</string-array>
<integer-array name="frameGenMultiplierValues">
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>
<string-array name="frameGenTargetRateNames">
<item>@string/frame_gen_target_rate_off</item>
<item>@string/frame_gen_target_rate_60</item>
<item>@string/frame_gen_target_rate_90</item>
<item>@string/frame_gen_target_rate_120</item>
<item>@string/frame_gen_target_rate_144</item>
<item>@string/frame_gen_target_rate_165</item>
</string-array>
<integer-array name="frameGenTargetRateValues">
<item>0</item>
<item>60</item>
<item>90</item>
<item>120</item>
<item>144</item>
<item>165</item>
</integer-array>
<string-array name="frameGenQueueTargetNames">
<item>@string/frame_gen_queue_target_0</item>
<item>@string/frame_gen_queue_target_1</item>
<item>@string/frame_gen_queue_target_2</item>
</string-array>
<integer-array name="frameGenQueueTargetValues">
<item>0</item>
<item>1</item>
<item>2</item>
</integer-array>
<string-array name="rendererVSyncNames">
<item>@string/renderer_vsync_immediate</item>
<item>@string/renderer_vsync_mailbox</item>
@@ -513,6 +471,9 @@
<item>@string/multiplier_x4</item>
<item>@string/multiplier_x8</item>
<item>@string/multiplier_x16</item>
<item>@string/multiplier_x32</item>
<item>@string/multiplier_x64</item>
<item>@string/multiplier_none</item>
</string-array>
<integer-array name="anisoValues">
<item>0</item>
@@ -521,6 +482,9 @@
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
<item>7</item>
<item>8</item>
</integer-array>
<string-array name="verticalAlignmentEntries">
@@ -674,16 +638,6 @@
<item>@string/error_keys_failed_init</item>
</string-array>
<string-array name="losslessDllResults">
<item>""</item>
<item>@string/error_lossless_copy_failed</item>
<item>@string/error_lossless_unreadable</item>
<item>@string/error_lossless_not_pe</item>
<item>@string/error_lossless_missing_shaders</item>
<item>@string/error_lossless_translation_failed</item>
<item>@string/error_lossless_cache_failed</item>
</string-array>
<!-- GPU Logging Arrays -->
<string-array name="gpuLogLevelEntries">
<item>Off</item>
@@ -298,67 +298,6 @@
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
<string name="gpu_driver_manager">GPU driver manager</string>
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
<string name="frame_gen">Frame generation</string>
<string name="frame_gen_per_game_description">Configure frame generation for this game</string>
<string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
<string name="frame_gen_multiplier">Frame multiplier</string>
<string name="frame_gen_multiplier_description">How many frames to display for each rendered frame. Higher values cost proportionally more GPU time. Asking for more than your display can present will slow emulation down.</string>
<string name="frame_gen_multiplier_2x">2x</string>
<string name="frame_gen_multiplier_3x">3x</string>
<string name="frame_gen_multiplier_4x">4x</string>
<string name="frame_gen_target_rate">Target frame rate</string>
<string name="frame_gen_target_rate_description">Pick the rate your display can actually show. The multiplier then rises or falls on its own to hold it, and rolls back any step that makes the game itself run slower.</string>
<string name="frame_gen_target_rate_off">Use a fixed multiplier</string>
<string name="frame_gen_target_rate_60">60 FPS</string>
<string name="frame_gen_target_rate_90">90 FPS</string>
<string name="frame_gen_target_rate_120">120 FPS</string>
<string name="frame_gen_target_rate_144">144 FPS</string>
<string name="frame_gen_target_rate_165">165 FPS</string>
<string name="frame_gen_queue_target">Frame queue target</string>
<string name="frame_gen_queue_target_description">How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency.</string>
<string name="frame_gen_queue_target_0">Lowest latency (Unbuffered)</string>
<string name="frame_gen_queue_target_1">Balanced (1 frame)</string>
<string name="frame_gen_queue_target_2">Smoothest (2 frames)</string>
<string name="frame_gen_flow_scale_auto">Match motion estimation to the game</string>
<string name="frame_gen_flow_scale_auto_description">Estimate motion at the resolution the game actually renders instead of the upscaled output. Costs nothing in accuracy, since upscaling adds no motion detail.</string>
<string name="frame_gen_flow_scale">Motion estimation resolution</string>
<string name="frame_gen_flow_scale_description">Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance.</string>
<string name="frame_gen_fp16">Half precision shaders</string>
<string name="frame_gen_fp16_description">Use the 16-bit shader variant. Falls back automatically if the driver or the file lacks it.</string>
<string name="frame_gen_dump_flow">Dump generated frame</string>
<string name="frame_gen_dump_flow_description">Write the optical flow mip levels and the interpolated frame to the lossless/debug folder once, for troubleshooting</string>
<string name="frame_gen_unsupported">Frame generation unavailable</string>
<string name="frame_gen_unsupported_description">This GPU driver does not support the Vulkan memory model, which the Lossless Scaling shaders require.</string>
<string name="lossless_scaling_setup_description">Optional. Provide your own Lossless.dll to enable frame generation later</string>
<string name="lossless_scaling_install">Install Lossless.dll</string>
<string name="lossless_scaling_install_description">Frame generation needs your own legal copy of Lossless.dll from Lossless Scaling</string>
<string name="lossless_scaling_replace_description">Select a different copy of Lossless.dll</string>
<string name="frame_generation_support">Frame generation</string>
<string name="frame_generation_supported">Supported</string>
<string name="frame_generation_unsupported">Unsupported (no Vulkan memory model)</string>
<string name="lossless_scaling">Lossless Scaling</string>
<string name="lossless_scaling_description">Provide your own copy of Lossless.dll to enable frame generation</string>
<string name="lossless_scaling_installed">Installed</string>
<string name="lossless_scaling_not_installed">Not installed</string>
<string name="lossless_scaling_replace">Replace</string>
<string name="lossless_scaling_remove">Remove</string>
<string name="lossless_scaling_remove_description">Delete the installed Lossless.dll and its prepared shaders</string>
<string name="lossless_scaling_remove_confirmation">Frame generation will stop working until you install Lossless.dll again. Your original file is not affected.</string>
<string name="lossless_scaling_missing">Lossless.dll not installed</string>
<string name="lossless_scaling_missing_description">Install it from Settings Lossless Scaling to use frame generation.</string>
<string name="lossless_scaling_locked">Close the game first</string>
<string name="lossless_scaling_locked_description">Lossless.dll cannot be changed while a game is running.</string>
<string name="lossless_scaling_remove_unavailable">Nothing to remove</string>
<string name="lossless_scaling_remove_unavailable_description">Lossless.dll is not installed yet.</string>
<string name="lossless_scaling_installing">Preparing frame generation shaders…</string>
<string name="lossless_scaling_install_success">Lossless.dll installed successfully</string>
<string name="lossless_scaling_install_failed">Could not install Lossless.dll</string>
<string name="error_lossless_copy_failed">The selected file could not be copied.</string>
<string name="error_lossless_unreadable">The selected file could not be read.</string>
<string name="error_lossless_not_pe">The selected file is not a Windows library. Select Lossless.dll from your Lossless Scaling installation.</string>
<string name="error_lossless_missing_shaders">This copy of Lossless.dll does not contain the frame generation shaders. Update Lossless Scaling and try again.</string>
<string name="error_lossless_translation_failed">The frame generation shaders could not be translated. This version of Lossless Scaling is not supported yet.</string>
<string name="error_lossless_cache_failed">The translated shaders could not be written to storage. Check that there is free space available.</string>
<string name="advanced_settings">Advanced settings</string>
<string name="settings_description">Configure emulator settings</string>
<string name="search_recently_played">Recently played</string>
@@ -1246,6 +1185,9 @@
<string name="multiplier_x4" translatable="false">x4</string>
<string name="multiplier_x8" translatable="false">x8</string>
<string name="multiplier_x16" translatable="false">x16</string>
<string name="multiplier_x32" translatable="false">x32</string>
<string name="multiplier_x64" translatable="false">x64</string>
<string name="multiplier_none">None</string>
<!-- Black backgrounds theme -->
<string name="use_black_backgrounds">Black backgrounds</string>
+9 -5
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "device_power_state.h"
@@ -14,11 +14,14 @@ extern std::atomic<bool> g_has_battery;
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#if TARGET_OS_MAC
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC
#if TARGET_OS_IPHONE
// ios doesnt have this
#else
#include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h>
#endif
#endif
#elif defined(__linux__)
#include <fstream>
#include <string>
@@ -48,7 +51,9 @@ namespace Common {
info.percentage = g_battery_percentage.load(std::memory_order_relaxed);
info.charging = g_is_charging.load(std::memory_order_relaxed);
info.has_battery = g_has_battery.load(std::memory_order_relaxed);
#elif defined(__APPLE__) && TARGET_OS_IPHONE
// Not implemented
info.has_battery = false;
#elif defined(__APPLE__) && TARGET_OS_MAC
CFTypeRef info_ref = IOPSCopyPowerSourcesInfo();
CFArrayRef sources = IOPSCopyPowerSourcesList(info_ref);
@@ -96,7 +101,6 @@ namespace Common {
#else
info.has_battery = false;
#endif
return info;
}
}
+1 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -20,7 +20,6 @@
#define KEYS_DIR "keys"
#define LOAD_DIR "load"
#define LOG_DIR "log"
#define LOSSLESS_DIR "lossless"
#define NAND_DIR "nand"
#define PLAY_TIME_DIR "play_time"
#define SCREENSHOTS_DIR "screenshots"
@@ -38,5 +37,3 @@
// yuzu-specific files
#define LOG_FILE "eden_log.txt"
#define LOSSLESS_DLL_FILE "Lossless.dll"
#define LOSSLESS_CACHE_FILE "lsfg_spirv.cache"
-1
View File
@@ -157,7 +157,6 @@ public:
GenerateEdenPath(EdenPath::KeysDir, eden_path / KEYS_DIR);
GenerateEdenPath(EdenPath::LoadDir, eden_path / LOAD_DIR);
GenerateEdenPath(EdenPath::LogDir, eden_path / LOG_DIR);
GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR);
GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR);
GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR);
GenerateEdenPath(EdenPath::SaveDir, eden_path / NAND_DIR);
+1 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -23,7 +23,6 @@ enum class EdenPath {
KeysDir, // Where key files are stored.
LoadDir, // Where cheat/mod files are stored.
LogDir, // Where log files are stored.
LosslessDir, // Where the user-supplied Lossless Scaling library is stored.
NANDDir, // Where the emulated NAND is stored.
PlayTimeDir, // Where play time data is stored.
SaveDir, // Where save data is stored.
+4
View File
@@ -27,7 +27,11 @@
#include <sys/random.h>
#elif defined(__APPLE__)
#include <sys/types.h>
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
// Not available on iOS for some fucking stupid reason...
#else
#include <sys/random.h>
#endif
#include <mach/vm_map.h>
#include <mach/mach.h>
#elif defined(__FreeBSD__)
-22
View File
@@ -380,28 +380,6 @@ void UpdateRescalingInfo() {
TranslateResolutionInfo(setup, info);
}
u32 FrameGenMultiplier() {
return std::clamp(values.frame_gen_multiplier.GetValue(), MIN_FRAME_GEN_MULTIPLIER,
MAX_FRAME_GEN_MULTIPLIER);
}
size_t FrameGenGenerations() {
if (!values.frame_gen.GetValue()) {
return 0;
}
return FrameGenMultiplier() - 1;
}
size_t FrameGenMaxGenerations() {
if (!values.frame_gen.GetValue()) {
return 0;
}
if (values.frame_gen_target_rate.GetValue() != 0) {
return MAX_FRAME_GEN_MULTIPLIER - 1;
}
return FrameGenMultiplier() - 1;
}
void RestoreGlobalState(bool is_powered_on) {
// If a game is running, DO NOT restore the global settings state
if (is_powered_on) {
+8 -76
View File
@@ -352,7 +352,7 @@ struct Values {
true};
SwitchableSetting<ScalingFilter> scaling_filter{linkage,
ScalingFilter::NearestNeighbor,
ScalingFilter::Bilinear,
"scaling_filter",
Category::Renderer,
Specialization::Default,
@@ -388,69 +388,6 @@ struct Values {
true,
true};
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
Specialization::Default, true, false};
SwitchableSetting<u32, true> frame_gen_multiplier{linkage,
2,
2,
4,
"frame_gen_multiplier",
Category::Renderer,
Specialization::Countable,
true,
false,
&frame_gen};
SwitchableSetting<u32, true> frame_gen_target_rate{linkage,
0,
0,
240,
"frame_gen_target_rate",
Category::Renderer,
Specialization::Countable,
true,
true,
&frame_gen};
SwitchableSetting<bool> frame_gen_flow_scale_auto{linkage,
true,
"frame_gen_flow_scale_auto",
Category::Renderer,
Specialization::Default,
true,
false,
&frame_gen};
SwitchableSetting<u32, true> frame_gen_flow_scale{linkage,
75,
25,
100,
"frame_gen_flow_scale",
Category::Renderer,
Specialization::Countable |
Specialization::Percentage,
true,
true,
&frame_gen};
SwitchableSetting<u32, true> frame_gen_queue_target{linkage,
1,
0,
2,
"frame_gen_queue_target",
Category::Renderer,
Specialization::Countable,
true,
false,
&frame_gen};
SwitchableSetting<bool> frame_gen_fp16{linkage, true, "frame_gen_fp16", Category::Renderer,
Specialization::Default, true, false, &frame_gen};
SwitchableSetting<bool> frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow",
Category::Renderer};
SwitchableSetting<bool> use_asynchronous_gpu_emulation{linkage,
#ifdef __ANDROID__
false,
@@ -632,8 +569,13 @@ struct Values {
SwitchableSetting<bool> emulate_bgr565{linkage, false, "emulate_bgr565",
Category::RendererHacks};
SwitchableSetting<bool> rescale_hack{linkage, false, "rescale_hack",
Category::RendererHacks};
SwitchableSetting<bool> rescale_hack{linkage,
#ifdef __ANDROID__
true,
#else
false,
#endif
"rescale_hack", Category::RendererHacks};
SwitchableSetting<bool> enable_gpu_buffer_readback{linkage,
false,
"enable_gpu_buffer_readback",
@@ -933,20 +875,10 @@ struct Values {
// Per-game overrides
bool use_squashed_iterated_blend;
};
extern Values values;
constexpr u32 MIN_FRAME_GEN_MULTIPLIER = 2;
constexpr u32 MAX_FRAME_GEN_MULTIPLIER = 4;
[[nodiscard]] u32 FrameGenMultiplier();
[[nodiscard]] size_t FrameGenGenerations();
[[nodiscard]] size_t FrameGenMaxGenerations();
bool getDebugKnobAt(u8 i);
void UpdateGPUAccuracy();
+1 -1
View File
@@ -128,7 +128,7 @@ ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt
GmtPlusZero, GmtMinusZero, GmtZero, Greenwich, Hongkong, Hst, Iceland, Iran, Israel, Jamaica,
Japan, Kwajalein, Libya, Met, Mst, Mst7Mdt, Navajo, Nz, NzChat, Poland, Portugal, Prc, Pst8Pdt,
Roc, Rok, Singapore, Turkey, Uct, Universal, Utc, WSu, Wet, Zulu);
ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16);
ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16, X32, X64, None);
ENUM(AstcDecodeMode, Cpu, Gpu, CpuAsynchronous);
ENUM(AstcRecompression, Uncompressed, Bc1, Bc3);
ENUM(FramePacingMode, Target_Auto, Target_30, Target_60, Target_90, Target_120);
+107 -6
View File
@@ -116,18 +116,119 @@ std::string ReplaceAll(std::string result, const std::string& src, const std::st
}
std::string UTF16ToUTF8(std::u16string_view input) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
return convert.to_bytes(input.data(), input.data() + input.size());
std::string result;
result.reserve(input.size());
for (size_t i = 0; i < input.size(); ++i) {
uint32_t codepoint = input[i];
// Handle surrogate pairs
if (codepoint >= 0xD800 && codepoint <= 0xDBFF) {
if (i + 1 < input.size()) {
uint32_t low = input[i + 1];
if (low >= 0xDC00 && low <= 0xDFFF) {
codepoint = ((codepoint - 0xD800) << 10) + (low - 0xDC00) + 0x10000;
++i;
}
}
}
if (codepoint <= 0x7F) {
result.push_back(static_cast<char>(codepoint));
} else if (codepoint <= 0x7FF) {
result.push_back(static_cast<char>(0xC0 | (codepoint >> 6)));
result.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
} else if (codepoint <= 0xFFFF) {
result.push_back(static_cast<char>(0xE0 | (codepoint >> 12)));
result.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F)));
result.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
} else {
result.push_back(static_cast<char>(0xF0 | (codepoint >> 18)));
result.push_back(static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F)));
result.push_back(static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F)));
result.push_back(static_cast<char>(0x80 | (codepoint & 0x3F)));
}
}
return result;
}
std::u16string UTF8ToUTF16(std::string_view input) {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
return convert.from_bytes(input.data(), input.data() + input.size());
std::u16string result;
size_t i = 0;
while (i < input.size()) {
uint32_t codepoint = 0;
unsigned char c = input[i];
size_t extra = 0;
if ((c & 0x80) == 0) {
codepoint = c;
extra = 0;
} else if ((c & 0xE0) == 0xC0) {
codepoint = c & 0x1F;
extra = 1;
} else if ((c & 0xF0) == 0xE0) {
codepoint = c & 0x0F;
extra = 2;
} else if ((c & 0xF8) == 0xF0) {
codepoint = c & 0x07;
extra = 3;
} else {
// Invalid UTF-8
++i;
continue;
}
if (i + extra >= input.size()) break;
for (size_t j = 1; j <= extra; ++j) {
if ((input[i + j] & 0xC0) != 0x80) {
codepoint = 0xFFFD;
break;
}
codepoint = (codepoint << 6) | (input[i + j] & 0x3F);
}
if (codepoint <= 0xFFFF) {
result.push_back(static_cast<char16_t>(codepoint));
} else {
codepoint -= 0x10000;
result.push_back(static_cast<char16_t>(0xD800 + (codepoint >> 10)));
result.push_back(static_cast<char16_t>(0xDC00 + (codepoint & 0x3FF)));
}
i += extra + 1;
}
return result;
}
std::u32string UTF8ToUTF32(std::string_view input) {
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert;
return convert.from_bytes(input.data(), input.data() + input.size());
std::u32string result;
size_t i = 0;
while (i < input.size()) {
uint32_t codepoint = 0;
unsigned char c = input[i];
size_t extra = 0;
if ((c & 0x80) == 0) {
codepoint = c;
extra = 0;
} else if ((c & 0xE0) == 0xC0) {
codepoint = c & 0x1F;
extra = 1;
} else if ((c & 0xF0) == 0xE0) {
codepoint = c & 0x0F;
extra = 2;
} else if ((c & 0xF8) == 0xF0) {
codepoint = c & 0x07;
extra = 3;
} else {
// Invalid UTF-8
++i;
continue;
}
if (i + extra >= input.size()) break;
for (size_t j = 1; j <= extra; ++j) {
if ((input[i + j] & 0xC0) != 0x80) {
codepoint = 0xFFFD;
break;
}
codepoint = (codepoint << 6) | (input[i + j] & 0x3F);
}
result.push_back(codepoint);
i += extra + 1;
}
return result;
}
#ifdef _WIN32
+4
View File
@@ -1269,6 +1269,10 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64 OR ARCHITECTURE_riscv64 OR ARCHITE
hle/service/jit/jit.cpp
hle/service/jit/jit.h)
target_link_libraries(core PRIVATE dynarmic::dynarmic)
# Quick hack for XCode generator...
if (IOS)
target_include_directories(core PRIVATE "${CMAKE_SOURCE_DIR}/dynarmic/src")
endif()
endif()
target_sources(core PRIVATE hle/service/ssl/ssl_backend_openssl.cpp)
+1 -1
View File
@@ -6,7 +6,7 @@
#pragma once
#include <dynarmic/interface/halt_reason.h>
#include "dynarmic/src/dynarmic/interface/halt_reason.h"
#include "core/arm/arm_interface.h"
+2 -2
View File
@@ -6,8 +6,8 @@
#pragma once
#include <dynarmic/interface/A32/a32.h>
#include <dynarmic/interface/code_page.h>
#include "dynarmic/src/dynarmic/interface/A32/a32.h"
#include "dynarmic/src/dynarmic/interface/code_page.h"
#include "core/arm/arm_interface.h"
#include "core/arm/dynarmic/dynarmic_exclusive_monitor.h"
+6 -6
View File
@@ -10,12 +10,12 @@
#include <memory>
#include <ankerl/unordered_dense.h>
#include <dynarmic/interface/A64/a64.h>
#include <dynarmic/interface/code_page.h>
#include "common/common_types.h"
#include "common/hash.h"
#include "core/arm/arm_interface.h"
#include "core/arm/dynarmic/dynarmic_exclusive_monitor.h"
#include "../../../dynarmic/src/dynarmic/interface/A64/a64.h"
#include "../../../dynarmic/src/dynarmic/interface/code_page.h"
#include "../../../common/common_types.h"
#include "../../../common/hash.h"
#include "../arm_interface.h"
#include "dynarmic_exclusive_monitor.h"
namespace Core::Memory {
class Memory;
+4 -1
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -5,7 +8,7 @@
#include <optional>
#include <dynarmic/interface/A32/coprocessor.h>
#include "dynarmic/interface/A32/coprocessor.h"
#include "common/common_types.h"
namespace Core {
@@ -1,9 +1,12 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <dynarmic/interface/exclusive_monitor.h>
#include "dynarmic/src/dynarmic/interface/exclusive_monitor.h"
#include "common/common_types.h"
#include "core/arm/exclusive_monitor.h"
+6 -3
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2023 merryhime <https://mary.rs>
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -7,9 +10,9 @@
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wshadow"
#include <dynarmic/frontend/A64/a64_types.h>
#include <dynarmic/frontend/A64/decoder/a64.h>
#include <dynarmic/frontend/imm.h>
#include "dynarmic/frontend/A64/a64_types.h"
#include "dynarmic/frontend/A64/decoder/a64.h"
#include "dynarmic/frontend/imm.h"
#pragma GCC diagnostic pop
+3 -3
View File
@@ -8,9 +8,9 @@
#include <map>
#include <span>
#include <boost/icl/interval_set.hpp>
#include <dynarmic/interface/A64/a64.h>
#include <dynarmic/interface/A64/config.h>
#include <dynarmic/interface/code_page.h>
#include "dynarmic/interface/A64/a64.h"
#include "dynarmic/interface/A64/config.h"
#include "dynarmic/interface/code_page.h"
#include "common/alignment.h"
#include "common/common_funcs.h"
@@ -4,7 +4,6 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cstring>
#include "common/assert.h"
#include "common/logging.h"
@@ -265,7 +264,7 @@ NvResult nvhost_ctrl_gpu::ZCullGetInfo(IoctlNvgpuGpuZcullGetInfoArgs& params) {
}
NvResult nvhost_ctrl_gpu::ZBCSetTable(IoctlZbcSetTable& params) {
if (params.type == 0 || params.type > supported_types) {
if (params.type > supported_types) {
LOG_ERROR(Service_NVDRV, "ZBCSetTable: invalid type {:#x}", params.type);
return NvResult::BadParameter;
}
@@ -280,61 +279,42 @@ NvResult nvhost_ctrl_gpu::ZBCSetTable(IoctlZbcSetTable& params) {
color_entry.format = params.format;
color_entry.ref_cnt = 1u;
const auto color_end = zbc_colors.begin() + zbc_used_color_entries;
auto color_it = std::find_if(zbc_colors.begin(), color_end,
[&](const ZbcColorEntry& color_in_question) {
return color_entry.format == color_in_question.format &&
color_entry.color_ds == color_in_question.color_ds &&
color_entry.color_l2 == color_in_question.color_l2;
});
auto color_it = std::ranges::find_if(zbc_colors,
[&](const ZbcColorEntry& color_in_question) {
return color_entry.format == color_in_question.format &&
color_entry.color_ds == color_in_question.color_ds &&
color_entry.color_l2 == color_in_question.color_l2;
});
if (color_it != color_end) {
if (color_it != zbc_colors.end()) {
++color_it->ref_cnt;
LOG_DEBUG(Service_NVDRV, "ZBCSetTable: reused color entry fmt={:#x}, ref_cnt={:#x}",
params.format, color_it->ref_cnt);
break;
} else {
zbc_colors.push_back(color_entry);
LOG_DEBUG(Service_NVDRV, "ZBCSetTable: added color entry fmt={:#x}, index={:#x}",
params.format, zbc_colors.size() - 1);
}
if (zbc_used_color_entries >= zbc_table_size) {
LOG_WARNING(Service_NVDRV, "ZBCSetTable: color table is full, fmt={:#x}",
params.format);
return NvResult::InsufficientMemory;
}
zbc_colors[zbc_used_color_entries] = color_entry;
LOG_DEBUG(Service_NVDRV, "ZBCSetTable: added color entry fmt={:#x}, index={:#x}",
params.format, zbc_used_color_entries);
++zbc_used_color_entries;
break;
}
case ZBCTypes::depth: {
ZbcDepthEntry depth_entry{params.depth, params.format, 1u};
const auto depth_end = zbc_depths.begin() + zbc_used_depth_entries;
auto depth_it = std::find_if(zbc_depths.begin(), depth_end,
[&](const ZbcDepthEntry& depth_entry_in_question) {
return depth_entry.format == depth_entry_in_question.format &&
depth_entry.depth == depth_entry_in_question.depth;
});
auto depth_it = std::ranges::find_if(zbc_depths,
[&](const ZbcDepthEntry& depth_entry_in_question) {
return depth_entry.format == depth_entry_in_question.format &&
depth_entry.depth == depth_entry_in_question.depth;
});
if (depth_it != depth_end) {
if (depth_it != zbc_depths.end()) {
++depth_it->ref_cnt;
LOG_DEBUG(Service_NVDRV, "ZBCSetTable: reused depth entry fmt={:#x}, ref_cnt={:#x}",
depth_entry.format, depth_it->ref_cnt);
break;
} else {
zbc_depths.push_back(depth_entry);
LOG_DEBUG(Service_NVDRV, "ZBCSetTable: added depth entry fmt={:#x}, index={:#x}",
depth_entry.format, zbc_depths.size() - 1);
}
if (zbc_used_depth_entries >= zbc_table_size) {
LOG_WARNING(Service_NVDRV, "ZBCSetTable: depth table is full, fmt={:#x}",
depth_entry.format);
return NvResult::InsufficientMemory;
}
zbc_depths[zbc_used_depth_entries] = depth_entry;
LOG_DEBUG(Service_NVDRV, "ZBCSetTable: added depth entry fmt={:#x}, index={:#x}",
depth_entry.format, zbc_used_depth_entries);
++zbc_used_depth_entries;
break;
}
}
@@ -349,34 +329,35 @@ NvResult nvhost_ctrl_gpu::ZBCQueryTable(IoctlZbcQueryTable& params) {
std::scoped_lock lk(zbc_mutex);
if (params.type == 0) {
params.index_size = zbc_table_size;
return NvResult::Success;
}
if (params.index_size >= zbc_table_size) {
LOG_ERROR(Service_NVDRV, "ZBCQueryTable: invalid index {:#x}", params.index_size);
return NvResult::BadParameter;
}
switch (static_cast<ZBCTypes>(params.type)) {
case ZBCTypes::color: {
if (params.index_size >= zbc_colors.size()) {
LOG_ERROR(Service_NVDRV, "ZBCQueryTable: invalid color index {:#x}", params.index_size);
return NvResult::BadParameter;
}
const auto& colors = zbc_colors[params.index_size];
std::copy_n(colors.color_ds.begin(), colors.color_ds.size(), std::begin(params.color_ds));
std::copy_n(colors.color_l2.begin(), colors.color_l2.size(), std::begin(params.color_l2));
params.depth = 0;
params.ref_cnt = colors.ref_cnt;
params.format = colors.format;
params.index_size = static_cast<u32>(zbc_colors.size());
break;
}
case ZBCTypes::depth: {
if (params.index_size >= zbc_depths.size()) {
LOG_ERROR(Service_NVDRV, "ZBCQueryTable: invalid depth index {:#x}", params.index_size);
return NvResult::BadParameter;
}
const auto& depth_entry = zbc_depths[params.index_size];
std::fill(std::begin(params.color_ds), std::end(params.color_ds), 0);
std::fill(std::begin(params.color_l2), std::end(params.color_l2), 0);
params.depth = depth_entry.depth;
params.ref_cnt = depth_entry.ref_cnt;
params.format = depth_entry.format;
break;
params.index_size = static_cast<u32>(zbc_depths.size());
}
}
@@ -6,7 +6,7 @@
#pragma once
#include <array>
#include <vector>
#include "common/common_funcs.h"
#include "common/common_types.h"
@@ -212,14 +212,10 @@ private:
Kernel::KEvent* unknown_event;
// ZBC Tables
static constexpr u32 zbc_table_size = 15u;
static constexpr u32 supported_types = 2u;
std::mutex zbc_mutex{};
std::array<ZbcColorEntry, zbc_table_size> zbc_colors{};
std::array<ZbcDepthEntry, zbc_table_size> zbc_depths{};
u32 zbc_used_color_entries{};
u32 zbc_used_depth_entries{};
std::vector<ZbcColorEntry> zbc_colors{};
std::vector<ZbcDepthEntry> zbc_depths{};
const u32 supported_types = 2u;
};
} // namespace Service::Nvidia::Devices
@@ -88,10 +88,25 @@ Result IApplicationDisplayService::GetIndirectDisplayTransactionService(
}
Result IApplicationDisplayService::OpenDisplay(Out<u64> out_display_id, DisplayName display_name) {
LOG_DEBUG(Service_VI, "called with display_name={}", display_name.data());
// Ensure the display name is null-terminated
display_name[display_name.size() - 1] = '\0';
LOG_DEBUG(Service_VI, "called with display_name={}", display_name.data());
// According to switchbrew, only "Default", "External", "Edid", "Internal" and "Null" are valid
const std::array<std::string_view, 5> valid_names = {
"Default", "External", "Edid", "Internal", "Null"
};
bool valid_name = false;
for (const auto& name : valid_names) {
if (name == display_name.data()) {
valid_name = true;
break;
}
}
R_UNLESS(valid_name, ResultOperationFailed);
R_RETURN(m_container->OpenDisplay(out_display_id, display_name));
}
@@ -1,29 +0,0 @@
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
function(target_architecture_specific_sources project arch)
if (NOT MULTIARCH_BUILD)
target_sources("${project}" PRIVATE ${ARGN})
return()
endif()
foreach(input_file IN LISTS ARGN)
if(input_file MATCHES ".cpp$")
if(NOT IS_ABSOLUTE ${input_file})
set(input_file "${CMAKE_CURRENT_SOURCE_DIR}/${input_file}")
endif()
set(output_file "${CMAKE_CURRENT_BINARY_DIR}/arch_gen/${input_file}")
add_custom_command(
OUTPUT "${output_file}"
COMMAND ${CMAKE_COMMAND} "-Darch=${arch}"
"-Dinput_file=${input_file}"
"-Doutput_file=${output_file}"
-P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/impl/TargetArchitectureSpecificSourcesWrapFile.cmake"
DEPENDS "${input_file}"
VERBATIM
)
target_sources(${project} PRIVATE "${output_file}")
endif()
endforeach()
endfunction()
+2 -3
View File
@@ -1,6 +1,5 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
include(TargetArchitectureSpecificSources)
add_library(dynarmic STATIC
mcl/bit.hpp
@@ -143,7 +142,7 @@ if ("x86_64" IN_LIST ARCHITECTURE)
target_compile_definitions(dynarmic PRIVATE XBYAK_OLD_DISP_CHECK=1)
target_link_libraries(dynarmic PRIVATE xbyak::xbyak)
target_architecture_specific_sources(dynarmic "x86_64"
target_sources(dynarmic PRIVATE
backend/x64/abi.cpp
backend/x64/abi.h
backend/x64/block_of_code.cpp
@@ -204,7 +203,7 @@ endif()
if ("arm64" IN_LIST ARCHITECTURE)
target_link_libraries(dynarmic PRIVATE merry::oaknut)
target_architecture_specific_sources(dynarmic "arm64"
target_sources(dynarmic PRIVATE
backend/arm64/a32_jitstate.cpp
backend/arm64/a32_jitstate.h
backend/arm64/a64_jitstate.h
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/* This file is part of the dynarmic project.
@@ -14,8 +14,8 @@
#include <string>
#include <vector>
#include "dynarmic/interface/A32/config.h"
#include "dynarmic/interface/halt_reason.h"
#include "config.h"
#include "dynarmic/src/dynarmic/interface/halt_reason.h"
namespace Dynarmic {
namespace A32 {
@@ -14,9 +14,9 @@
#include <memory>
#include <optional>
#include "dynarmic/frontend/A32/translate/translate_callbacks.h"
#include "dynarmic/interface/A32/arch_version.h"
#include "dynarmic/interface/optimization_flags.h"
#include "../../frontend/A32/translate/translate_callbacks.h"
#include "arch_version.h"
#include "../optimization_flags.h"
namespace Dynarmic {
class ExclusiveMonitor;
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/* This file is part of the dynarmic project.
@@ -15,8 +15,8 @@
#include <string>
#include <vector>
#include "dynarmic/interface/A64/config.h"
#include "dynarmic/interface/halt_reason.h"
#include "config.h"
#include "../halt_reason.h"
namespace Dynarmic {
namespace A64 {
@@ -14,7 +14,7 @@
#include <memory>
#include <optional>
#include "dynarmic/interface/optimization_flags.h"
#include "../optimization_flags.h"
namespace Dynarmic {
class ExclusiveMonitor;
@@ -14,7 +14,7 @@
#include <cstring>
#include <boost/container/static_vector.hpp>
#include <dynarmic/common/spin_lock.h>
#include "dynarmic/src/dynarmic/common/spin_lock.h"
namespace Dynarmic {
+1 -2
View File
@@ -1,6 +1,5 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
include(TargetArchitectureSpecificSources)
add_executable(dynarmic_tests
fp/FPToFixed.cpp
@@ -50,7 +49,7 @@ endif()
if ("x86_64" IN_LIST ARCHITECTURE)
target_link_libraries(dynarmic_tests PRIVATE xbyak::xbyak)
target_architecture_specific_sources(dynarmic_tests "x86_64"
target_sources(dynarmic PRIVATE
x64_cpu_info.cpp
native/preserve_xmm.cpp
)
+11
View File
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#ifndef AppUI_Bridging_Header_h
#define AppUI_Bridging_Header_h
#import "AppUIObjC.h"
#endif /* AppUI_Bridging_Header_h */
+113
View File
@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
import UIKit
import Foundation
import QuartzCore.CAMetalLayer
import SwiftUI
public struct AppUI {
public static let shared = AppUI()
fileprivate let appUIObjC = AppUIObjC.shared()
public func configure(layer: CAMetalLayer, with size: CGSize) {
appUIObjC.configure(layer: layer, with: size)
}
public func information(for url: URL) -> AppUIInformation {
appUIObjC.gameInformation.information(for: url)
}
public func insert(game url: URL) {
appUIObjC.insert(game: url)
}
public func insert(games urls: [URL]) {
appUIObjC.insert(games: urls)
}
public func bootOS() {
appUIObjC.bootOS()
}
public func pause() {
appUIObjC.pause()
}
public func play() {
appUIObjC.play()
}
public func ispaused() -> Bool {
return appUIObjC.ispaused()
}
public func FirstFrameShowed() -> Bool {
return appUIObjC.hasfirstfame()
}
public func canGetFullPath() -> Bool {
return appUIObjC.canGetFullPath()
}
public func exit() {
appUIObjC.quit()
}
public func step() {
appUIObjC.step()
}
public func orientationChanged(orientation: UIInterfaceOrientation, with layer: CAMetalLayer, size: CGSize) {
appUIObjC.orientationChanged(orientation: orientation, with: layer, size: size)
}
public func touchBegan(at point: CGPoint, for index: UInt) {
appUIObjC.touchBegan(at: point, for: index)
}
public func touchEnded(for index: UInt) {
appUIObjC.touchEnded(for: index)
}
public func touchMoved(at point: CGPoint, for index: UInt) {
appUIObjC.touchMoved(at: point, for: index)
}
public func gyroMoved(x: Float, y: Float, z: Float, accelX: Float, accelY: Float, accelZ: Float, controllerId: Int32, deltaTimestamp: Int32) {
// Calling the Objective-C function with both gyroscope and accelerometer data
appUIObjC.virtualControllerGyro(controllerId,
deltaTimestamp: deltaTimestamp,
gyroX: x, gyroY: y, gyroZ: z,
accelX: accelX, accelY: accelY, accelZ: accelZ)
}
public func thumbstickMoved(analog: VirtualControllerAnalogType, x: Float, y: Float, controllerid: Int) {
appUIObjC.thumbstickMoved(analog, x: CGFloat(x), y: CGFloat(y), controllerId: Int32(controllerid))
}
public func virtualControllerButtonDown(button: VirtualControllerButtonType, controllerid: Int) {
appUIObjC.virtualControllerButtonDown(button, controllerId: Int32(controllerid))
}
public func virtualControllerButtonUp(button: VirtualControllerButtonType, controllerid: Int) {
appUIObjC.virtualControllerButtonUp(button, controllerId: Int32(controllerid))
}
public func settingsSaved() {
appUIObjC.settingsChanged()
}
}
struct EdenApp: App {
@StateObject private var core = EmulationViewModel()
var body: some Scene {
WindowGroup {
ContentView(core: core)
}
}
}
+26
View File
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AppUIInformation : NSObject
@property (nonatomic, strong) NSString *developer;
@property (nonatomic, strong) NSData *iconData;
@property (nonatomic) BOOL isHomebrew;
@property (nonatomic) uint64_t programID;
@property (nonatomic, strong) NSString *title, *version;
-(AppUIInformation *) initWithDeveloper:(NSString *)developer iconData:(NSData *)iconData isHomebrew:(BOOL)isHomebrew programID:(uint64_t)programID title:(NSString *)title version:(NSString *)version;
@end
@interface AppUIGameInformation : NSObject
+(AppUIGameInformation *) sharedInstance NS_SWIFT_NAME(shared());
-(AppUIInformation *) informationForGame:(NSURL *)url NS_SWIFT_NAME(information(for:));
@end
NS_ASSUME_NONNULL_END
+436
View File
@@ -0,0 +1,436 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#import <Foundation/Foundation.h>
#import "AppUIGameInformation.h"
#import "EmulationSession.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "core/core.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/patch_manager.h"
#include "core/loader/loader.h"
#include "core/loader/nro.h"
#include "frontend_common/config.h"
struct GameMetadata {
std::string title;
u64 programId;
std::string developer;
std::string version;
std::vector<u8> icon;
bool isHomebrew;
};
class SdlConfig final : public Config {
public:
explicit SdlConfig(std::optional<std::string> config_path);
~SdlConfig() override;
void ReloadAllValues() override;
void SaveAllValues() override;
protected:
void ReadSdlValues();
void ReadSdlPlayerValues(std::size_t player_index);
void ReadSdlControlValues();
void ReadHidbusValues() override;
void ReadDebugControlValues() override;
void ReadPathValues() override {}
void ReadShortcutValues() override {}
void ReadUIValues() override {}
void ReadUIGamelistValues() override {}
void ReadUILayoutValues() override {}
void ReadMultiplayerValues() override {}
void SaveSdlValues();
void SaveSdlPlayerValues(std::size_t player_index);
void SaveSdlControlValues();
void SaveHidbusValues() override;
void SaveDebugControlValues() override;
void SavePathValues() override {}
void SaveShortcutValues() override {}
void SaveUIValues() override {}
void SaveUIGamelistValues() override {}
void SaveUILayoutValues() override {}
void SaveMultiplayerValues() override {}
std::vector<Settings::BasicSetting*>& FindRelevantList(Settings::Category category) override;
public:
static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
static const std::array<int, Settings::NativeMotion::NumMotions> default_motions;
static const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> default_analogs;
static const std::array<int, 2> default_stick_mod;
static const std::array<int, 2> default_ringcon_analogs;
};
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include "common/logging.h"
#include "input_common/main.h"
const std::array<int, Settings::NativeButton::NumButtons> SdlConfig::default_buttons = {
SDL_SCANCODE_A, SDL_SCANCODE_S, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_T,
SDL_SCANCODE_G, SDL_SCANCODE_F, SDL_SCANCODE_H, SDL_SCANCODE_Q, SDL_SCANCODE_W,
SDL_SCANCODE_M, SDL_SCANCODE_N, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_B,
};
const std::array<int, Settings::NativeMotion::NumMotions> SdlConfig::default_motions = {
SDL_SCANCODE_7,
SDL_SCANCODE_8,
};
const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> SdlConfig::default_analogs{
{
{
SDL_SCANCODE_UP,
SDL_SCANCODE_DOWN,
SDL_SCANCODE_LEFT,
SDL_SCANCODE_RIGHT,
},
{
SDL_SCANCODE_I,
SDL_SCANCODE_K,
SDL_SCANCODE_J,
SDL_SCANCODE_L,
},
}};
const std::array<int, 2> SdlConfig::default_stick_mod = {
SDL_SCANCODE_D,
0,
};
const std::array<int, 2> SdlConfig::default_ringcon_analogs{{
0,
0,
}};
SdlConfig::SdlConfig(const std::optional<std::string> config_path) {
Initialize(config_path);
ReadSdlValues();
SaveSdlValues();
}
SdlConfig::~SdlConfig() {
if (global) {
SdlConfig::SaveAllValues();
}
}
void SdlConfig::ReloadAllValues() {
Reload();
ReadSdlValues();
SaveSdlValues();
}
void SdlConfig::SaveAllValues() {
SaveValues();
SaveSdlValues();
}
void SdlConfig::ReadSdlValues() {
ReadSdlControlValues();
}
void SdlConfig::ReadSdlControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
ReadSdlPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
ReadDebugControlValues();
ReadHidbusValues();
EndGroup();
}
void SdlConfig::ReadSdlPlayerValues(const std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix.append("player_").append(ToString(player_index)).append("_");
}
auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig()) {
const auto profile_name =
ReadStringSetting(std::string(player_prefix).append("profile_name"));
if (profile_name.empty()) {
// Use the global input config
player = Settings::values.players.GetValue(true)[player_index];
player.profile_name = "";
return;
}
}
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
auto& player_buttons = player.buttons[i];
player_buttons = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
if (player_buttons.empty()) {
player_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
auto& player_analogs = player.analogs[i];
player_analogs = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
if (player_analogs.empty()) {
player_analogs = default_param;
}
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
auto& player_motions = player.motions[i];
player_motions = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
if (player_motions.empty()) {
player_motions = default_param;
}
}
}
void SdlConfig::ReadDebugControlValues() {
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
auto& debug_pad_buttons = Settings::values.debug_pad_buttons[i];
debug_pad_buttons = ReadStringSetting(
std::string("debug_pad_").append(Settings::NativeButton::mapping[i]), default_param);
if (debug_pad_buttons.empty()) {
debug_pad_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
auto& debug_pad_analogs = Settings::values.debug_pad_analogs[i];
debug_pad_analogs = ReadStringSetting(
std::string("debug_pad_").append(Settings::NativeAnalog::mapping[i]), default_param);
if (debug_pad_analogs.empty()) {
debug_pad_analogs = default_param;
}
}
}
void SdlConfig::ReadHidbusValues() {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
0, 0, default_ringcon_analogs[0], default_ringcon_analogs[1], 0, 0.05f);
auto& ringcon_analogs = Settings::values.ringcon_analogs;
ringcon_analogs = ReadStringSetting(std::string("ring_controller"), default_param);
if (ringcon_analogs.empty()) {
ringcon_analogs = default_param;
}
}
void SdlConfig::SaveSdlValues() {
LOG_DEBUG(Config, "Saving SDL configuration values");
SaveSdlControlValues();
WriteToIni();
}
void SdlConfig::SaveSdlControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
SaveSdlPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
SaveDebugControlValues();
SaveHidbusValues();
EndGroup();
}
void SdlConfig::SaveSdlPlayerValues(const std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix = std::string("player_").append(ToString(player_index)).append("_");
}
const auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig() && player.profile_name.empty()) {
// No custom profile selected
return;
}
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
player.buttons[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
player.analogs[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
player.motions[i], std::make_optional(default_param));
}
}
void SdlConfig::SaveDebugControlValues() {
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
WriteStringSetting(std::string("debug_pad_").append(Settings::NativeButton::mapping[i]),
Settings::values.debug_pad_buttons[i],
std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
WriteStringSetting(std::string("debug_pad_").append(Settings::NativeAnalog::mapping[i]),
Settings::values.debug_pad_analogs[i],
std::make_optional(default_param));
}
}
void SdlConfig::SaveHidbusValues() {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
0, 0, default_ringcon_analogs[0], default_ringcon_analogs[1], 0, 0.05f);
WriteStringSetting(std::string("ring_controller"), Settings::values.ringcon_analogs,
std::make_optional(default_param));
}
std::vector<Settings::BasicSetting*>& SdlConfig::FindRelevantList(Settings::Category category) {
return Settings::values.linkage.by_category[category];
}
std::unordered_map<std::string, GameMetadata> m_game_metadata_cache;
GameMetadata CacheGameMetadata(const std::string& path) {
const auto file =
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
GameMetadata entry;
loader->ReadTitle(entry.title);
loader->ReadProgramId(entry.programId);
loader->ReadIcon(entry.icon);
const FileSys::PatchManager pm{
entry.programId, EmulationSession::GetInstance().System().GetFileSystemController(),
EmulationSession::GetInstance().System().GetContentProvider()};
const auto control = pm.GetControlMetadata();
if (control.first != nullptr) {
entry.developer = control.first->GetDeveloperName();
entry.version = control.first->GetVersionString();
} else {
FileSys::NACP nacp;
if (loader->ReadControlData(nacp) == Loader::ResultStatus::Success) {
entry.developer = nacp.GetDeveloperName();
} else {
entry.developer = "";
}
entry.version = "1.0.0";
}
if (loader->GetFileType() == Loader::FileType::NRO) {
auto loader_nro = reinterpret_cast<Loader::AppLoader_NRO*>(loader.get());
entry.isHomebrew = loader_nro->IsHomebrew();
} else {
entry.isHomebrew = false;
}
m_game_metadata_cache[path] = entry;
return entry;
}
GameMetadata GameMetadata(const std::string& path, bool reload = false) {
if (!EmulationSession::GetInstance().IsInitialized()) {
NSURL *dir_url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
const char *directory_cstr = [[dir_url path] UTF8String];
Common::FS::SetAppDirectory(directory_cstr);
EmulationSession::GetInstance().System().Initialize();
EmulationSession::GetInstance().InitializeSystem(false);
}
if (reload) {
return CacheGameMetadata(path);
}
if (auto search = m_game_metadata_cache.find(path); search != m_game_metadata_cache.end()) {
return search->second;
}
return CacheGameMetadata(path);
}
@implementation AppUIInformation
-(AppUIInformation *) initWithDeveloper:(NSString *)developer iconData:(NSData *)iconData isHomebrew:(BOOL)isHomebrew programID:(uint64_t)programID
title:(NSString *)title version:(NSString *)version {
if (self = [super init]) {
self.developer = developer;
self.iconData = iconData;
self.isHomebrew = isHomebrew;
self.programID = programID;
self.title = title;
self.version = version;
} return self;
}
@end
@implementation AppUIGameInformation
+(AppUIGameInformation *) sharedInstance {
static AppUIGameInformation *sharedInstance = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
-(AppUIInformation *) informationForGame:(NSURL *)url {
auto gameMetadata = GameMetadata([url.path UTF8String]);
return [[AppUIInformation alloc] initWithDeveloper:[NSString stringWithCString:gameMetadata.developer.c_str() encoding:NSUTF8StringEncoding]
iconData:[NSData dataWithBytes:gameMetadata.icon.data() length:gameMetadata.icon.size()]
isHomebrew:gameMetadata.isHomebrew programID:gameMetadata.programId
title:[NSString stringWithCString:gameMetadata.title.c_str() encoding:NSUTF8StringEncoding]
version:[NSString stringWithCString:gameMetadata.version.c_str() encoding:NSUTF8StringEncoding]];
}
@end
+93
View File
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#import <Foundation/Foundation.h>
#import <MetalKit/MetalKit.h>
#import <UIKit/UIKit.h>
#import "AppUIGameInformation.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, VirtualControllerAnalogType) {
VirtualControllerAnalogTypeLeft = 0,
VirtualControllerAnalogTypeRight = 1
};
typedef NS_ENUM(NSUInteger, VirtualControllerButtonType) {
VirtualControllerButtonTypeA = 0,
VirtualControllerButtonTypeB = 1,
VirtualControllerButtonTypeX = 2,
VirtualControllerButtonTypeY = 3,
VirtualControllerButtonTypeL = 4,
VirtualControllerButtonTypeR = 5,
VirtualControllerButtonTypeTriggerL = 6,
VirtualControllerButtonTypeTriggerR = 7,
VirtualControllerButtonTypeTriggerZL = 8,
VirtualControllerButtonTypeTriggerZR = 9,
VirtualControllerButtonTypePlus = 10,
VirtualControllerButtonTypeMinus = 11,
VirtualControllerButtonTypeDirectionalPadLeft = 12,
VirtualControllerButtonTypeDirectionalPadUp = 13,
VirtualControllerButtonTypeDirectionalPadRight = 14,
VirtualControllerButtonTypeDirectionalPadDown = 15,
VirtualControllerButtonTypeSL = 16,
VirtualControllerButtonTypeSR = 17,
VirtualControllerButtonTypeHome = 18,
VirtualControllerButtonTypeCapture = 19
};
@interface AppUIObjC : NSObject {
CAMetalLayer *_layer;
CGSize _size;
}
@property (nonatomic, strong) AppUIGameInformation *gameInformation;
+(AppUIObjC *) sharedInstance NS_SWIFT_NAME(shared());
-(void) configureLayer:(CAMetalLayer *)layer withSize:(CGSize)size NS_SWIFT_NAME(configure(layer:with:));
-(void) bootOS;
-(void) pause;
-(void) play;
-(BOOL) ispaused;
-(BOOL) canGetFullPath;
-(void) quit;
-(void) insertGame:(NSURL *)url NS_SWIFT_NAME(insert(game:));
-(void) insertGames:(NSArray<NSURL *> *)games NS_SWIFT_NAME(insert(games:));
-(void) step;
-(BOOL) hasfirstfame;
-(void) touchBeganAtPoint:(CGPoint)point index:(NSUInteger)index NS_SWIFT_NAME(touchBegan(at:for:));
-(void) touchEndedForIndex:(NSUInteger)index;
-(void) touchMovedAtPoint:(CGPoint)point index:(NSUInteger)index NS_SWIFT_NAME(touchMoved(at:for:));
-(void) thumbstickMoved:(VirtualControllerAnalogType)analog
x:(CGFloat)x
y:(CGFloat)y
controllerId:(int)controllerId;
-(void) virtualControllerGyro:(int)controllerId
deltaTimestamp:(int)delta_timestamp
gyroX:(float)gyro_x
gyroY:(float)gyro_y
gyroZ:(float)gyro_z
accelX:(float)accel_x
accelY:(float)accel_y
accelZ:(float)accel_z;
-(void) virtualControllerButtonDown:(VirtualControllerButtonType)button
controllerId:(int)controllerId;
-(void) virtualControllerButtonUp:(VirtualControllerButtonType)button
controllerId:(int)controllerId;
-(void) orientationChanged:(UIInterfaceOrientation)orientation with:(CAMetalLayer *)layer size:(CGSize)size NS_SWIFT_NAME(orientationChanged(orientation:with:size:));
-(void) settingsChanged;
@end
NS_ASSUME_NONNULL_END
+251
View File
@@ -0,0 +1,251 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#import "AppUIObjC.h"
#import "Config.h"
#import "EmulationSession.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "common/fs/fs.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/savedata_factory.h"
#include "core/loader/nro.h"
#include "frontend_common/content_manager.h"
#include "common/settings_enums.h"
#include "network/announce_multiplayer_session.h"
#include "common/announce_multiplayer_room.h"
#include "network/network.h"
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h"
#include "core/frontend/applets/general.h"
#include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#include "hid_core/hid_core.h"
#include "hid_core/hid_types.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#import <mach/mach.h>
@implementation AppUIObjC
-(AppUIObjC *) init {
if (self = [super init]) {
_gameInformation = [AppUIGameInformation sharedInstance];
NSURL *dir_url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
const char *directory_cstr = [[dir_url path] UTF8String];
Common::FS::SetAppDirectory(directory_cstr);
// Config{"config", Config::ConfigType::GlobalConfig};
EmulationSession::GetInstance().System().Initialize();
EmulationSession::GetInstance().InitializeSystem(false);
EmulationSession::GetInstance().InitializeGpuDriver();
Settings::values.dump_shaders.SetValue(true);
Settings::values.use_asynchronous_shaders.SetValue(true);
// Settings::values.astc_recompression.SetValue(Settings::AstcRecompression::Bc3);
// Settings::values.resolution_setup.SetValue(Settings::ResolutionSetup::Res1X);
// Settings::values.scaling_filter.SetValue(Settings::ScalingFilter::Bilinear);
} return self;
}
+(AppUIObjC *) sharedInstance {
static AppUIObjC *sharedInstance = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (BOOL)ispaused {
return EmulationSession::GetInstance().IsPaused();
}
-(void) pause {
EmulationSession::GetInstance().System().Pause();
EmulationSession::GetInstance().HaltEmulation();
EmulationSession::GetInstance().PauseEmulation();
}
-(void) play {
EmulationSession::GetInstance().System().Run();
EmulationSession::GetInstance().RunEmulation();
EmulationSession::GetInstance().UnPauseEmulation();
}
-(BOOL)hasfirstfame {
@try {
auto* window = &EmulationSession::GetInstance().Window();
if (window && window->HasFirstFrame()) {
return YES;
}
}
@catch (NSException *exception) {
NSLog(@"Exception occurred: %@", exception);
// Handle the exception, maybe return a default value
return NO;
}
return NO;
}
- (BOOL)canGetFullPath {
@try {
Core::System& system = EmulationSession::GetInstance().System();
auto bis_system = system.GetFileSystemController().GetSystemNANDContents();
if (bis_system == nullptr) {
return NO;
}
constexpr u64 QLaunchId = static_cast<u64>(Service::AM::AppletProgramId::QLaunch);
auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
if (qlaunch_applet_nca == nullptr) {
return NO;
}
const auto filename = qlaunch_applet_nca->GetFullPath();
// If GetFullPath() is successful
return YES;
} @catch (NSException *exception) {
// Handle the exception if needed
return NO;
}
}
-(void) quit {
EmulationSession::GetInstance().ShutdownEmulation();
}
-(void) configureLayer:(CAMetalLayer *)layer withSize:(CGSize)size {
_layer = layer;
_size = size;
EmulationSession::GetInstance().SetNativeWindow(layer, size);
}
-(void) bootOS {
EmulationSession::GetInstance().BootOS();
}
-(void) insertGame:(NSURL *)url {
EmulationSession::GetInstance().InitializeEmulation([url.path UTF8String], [_gameInformation informationForGame:url].programID, true);
}
-(void) insertGames:(NSArray<NSURL *> *)games {
for (NSURL *url in games) {
EmulationSession::GetInstance().ConfigureFilesystemProvider([url.path UTF8String]);
}
}
-(void) step {
void(EmulationSession::GetInstance().System().Run());
}
-(void) touchBeganAtPoint:(CGPoint)point index:(NSUInteger)index {
float h_ratio, w_ratio;
h_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().height / (_size.height * [[UIScreen mainScreen] nativeScale]);
w_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().width / (_size.width * [[UIScreen mainScreen] nativeScale]);
EmulationSession::GetInstance().Window().OnTouchPressed([[NSNumber numberWithUnsignedInteger:index] intValue],
(point.x) * [[UIScreen mainScreen] nativeScale] * w_ratio,
((point.y) * [[UIScreen mainScreen] nativeScale] * h_ratio));
}
-(void) touchEndedForIndex:(NSUInteger)index {
EmulationSession::GetInstance().Window().OnTouchReleased([[NSNumber numberWithUnsignedInteger:index] intValue]);
}
-(void) touchMovedAtPoint:(CGPoint)point index:(NSUInteger)index {
float h_ratio, w_ratio;
h_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().height / (_size.height * [[UIScreen mainScreen] nativeScale]);
w_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().width / (_size.width * [[UIScreen mainScreen] nativeScale]);
EmulationSession::GetInstance().Window().OnTouchMoved([[NSNumber numberWithUnsignedInteger:index] intValue],
(point.x) * [[UIScreen mainScreen] nativeScale] * w_ratio,
((point.y) * [[UIScreen mainScreen] nativeScale] * h_ratio));
}
-(void) thumbstickMoved:(VirtualControllerAnalogType)analog
x:(CGFloat)x
y:(CGFloat)y
controllerId:(int)controllerId {
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(controllerId, [[NSNumber numberWithUnsignedInteger:analog] intValue], CGFloat(x), CGFloat(y));
}
-(void) virtualControllerButtonDown:(VirtualControllerButtonType)button
controllerId:(int)controllerId {
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadButtonEvent(controllerId, [[NSNumber numberWithUnsignedInteger:button] intValue], true);
}
-(void) virtualControllerGyro:(int)controllerId
deltaTimestamp:(int)delta_timestamp
gyroX:(float)gyro_x
gyroY:(float)gyro_y
gyroZ:(float)gyro_z
accelX:(float)accel_x
accelY:(float)accel_y
accelZ:(float)accel_z
{
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadMotionEvent(controllerId, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
-(void) virtualControllerButtonUp:(VirtualControllerButtonType)button
controllerId:(int)controllerId {
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadButtonEvent(controllerId, [[NSNumber numberWithUnsignedInteger:button] intValue], false);
}
-(void) orientationChanged:(UIInterfaceOrientation)orientation with:(CAMetalLayer *)layer size:(CGSize)size {
_layer = layer;
_size = size;
EmulationSession::GetInstance().Window().OnSurfaceChanged(layer, size);
}
-(void) settingsChanged {
//
}
@end
+86
View File
@@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
enable_language(Swift OBJCXX)
add_executable(eden-ios
AppUI-Bridging-Header.h
AppUI.swift
AppUIGameInformation.h
AppUIGameInformation.mm
AppUIObjC.h
AppUIObjC.mm
Config.h
Config.mm
EmulationSession.h
EmulationSession.mm
EmulationWindow.h
EmulationWindow.mm
VMA.cpp
EmulationGame.swift
JoystickView.swift
EmulationHandler.swift
PomeloApp.swift
FileManager.swift
EmulationView.swift
LibraryView.swift
KeyboardHostingController.swift
FolderMonitor.swift
GameButtonView.swift
EmulationScreenView.swift
GameListView.swift
# Extensions
SwiftUIJoystick.swift
)
set(MACOSX_BUNDLE_GUI_IDENTIFIER "dev.eden-emu.eden")
set(MACOSX_BUNDLE_BUNDLE_NAME "Eden")
set(MACOSX_BUNDLE_INFO_STRING "Eden: A high-performance Nintendo Switch emulator")
# TODO(crueter): Copyright, and versioning
# Keep bundle identifier as-is, for compatibility sake
set_target_properties(eden-ios PROPERTIES
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/AppUI-Bridging-Header.h"
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "eden-ios-Swift.h"
XCODE_ATTRIBUTE_DERIVED_FILE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(eden-ios PRIVATE common core input_common frontend_common video_core sirit::sirit)
target_link_libraries(eden-ios PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
target_link_libraries(eden-ios PRIVATE SDL2::SDL2 glad stb::headers)
create_target_directory_groups(eden-ios)
# FIXME(crueter): This should /all/ be in a module of some kind!
# Xcode will automatically generate the Assets.car and icns file for us.
set(_dist "${CMAKE_SOURCE_DIR}/dist")
if (CMAKE_GENERATOR MATCHES "Xcode")
set(_icons "${_dist}/eden.icon")
set_target_properties(eden-ios PROPERTIES
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME eden
MACOSX_BUNDLE_ICON_FILE eden
# Also force xcode to manage signing for us.
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED ON
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED ON
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Automatic)
# Otherwise, we'll use our own.
else()
set(_icons "${_dist}/eden.icns" "${_dist}/Assets.car")
endif()
set_source_files_properties(${_icons} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
target_sources(eden-ios PRIVATE ${_icons})
set_target_properties(eden-ios PROPERTIES MACOSX_BUNDLE TRUE)
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dylib")
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
set_source_files_properties(${MOLTENVK_LIBRARY} PROPERTIES
MACOSX_PACKAGE_LOCATION Frameworks
XCODE_FILE_ATTRIBUTES "CodeSignOnCopy")
target_sources(eden-ios PRIVATE ${MOLTENVK_LIBRARY})
+19
View File
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <common/settings_common.h>
#include "common/common_types.h"
#include "common/settings_setting.h"
#include "common/settings_enums.h"
namespace IOSSettings {
struct Values {
Settings::Linkage linkage;
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay};
};
extern Values values;
} // namespace IOSSettings
+11
View File
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "Config.h"
namespace IOSSettings {
Values values;
} // namespace IOSSettings
+31
View File
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import Foundation
struct EmulationGame : Comparable, Hashable, Identifiable {
var id = UUID()
let developer: String
let fileURL: URL
let imageData: Data
let title: String
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(developer)
hasher.combine(fileURL)
hasher.combine(imageData)
hasher.combine(title)
}
static func < (lhs: EmulationGame, rhs: EmulationGame) -> Bool {
lhs.title < rhs.title
}
static func == (lhs: EmulationGame, rhs: EmulationGame) -> Bool {
lhs.title == rhs.title
}
}
+96
View File
@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import Metal
import Foundation
class EmulationSessionViewModel: ObservableObject {
@Published var isShowingCustomButton = true
@State var should = false
var device: MTLDevice?
@State var mtkView: MTKView = MTKView()
var CaLayer: CAMetalLayer?
private var sudachiGame: EmulationGame?
private let appui = AppUI.shared
private var thread: Thread!
private var isRunning = false
var doesneedresources = false
@State var iscustom: Bool = false
init(game: EmulationGame?) {
self.device = MTLCreateSystemDefaultDevice()
self.sudachiGame = game
}
func configureAppUI(with mtkView: MTKView) {
self.mtkView = mtkView
device = self.mtkView.device
guard !isRunning else { return }
isRunning = true
appui.configure(layer: mtkView.layer as! CAMetalLayer, with: mtkView.frame.size)
iscustom = ((sudachiGame?.fileURL.startAccessingSecurityScopedResource()) != nil)
DispatchQueue.global(qos: .userInitiated).async { [self] in
if let sudachiGame = self.sudachiGame {
self.appui.insert(game: sudachiGame.fileURL)
} else {
self.appui.bootOS()
}
}
thread = .init(block: self.step)
thread.name = "Yuzu"
thread.qualityOfService = .userInteractive
thread.threadPriority = 0.9
thread.start()
}
private func step() {
while true {
appui.step()
}
}
func customButtonTapped() {
stopEmulation()
}
private func stopEmulation() {
if isRunning {
isRunning = false
appui.exit()
thread.cancel()
if iscustom {
sudachiGame?.fileURL.stopAccessingSecurityScopedResource()
}
}
}
func handleOrientationChange() {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let interfaceOrientation = self.getInterfaceOrientation(from: UIDevice.current.orientation)
self.appui.orientationChanged(orientation: interfaceOrientation, with: self.mtkView.layer as! CAMetalLayer, size: mtkView.frame.size)
}
}
private func getInterfaceOrientation(from deviceOrientation: UIDeviceOrientation) -> UIInterfaceOrientation {
switch deviceOrientation {
case .portrait:
return .portrait
case .portraitUpsideDown:
return .portraitUpsideDown
case .landscapeLeft:
return .landscapeRight
case .landscapeRight:
return .landscapeLeft
default:
return .unknown
}
}
}
+133
View File
@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import MetalKit
class EmulationScreenView: UIView {
var primaryScreen: UIView!
var portraitconstraints = [NSLayoutConstraint]()
var landscapeconstraints = [NSLayoutConstraint]()
var fullscreenconstraints = [NSLayoutConstraint]()
let appui = AppUI.shared
let userDefaults = UserDefaults.standard
override init(frame: CGRect) {
super.init(frame: frame)
if UIDevice.current.userInterfaceIdiom == .pad {
setupAppUIScreenforiPad()
} else {
setupAppUIScreen()
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
if UIDevice.current.userInterfaceIdiom == .pad {
setupAppUIScreenforiPad()
} else {
setupAppUIScreen()
}
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
guard let touch = touches.first else {
return
}
print("Location: \(touch.location(in: primaryScreen))")
appui.touchBegan(at: touch.location(in: primaryScreen), for: 0)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
print("Touch Ended")
appui.touchEnded(for: 0)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
guard let touch = touches.first else {
return
}
let location = touch.location(in: primaryScreen)
print("Location Moved: \(location)")
appui.touchMoved(at: location, for: 0)
}
func setupAppUIScreenforiPad() {
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
primaryScreen.translatesAutoresizingMaskIntoConstraints = false
primaryScreen.clipsToBounds = true
primaryScreen.layer.borderColor = UIColor.secondarySystemBackground.cgColor
primaryScreen.layer.borderWidth = 3
primaryScreen.layer.cornerCurve = .continuous
primaryScreen.layer.cornerRadius = 10
addSubview(primaryScreen)
portraitconstraints = [
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
primaryScreen.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10),
primaryScreen.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10),
primaryScreen.heightAnchor.constraint(equalTo: primaryScreen.widthAnchor, multiplier: 9 / 16),
]
landscapeconstraints = [
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 50),
primaryScreen.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -100),
primaryScreen.widthAnchor.constraint(equalTo: primaryScreen.heightAnchor, multiplier: 16 / 9),
primaryScreen.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
]
updateConstraintsForOrientation()
}
func setupAppUIScreen() {
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
primaryScreen.translatesAutoresizingMaskIntoConstraints = false
primaryScreen.clipsToBounds = true
primaryScreen.layer.borderColor = UIColor.secondarySystemBackground.cgColor
primaryScreen.layer.borderWidth = 3
primaryScreen.layer.cornerCurve = .continuous
primaryScreen.layer.cornerRadius = 10
addSubview(primaryScreen)
portraitconstraints = [
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
primaryScreen.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10),
primaryScreen.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10),
primaryScreen.heightAnchor.constraint(equalTo: primaryScreen.widthAnchor, multiplier: 9 / 16),
]
landscapeconstraints = [
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
primaryScreen.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -10),
primaryScreen.widthAnchor.constraint(equalTo: primaryScreen.heightAnchor, multiplier: 16 / 9),
primaryScreen.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
]
updateConstraintsForOrientation()
}
override func layoutSubviews() {
super.layoutSubviews()
updateConstraintsForOrientation()
}
private func updateConstraintsForOrientation() {
removeConstraints(portraitconstraints)
removeConstraints(landscapeconstraints)
let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait
addConstraints(isPortrait ? portraitconstraints : landscapeconstraints)
}
}
+103
View File
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CAMetalLayer.h>
#if __has_include(<Metal/Metal.hpp>)
#import <Metal/Metal.hpp>
#else
#import <Metal/Metal.h>
#endif
#import "EmulationWindow.h"
#include "common/detached_tasks.h"
#include "core/core.h"
#include "core/file_sys/registered_cache.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/perf_stats.h"
#include "frontend_common/content_manager.h"
#include "video_core/rasterizer_interface.h"
class EmulationSession final {
public:
explicit EmulationSession();
~EmulationSession() = default;
static EmulationSession& GetInstance();
const Core::System& System() const;
Core::System& System();
FileSys::ManualContentProvider* GetContentProvider();
InputCommon::InputSubsystem& GetInputSubsystem();
const EmulationWindow& Window() const;
EmulationWindow& Window();
CAMetalLayer* NativeWindow() const;
void SetNativeWindow(CAMetalLayer* native_window, CGSize size);
void SurfaceChanged();
void InitializeGpuDriver();
bool IsRunning() const;
bool IsPaused() const;
void PauseEmulation();
void UnPauseEmulation();
void HaltEmulation();
void RunEmulation();
void ShutdownEmulation();
const Core::PerfStatsResults& PerfStats();
void ConfigureFilesystemProvider(const std::string& filepath);
void InitializeSystem(bool reload);
void SetAppletId(int applet_id);
Core::SystemResultStatus InitializeEmulation(const std::string& filepath,
const std::size_t program_index,
const bool frontend_initiated);
Core::SystemResultStatus BootOS();
static void OnEmulationStarted();
static u64 GetProgramId(std::string programId);
bool IsInitialized() { return is_initialized; };
bool IsHandheldOnly();
void SetDeviceType([[maybe_unused]] int index, int type);
void OnGamepadConnectEvent([[maybe_unused]] int index);
void OnGamepadDisconnectEvent([[maybe_unused]] int index);
private:
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
static void OnEmulationStopped(Core::SystemResultStatus result);
static void ChangeProgram(std::size_t program_index);
private:
// Window management
std::unique_ptr<EmulationWindow> m_window;
CAMetalLayer* m_native_window{};
// Core emulation
Core::System m_system;
InputCommon::InputSubsystem m_input_subsystem;
Common::DetachedTasks m_detached_tasks;
Core::PerfStatsResults m_perf_stats{};
std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
std::atomic<bool> m_is_running = false;
std::atomic<bool> m_is_paused = false;
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
int m_applet_id{1};
// GPU driver parameters
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
// Synchronization
std::condition_variable_any m_cv;
mutable std::mutex m_mutex;
bool is_initialized = false;
CGSize m_size;
// Program index for next boot
std::atomic<s32> m_next_program_index = -1;
};
+474
View File
@@ -0,0 +1,474 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#import "EmulationSession.h"
#include <SDL.h>
#include <codecvt>
#include <locale>
#include <string>
#include <string_view>
#include <dlfcn.h>
#include "common/fs/fs.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/savedata_factory.h"
#include "core/loader/nro.h"
#include "frontend_common/content_manager.h"
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
#include "common/fs/path_util.h"
#include "common/logging.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h"
#include "core/frontend/applets/general.h"
#include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#include "frontend_common/config.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/hid_types.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#define jconst [[maybe_unused]] const auto
#define jauto [[maybe_unused]] auto
static EmulationSession s_instance;
EmulationSession::EmulationSession() {
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
}
EmulationSession& EmulationSession::GetInstance() {
return s_instance;
}
const Core::System& EmulationSession::System() const {
return m_system;
}
Core::System& EmulationSession::System() {
return m_system;
}
FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
return m_manual_provider.get();
}
InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
return m_input_subsystem;
}
const EmulationWindow& EmulationSession::Window() const {
return *m_window;
}
EmulationWindow& EmulationSession::Window() {
return *m_window;
}
CAMetalLayer* EmulationSession::NativeWindow() const {
return m_native_window;
}
void EmulationSession::SetNativeWindow(CAMetalLayer* native_window, CGSize size) {
m_native_window = native_window;
m_size = size;
}
void EmulationSession::InitializeGpuDriver() {
m_vulkan_library = std::make_shared<Common::DynamicLibrary>(dlopen("@executable_path/Frameworks/MoltenVK", RTLD_NOW));
}
bool EmulationSession::IsRunning() const {
return m_is_running;
}
bool EmulationSession::IsPaused() const {
return m_is_running && m_is_paused;
}
const Core::PerfStatsResults& EmulationSession::PerfStats() {
m_perf_stats = m_system.GetAndResetPerfStats();
return m_perf_stats;
}
void EmulationSession::SurfaceChanged() {
if (!IsRunning()) {
return;
}
m_window->OnSurfaceChanged(m_native_window, m_size);
}
void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) {
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::OpenMode::Read);
if (!file) {
return;
}
auto loader = Loader::GetLoader(m_system, file);
if (!loader) {
return;
}
const auto file_type = loader->GetFileType();
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
return;
}
u64 program_id = 0;
const auto res2 = loader->ReadProgramId(program_id);
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
m_manual_provider->AddEntry(FileSys::TitleType::Application,
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
program_id, file);
} else if (res2 == Loader::ResultStatus::Success &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
const auto nsp = file_type == Loader::FileType::NSP
? std::make_shared<FileSys::NSP>(file)
: FileSys::XCI{file}.GetSecurePartitionNSP();
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
entry.second->GetBaseFile());
}
}
}
}
void EmulationSession::InitializeSystem(bool reload) {
if (!reload) {
SDL_SetMainReady();
// Initialize logging system
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
Common::Log::Start();
}
// Initialize filesystem.
m_system.SetFilesystem(m_vfs);
m_system.GetUserChannel().clear();
m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
m_manual_provider.get());
m_system.GetFileSystemController().CreateFactories(*m_vfs);
is_initialized = true;
}
void EmulationSession::SetAppletId(int applet_id) {
m_applet_id = applet_id;
m_system.GetFrontendAppletHolder().SetCurrentAppletId(
static_cast<Service::AM::AppletId>(m_applet_id));
}
Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath,
const std::size_t program_index,
const bool frontend_initiated) {
std::scoped_lock lock(m_mutex);
// Create the render window.
m_window = std::make_unique<EmulationWindow>(&m_input_subsystem, m_native_window, m_size, m_vulkan_library);
// Initialize system.
m_system.SetShuttingDown(false);
m_system.ApplySettings();
Settings::LogSettings();
m_system.HIDCore().ReloadInputDevices();
m_system.SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet{});
// Initialize filesystem.
ConfigureFilesystemProvider(filepath);
// Load the ROM.
Service::AM::FrontendAppletParameters params{
.applet_id = static_cast<Service::AM::AppletId>(m_applet_id),
.launch_type = frontend_initiated ? Service::AM::LaunchType::FrontendInitiated
: Service::AM::LaunchType::ApplicationInitiated,
.program_index = static_cast<s32>(program_index),
};
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params);
if (m_load_result != Core::SystemResultStatus::Success) {
return m_load_result;
}
// Complete initialization.
m_system.GPU().Start();
m_system.GetCpuManager().OnGpuReady();
m_system.RegisterExitCallback([&] { HaltEmulation(); });
if (Settings::values.use_disk_shader_cache.GetValue()) {
m_system.Renderer().ReadRasterizer()->LoadDiskResources(
m_system.GetApplicationProcessProgramID(), std::stop_token{},
[](VideoCore::LoadCallbackStage, size_t value, size_t total) {});
}
// Register an ExecuteProgram callback such that Core can execute a sub-program
m_system.RegisterExecuteProgramCallback([&](std::size_t program_index_) {
m_next_program_index = program_index_;
EmulationSession::GetInstance().HaltEmulation();
ChangeProgram(m_next_program_index);
});
OnEmulationStarted();
return Core::SystemResultStatus::Success;
}
Core::SystemResultStatus EmulationSession::BootOS() {
std::scoped_lock lock(m_mutex);
// Create the render window.
m_window = std::make_unique<EmulationWindow>(&m_input_subsystem, m_native_window, m_size, m_vulkan_library);
// Initialize system.
m_system.SetShuttingDown(false);
m_system.ApplySettings();
Settings::LogSettings();
m_system.HIDCore().ReloadInputDevices();
m_system.SetFrontendAppletSet(Service::AM::Frontend::FrontendAppletSet{});
constexpr u64 QLaunchId = static_cast<u64>(Service::AM::AppletProgramId::QLaunch);
auto bis_system = m_system.GetFileSystemController().GetSystemNANDContents();
auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
m_system.GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::QLaunch);
const auto filename = qlaunch_applet_nca->GetFullPath();
auto params = Service::AM::FrontendAppletParameters {
.program_id = QLaunchId,
.applet_id = Service::AM::AppletId::QLaunch,
.applet_type = Service::AM::AppletType::LibraryApplet
};
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filename, params);
if (m_load_result != Core::SystemResultStatus::Success) {
return m_load_result;
}
// Complete initialization.
m_system.GPU().Start();
m_system.GetCpuManager().OnGpuReady();
m_system.RegisterExitCallback([&] { HaltEmulation(); });
if (Settings::values.use_disk_shader_cache.GetValue()) {
m_system.Renderer().ReadRasterizer()->LoadDiskResources(
m_system.GetApplicationProcessProgramID(), std::stop_token{},
[](VideoCore::LoadCallbackStage, size_t value, size_t total) {});
}
// Register an ExecuteProgram callback such that Core can execute a sub-program
m_system.RegisterExecuteProgramCallback([&](std::size_t program_index_) {
m_next_program_index = program_index_;
EmulationSession::GetInstance().HaltEmulation();
});
OnEmulationStarted();
return Core::SystemResultStatus::Success;
}
void EmulationSession::ShutdownEmulation() {
std::scoped_lock lock(m_mutex);
if (m_next_program_index != -1) {
ChangeProgram(m_next_program_index);
m_next_program_index = -1;
}
m_is_running = false;
// Unload user input.
m_system.HIDCore().UnloadInputDevices();
// Enable all controllers
m_system.HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All});
// Shutdown the main emulated process
if (m_load_result == Core::SystemResultStatus::Success) {
m_system.DetachDebugger();
m_system.ShutdownMainProcess();
m_detached_tasks.WaitForAllTasks();
m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
m_window.reset();
OnEmulationStopped(Core::SystemResultStatus::Success);
return;
}
// Tear down the render window.
m_window.reset();
}
void EmulationSession::PauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Pause();
m_is_paused = true;
}
void EmulationSession::UnPauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Run();
m_is_paused = false;
}
void EmulationSession::HaltEmulation() {
std::scoped_lock lock(m_mutex);
m_is_running = false;
m_cv.notify_one();
}
void EmulationSession::RunEmulation() {
{
std::scoped_lock lock(m_mutex);
m_is_running = true;
}
// Load the disk shader cache.
if (Settings::values.use_disk_shader_cache.GetValue()) {
LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
m_system.Renderer().ReadRasterizer()->LoadDiskResources(
m_system.GetApplicationProcessProgramID(), std::stop_token{}, LoadDiskCacheProgress);
LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
}
void(m_system.Run());
if (m_system.DebuggerEnabled()) {
m_system.InitializeDebugger();
}
while (true) {
{
[[maybe_unused]] std::unique_lock lock(m_mutex);
if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
[&]() { return !m_is_running; })) {
// Emulation halted.
break;
}
}
}
// Reset current applet ID.
m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
}
void EmulationSession::LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress,
int max) {
}
void EmulationSession::OnEmulationStarted() {
}
void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
}
void EmulationSession::ChangeProgram(std::size_t program_index) {
LOG_INFO(Frontend, "Trying To Switch Program");
// Halt current emulation session
EmulationSession::GetInstance().HaltEmulation();
// Save the current state if necessary
// Shutdown the current emulation session cleanly
// Update the program index
EmulationSession::GetInstance().m_next_program_index = program_index;
// Initialize the new program
// Start the new emulation session
EmulationSession::GetInstance().RunEmulation();
}
u64 EmulationSession::GetProgramId(std::string programId) {
try {
return std::stoull(programId);
} catch (...) {
return 0;
}
}
bool EmulationSession::IsHandheldOnly() {
jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
if (npad_style_set.fullkey == 1) {
return false;
}
if (npad_style_set.handheld == 0) {
return false;
}
return !Settings::IsDockedMode();
}
void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
}
void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
// Ensure that player1 is configured correctly and handheld disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
handheld->Disconnect();
}
}
// Ensure that handheld is configured correctly and player 1 disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
player1->Disconnect();
}
}
if (!controller->IsConnected()) {
controller->Connect();
}
}
void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->Disconnect();
}
+558
View File
@@ -0,0 +1,558 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import Foundation
import GameController
import UIKit
import Metal
struct MetalView: UIViewRepresentable {
let device: MTLDevice?
let configure: (UIView) -> Void
func makeUIView(context: Context) -> EmulationScreenView {
let view = EmulationScreenView()
configure(view.primaryScreen)
return view
}
func updateUIView(_ uiView: EmulationScreenView, context: Context) {
//
}
}
struct ControllerView: View {
let appui = AppUI.shared
@State var isPressed = false
@State var controllerconnected = false
@State private var x: CGFloat = 0.0
@State private var y: CGFloat = 0.0
@Environment(\.presentationMode) var presentationMode
var body: some View {
GeometryReader { geometry in
ZStack {
if !controllerconnected {
OnScreenController(geometry: geometry) // i did this to clean it up as it was quite long lmfao
}
}
}
.onAppear {
print("checking for controller:")
controllerconnected = false
DispatchQueue.main.async {
setupControllers() // i dont know what half of this shit does
}
}
}
// Add a dictionary to track controller IDs
@State var controllerIDs: [GCController: Int] = [:]
private func setupControllers() {
NotificationCenter.default.addObserver(forName: .GCControllerDidConnect, object: nil, queue: .main) { notification in
if let controller = notification.object as? GCController {
print("wow controller onstart") // yippeeee
self.setupController(controller)
self.controllerconnected = true
} else {
print("not GCController :((((((") // wahhhhhhh
}
}
NotificationCenter.default.addObserver(forName: .GCControllerDidDisconnect, object: nil, queue: .main) { notification in
if let controller = notification.object as? GCController {
print("wow controller gone")
if self.controllerIDs.isEmpty {
controllerconnected = false
}
self.controllerIDs.removeValue(forKey: controller) // Remove the controller ID
}
}
GCController.controllers().forEach { controller in
print("wow controller")
self.controllerconnected = true
self.setupController(controller)
}
}
private func setupController(_ controller: GCController) {
// Assign a unique ID to the controller, max 5 controllers
if controllerIDs.count < 6, controllerIDs[controller] == nil {
controllerIDs[controller] = controllerIDs.count
}
guard let controllerId = controllerIDs[controller] else { return }
if let extendedGamepad = controller.extendedGamepad {
// Handle extended gamepad
extendedGamepad.dpad.up.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadUp, controllerId: controllerId) : self.touchUpInside(.directionalPadUp, controllerId: controllerId)
}
extendedGamepad.dpad.down.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadDown, controllerId: controllerId) : self.touchUpInside(.directionalPadDown, controllerId: controllerId)
}
extendedGamepad.dpad.left.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadLeft, controllerId: controllerId) : self.touchUpInside(.directionalPadLeft, controllerId: controllerId)
}
extendedGamepad.dpad.right.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadRight, controllerId: controllerId) : self.touchUpInside(.directionalPadRight, controllerId: controllerId)
}
extendedGamepad.buttonOptions?.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.minus, controllerId: controllerId) : self.touchUpInside(.minus, controllerId: controllerId)
}
extendedGamepad.buttonMenu.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.plus, controllerId: controllerId) : self.touchUpInside(.plus, controllerId: controllerId)
}
extendedGamepad.buttonA.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.A, controllerId: controllerId) : self.touchUpInside(.A, controllerId: controllerId)
}
extendedGamepad.buttonB.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.B, controllerId: controllerId) : self.touchUpInside(.B, controllerId: controllerId)
}
extendedGamepad.buttonX.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.X, controllerId: controllerId) : self.touchUpInside(.X, controllerId: controllerId)
}
extendedGamepad.buttonY.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.Y, controllerId: controllerId) : self.touchUpInside(.Y, controllerId: controllerId)
}
extendedGamepad.leftShoulder.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.triggerL, controllerId: controllerId) : self.touchUpInside(.L, controllerId: controllerId)
}
extendedGamepad.leftTrigger.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.triggerZL, controllerId: controllerId) : self.touchUpInside(.triggerZL, controllerId: controllerId)
}
extendedGamepad.rightShoulder.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.triggerR, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId)
}
extendedGamepad.leftThumbstickButton?.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.L, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId)
}
extendedGamepad.rightThumbstickButton?.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.R, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId)
}
extendedGamepad.rightTrigger.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.triggerZR, controllerId: controllerId) : self.touchUpInside(.triggerZR, controllerId: controllerId)
}
extendedGamepad.buttonHome?.pressedChangedHandler = { button, value, pressed in
if pressed {
appui.exit()
presentationMode.wrappedValue.dismiss()
}
}
extendedGamepad.leftThumbstick.valueChangedHandler = { dpad, x, y in
self.appui.thumbstickMoved(analog: .left, x: x, y: y, controllerid: controllerId)
}
extendedGamepad.rightThumbstick.valueChangedHandler = { dpad, x, y in
self.appui.thumbstickMoved(analog: .right, x: x, y: y, controllerid: controllerId)
}
if let motion = controller.motion {
var lastTimestamp = Date().timeIntervalSince1970 // Initialize timestamp when motion starts
motion.valueChangedHandler = { motion in
// Get current time
let currentTimestamp = Date().timeIntervalSince1970
// Update last timestamp
lastTimestamp = currentTimestamp
// Get gyroscope data
let gyroX = motion.rotationRate.x
let gyroY = motion.rotationRate.y
let gyroZ = motion.rotationRate.z
// Get accelerometer data
let accelX = motion.gravity.x + motion.userAcceleration.x
let accelY = motion.gravity.y + motion.userAcceleration.y
let accelZ = motion.gravity.z + motion.userAcceleration.z
print("\(gyroX), \(gyroY), \(gyroZ), \(accelX), \(accelY), \(accelZ)")
// Call your gyroMoved function with the motion data
appui.gyroMoved(x: Float(gyroX), y: Float(gyroY), z: Float(gyroZ), accelX: Float(accelX), accelY: Float(accelY), accelZ: Float(accelZ), controllerId: Int32(controllerId), deltaTimestamp: Int32(lastTimestamp))
}
}
} else if let microGamepad = controller.microGamepad {
// Handle micro gamepad
microGamepad.dpad.up.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadUp, controllerId: controllerId) : self.touchUpInside(.directionalPadUp, controllerId: controllerId)
}
microGamepad.dpad.down.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadDown, controllerId: controllerId) : self.touchUpInside(.directionalPadDown, controllerId: controllerId)
}
microGamepad.dpad.left.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadLeft, controllerId: controllerId) : self.touchUpInside(.directionalPadLeft, controllerId: controllerId)
}
microGamepad.dpad.right.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.directionalPadRight, controllerId: controllerId) : self.touchUpInside(.directionalPadRight, controllerId: controllerId)
}
microGamepad.buttonA.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.A, controllerId: controllerId) : self.touchUpInside(.A, controllerId: controllerId)
}
microGamepad.buttonX.pressedChangedHandler = { button, value, pressed in
pressed ? self.touchDown(.X, controllerId: controllerId) : self.touchUpInside(.X, controllerId: controllerId)
}
}
}
private func touchDown(_ button: VirtualControllerButtonType, controllerId: Int) {
appui.virtualControllerButtonDown(button: button, controllerid: controllerId) }
private func touchUpInside(_ button: VirtualControllerButtonType, controllerId: Int) {
appui.virtualControllerButtonUp(button: button, controllerid: controllerId)
}
}
struct OnScreenController: View {
@State var geometry: GeometryProxy
var body: some View {
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
// portrait
VStack {
Spacer()
VStack {
HStack {
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
.padding()
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this works
ABXYView()
}
}
.padding()
}
HStack {
ButtonView(button: .plus).padding(.horizontal, 40)
ButtonView(button: .minus).padding(.horizontal, 40)
}
}
.padding(.bottom, geometry.size.height / 3.2) // very broken
}
} else {
// could be landscape
VStack {
HStack {
Spacer()
ButtonView(button: .home)
.padding(.horizontal)
}
Spacer()
VStack {
HStack {
// gotta fuckin add + and - now
VStack {
ShoulderButtonsViewLeft()
ZStack {
Joystick()
DPadView()
}
}
HStack {
Spacer()
VStack {
Spacer()
ButtonView(button: .plus) // Adding the + button
}
VStack {
Spacer()
ButtonView(button: .minus) // Adding the - button
}
Spacer()
}
VStack {
ShoulderButtonsViewRight()
ZStack {
Joystick(iscool: true) // hope this work s
ABXYView()
}
}
}
}
.padding(.bottom, geometry.size.height / 11) // also extremally broken (
}
}
}
}
struct ShoulderButtonsViewLeft: View {
var body: some View {
HStack {
ButtonView(button: .triggerZL).padding(.horizontal)
ButtonView(button: .triggerL).padding(.horizontal)
}
.frame(width: 160, height: 20)
}
}
struct ShoulderButtonsViewRight: View {
var body: some View {
HStack {
ButtonView(button: .triggerR).padding(.horizontal)
ButtonView(button: .triggerZR).padding(.horizontal)
}
.frame(width: 160, height: 20)
}
}
struct DPadView: View {
var body: some View {
VStack {
ButtonView(button: .directionalPadUp)
HStack {
ButtonView(button: .directionalPadLeft)
Spacer(minLength: 20)
ButtonView(button: .directionalPadRight)
}
ButtonView(button: .directionalPadDown).padding(.horizontal)
}
.frame(width: 145, height: 145)
}
}
struct ABXYView: View {
var body: some View {
VStack {
ButtonView(button: .X)
HStack {
ButtonView(button: .Y)
Spacer(minLength: 20)
ButtonView(button: .A)
}
ButtonView(button: .B).padding(.horizontal)
}
.frame(width: 145, height: 145)
}
}
class Haptics {
static let shared = Haptics()
private init() { }
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
print("haptics")
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
}
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) {
UINotificationFeedbackGenerator().notificationOccurred(feedbackType)
}
}
struct ButtonView: View {
var button: VirtualControllerButtonType
@StateObject private var viewModel: EmulationViewModel = EmulationViewModel()
let appui = AppUI.shared
@State var mtkView: MTKView?
@State var width: CGFloat = 45
@State var height: CGFloat = 45
@State var isPressed = false
var id: Int {
if onscreenjoy {
return 8
}
return 0
}
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
var body: some View {
Image(systemName: buttonText)
.resizable()
.frame(width: width, height: height)
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
.opacity(isPressed ? 0.5 : 1)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
if !self.isPressed {
self.isPressed = true
DispatchQueue.main.async {
if button == .home {
presentationMode.wrappedValue.dismiss()
appui.exit()
} else {
appui.virtualControllerButtonDown(button: button, controllerid: id)
Haptics.shared.play(.heavy)
}
}
}
}
.onEnded { _ in
self.isPressed = false
DispatchQueue.main.async {
if button != .home {
appui.virtualControllerButtonUp(button: button, controllerid: id)
}
}
}
)
.onAppear() {
if button == .triggerL || button == .triggerZL || button == .triggerZR || button == .triggerR {
width = 65
}
if button == .minus || button == .plus || button == .home {
width = 35
height = 35
}
}
}
private var buttonText: String {
switch button {
case .A: return "a.circle.fill"
case .B: return "b.circle.fill"
case .X: return "x.circle.fill"
case .Y: return "y.circle.fill"
case .directionalPadUp: return "arrowtriangle.up.circle.fill"
case .directionalPadDown: return "arrowtriangle.down.circle.fill"
case .directionalPadLeft: return "arrowtriangle.left.circle.fill"
case .directionalPadRight: return "arrowtriangle.right.circle.fill"
case .triggerZL: return"zl.rectangle.roundedtop.fill"
case .triggerZR: return "zr.rectangle.roundedtop.fill"
case .triggerL: return "l.rectangle.roundedbottom.fill"
case .triggerR: return "r.rectangle.roundedbottom.fill"
case .plus: return "plus.circle.fill"
case .minus: return "minus.circle.fill"
case .home: return "house.circle.fill"
default: return ""
}
}
}
struct EmulationView: View {
@StateObject private var viewModel: EmulationViewModel
@State var controllerconnected = false
@State var appui = AppUI.shared
var device: MTLDevice? = MTLCreateSystemDefaultDevice()
@State var CaLayer: CAMetalLayer?
@State var ShowPopup: Bool = false
@State var mtkview: MTKView?
@State private var thread: Thread!
@State var uiTabBarController: UITabBarController?
@State private var isFirstFrameShown = false
@State private var timer: Timer?
@Environment(\.scenePhase) var scenePhase
init(game: EmulationGame?) {
_viewModel = StateObject(wrappedValue: EmulationViewModel())
}
var body: some View {
ZStack {
MetalView(device: device) { view in
DispatchQueue.main.async {
if let metalView = view as? MTKView {
mtkview = metalView
viewModel.configureAppUI(with: metalView)
} else {
print("Error: view is not of type MTKView")
}
}
}
.onRotate { size in
viewModel.handleOrientationChange()
}
ControllerView()
}
.overlay(
// Loading screen overlay on top of MetalView
Group {
if !isFirstFrameShown {
LoadingView()
}
}
.transition(.opacity)
)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = true
startPollingFirstFrameShowed()
}
.onDisappear {
stopPollingFirstFrameShowed()
uiTabBarController?.tabBar.isHidden = false
viewModel.customButtonTapped()
}
.navigationBarBackButtonHidden(true)
// .introspect(.tabView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { (tabBarController) in
// tabBarController.tabBar.isHidden = true
// uiTabBarController = tabBarController
// }
}
private func startPollingFirstFrameShowed() {
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
if appui.FirstFrameShowed() {
withAnimation {
isFirstFrameShown = true
}
stopPollingFirstFrameShowed()
}
}
}
private func stopPollingFirstFrameShowed() {
timer?.invalidate()
timer = nil
print("Timer Invalidated")
}
}
struct LoadingView: View {
var body: some View {
VStack {
ProgressView("Loading...")
// .font(.system(size: 90))
.progressViewStyle(CircularProgressViewStyle())
.padding()
Text("Please wait while the game loads.")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.8))
.foregroundColor(.white)
}
}
extension View {
func onRotate(perform action: @escaping (CGSize) -> Void) -> some View {
self.modifier(DeviceRotationModifier(action: action))
}
}
struct DeviceRotationModifier: ViewModifier {
let action: (CGSize) -> Void
@State var startedfirst: Bool = false
func body(content: Content) -> some View { content
.background(GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
})
.onPreferenceChange(SizePreferenceKey.self) { newSize in
if startedfirst {
action(newSize)
} else {
startedfirst = true
}
}
}
}
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
+86
View File
@@ -0,0 +1,86 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#if __has_include(<Metal/Metal.hpp>)
#import <Metal/Metal.hpp>
#else
#import <Metal/Metal.h>
#endif
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <QuartzCore/CAMetalLayer.h>
#include <memory>
#include <span>
#include "core/frontend/emu_window.h"
#include "core/frontend/graphics_context.h"
#include "input_common/main.h"
class GraphicsContext_Apple final : public Core::Frontend::GraphicsContext {
public:
explicit GraphicsContext_Apple(std::shared_ptr<Common::DynamicLibrary> driver_library)
: m_driver_library{driver_library} {}
~GraphicsContext_Apple() = default;
std::shared_ptr<Common::DynamicLibrary> GetDriverLibrary() override {
return m_driver_library;
}
private:
std::shared_ptr<Common::DynamicLibrary> m_driver_library;
};
NS_ASSUME_NONNULL_BEGIN
class EmulationWindow final : public Core::Frontend::EmuWindow {
public:
EmulationWindow(InputCommon::InputSubsystem* input_subsystem, CAMetalLayer* surface, CGSize size,
std::shared_ptr<Common::DynamicLibrary> driver_library);
~EmulationWindow() = default;
void OnSurfaceChanged(CAMetalLayer* surface, CGSize size);
void OrientationChanged(UIInterfaceOrientation orientation);
void OnFrameDisplayed() override;
void OnTouchPressed(int id, float x, float y);
void OnTouchMoved(int id, float x, float y);
void OnTouchReleased(int id);
void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
return {std::make_unique<GraphicsContext_Apple>(m_driver_library)};
}
bool HasFirstFrame() const {
return m_first_frame;
}
bool IsShown() const override {
return true;
};
private:
float m_window_width{};
float m_window_height{};
CGSize m_size;
bool is_portrait = true;
InputCommon::InputSubsystem* m_input_subsystem{};
std::shared_ptr<Common::DynamicLibrary> m_driver_library;
bool m_first_frame = false;
};
NS_ASSUME_NONNULL_END
+82
View File
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
// SPDX-License-Identifier: GPL-2.0-or-later
#import "EmulationWindow.h"
#import "EmulationSession.h"
#include <SDL.h>
#include "common/logging.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h"
#include "input_common/main.h"
void EmulationWindow::OnSurfaceChanged(CAMetalLayer* surface, CGSize size) {
m_size = size;
m_window_width = size.width;
m_window_height = size.height;
// Ensures that we emulate with the correct aspect ratio.
// UpdateCurrentFramebufferLayout(m_window_width, m_window_height);
window_info.render_surface = (__bridge void *)surface;
window_info.render_surface_scale = [[UIScreen mainScreen] nativeScale];
}
void EmulationWindow::OrientationChanged(UIInterfaceOrientation orientation) {
is_portrait = orientation == UIInterfaceOrientationPortrait;
}
void EmulationWindow::OnTouchPressed(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(touch_x,
touch_y, id);
}
void EmulationWindow::OnTouchMoved(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(touch_x,
touch_y, id);
}
void EmulationWindow::OnTouchReleased(int id) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id);
}
void EmulationWindow::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
}
void EmulationWindow::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
}
void EmulationWindow::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z) {
m_input_subsystem->GetVirtualGamepad()->SetMotionState(player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
void EmulationWindow::OnFrameDisplayed() {
if (!m_first_frame) {
m_first_frame = true;
}
}
EmulationWindow::EmulationWindow(InputCommon::InputSubsystem* input_subsystem, CAMetalLayer* surface, CGSize size, std::shared_ptr<Common::DynamicLibrary> driver_library)
: m_window_width{}, m_window_height{}, m_size{size}, is_portrait{true}, m_input_subsystem{input_subsystem}, m_driver_library{driver_library}, m_first_frame{false} {
LOG_INFO(Frontend, "initializing");
if (!surface) {
LOG_CRITICAL(Frontend, "surface is nullptr");
return;
}
OnSurfaceChanged(surface, m_size);
window_info.render_surface_scale = [[UIScreen mainScreen] nativeScale];
window_info.type = Core::Frontend::WindowSystemType::Cocoa;
m_input_subsystem->Initialize();
}
+197
View File
@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import Foundation
import UIKit
struct Core : Comparable, Hashable {
let name = "Yuzu"
var games: [EmulationGame]
let root: URL
static func < (lhs: Core, rhs: Core) -> Bool {
lhs.name < rhs.name
}
func AddFirmware(at fileURL: URL) {
do {
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let destinationURL = documentsDirectory.appendingPathComponent("nand/system/Contents/registered")
if !fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
}
//try Zip.unzipFile(fileURL, destination: destinationURL, overwrite: true, password: nil)
print("File unzipped successfully to \(destinationURL.path)")
} catch {
print("Failed to unzip file: \(error)")
}
}
}
enum LibManError : Error {
case ripenum, urlgobyebye
}
class LibraryManager {
static let shared = LibraryManager()
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("roms", conformingTo: .folder)
func removerom(_ game: EmulationGame) throws {
do {
try FileManager.default.removeItem(at: game.fileURL)
} catch {
throw error
}
}
func homebrewroms() -> [EmulationGame] {
// TODO(lizzie): this is horrible
var urls: [URL] = []
let sdmc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sdmc", conformingTo: .folder)
let sdfolder = sdmc.appendingPathComponent("switch", conformingTo: .folder)
if FileManager.default.fileExists(atPath: sdfolder.path) {
if let dirContents = FileManager.default.enumerator(at: sdmc, includingPropertiesForKeys: nil, options: []) {
do {
try dirContents.forEach() { files in
if let file = files as? URL {
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
if let isfile = getaboutfile.isRegularFile, isfile {
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
urls.append(file)
}
}
}
}
} catch {
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) {
do {
try dirContents.forEach() { files in
if let file = files as? URL {
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
if let isfile = getaboutfile.isRegularFile, isfile {
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
urls.append(file)
}
}
}
}
} catch {
print("damn")
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) {
do {
try dirContents.forEach() { files in
if let file = files as? URL {
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
if let isfile = getaboutfile.isRegularFile, isfile {
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
urls.append(file)
}
}
}
}
} catch {
return []
}
} else {
return []
}
}
}
}
}
}
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) {
do {
try dirContents.forEach() { files in
if let file = files as? URL {
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
if let isfile = getaboutfile.isRegularFile, isfile {
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
urls.append(file)
}
}
}
}
} catch {
return []
}
} else {
return []
}
func games(from urls: [URL]) -> [EmulationGame] {
var pomelogames: [EmulationGame] = []
pomelogames = urls.reduce(into: [EmulationGame]()) { partialResult, element in
let iscustom = element.startAccessingSecurityScopedResource()
let information = AppUI.shared.information(for: element)
let game = EmulationGame(developer: information.developer, fileURL: element, imageData: information.iconData, title: information.title)
if iscustom {
element.stopAccessingSecurityScopedResource()
}
partialResult.append(game)
}
return pomelogames
}
return games(from: urls)
}
func library() throws -> Core {
func getromsfromdir() throws -> [URL] {
guard let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) else {
print("uhoh how unfortunate for some reason FileManager.default.enumerator aint workin")
throw LibManError.ripenum
}
let appui = AppUI.shared
var urls: [URL] = []
try dirContents.forEach() { files in
if let file = files as? URL {
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
if let isfile = getaboutfile.isRegularFile, isfile {
if ["nca", "nro", "nsp", "nso", "xci"].contains(file.pathExtension.lowercased()) {
urls.append(file)
}
}
}
}
let sdmc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sdmc", conformingTo: .folder)
let sdfolder = sdmc.appendingPathComponent("switch", conformingTo: .folder)
if FileManager.default.fileExists(atPath: sdfolder.path) {
if let dirContents = FileManager.default.enumerator(at: sdmc, includingPropertiesForKeys: nil, options: []) {
try dirContents.forEach() { files in
if let file = files as? URL {
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
if let isfile = getaboutfile.isRegularFile, isfile {
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
urls.append(file)
}
}
}
}
}
}
appui.insert(games: urls)
return urls
}
func games(from urls: [URL], core: inout Core) {
core.games = urls.reduce(into: [EmulationGame]()) { partialResult, element in
let iscustom = element.startAccessingSecurityScopedResource()
let information = AppUI.shared.information(for: element)
let game = EmulationGame(developer: information.developer, fileURL: element, imageData: information.iconData, title: information.title)
if iscustom {
element.stopAccessingSecurityScopedResource()
}
partialResult.append(game)
}
}
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
var YuzuCore = Core(games: [], root: directory)
games(from: try getromsfromdir(), core: &YuzuCore)
return YuzuCore
}
}
+49
View File
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import Foundation
class FolderMonitor {
private var folderDescriptor: Int32 = -1
private var folderMonitorSource: DispatchSourceFileSystemObject?
private let folderURL: URL
private let onFolderChange: () -> Void
init(folderURL: URL, onFolderChange: @escaping () -> Void) {
self.folderURL = folderURL
self.onFolderChange = onFolderChange
startMonitoring()
}
private func startMonitoring() {
folderDescriptor = open(folderURL.path, O_EVTONLY)
guard folderDescriptor != -1 else {
print("Failed to open folder descriptor.")
return
}
folderMonitorSource = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: folderDescriptor,
eventMask: .write,
queue: DispatchQueue.global()
)
folderMonitorSource?.setEventHandler { [weak self] in
self?.folderDidChange()
}
folderMonitorSource?.setCancelHandler {
close(self.folderDescriptor)
}
folderMonitorSource?.resume()
}
private func folderDidChange() {
// Detect the change and call the refreshcore function
print("Folder changed! New file added or removed.")
DispatchQueue.main.async { [weak self] in
self?.onFolderChange()
}
}
deinit {
folderMonitorSource?.cancel()
}
}
+147
View File
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import Foundation
import UIKit
import UniformTypeIdentifiers
import Combine
struct SettingsView: View {
@ObservedObject var core: EmulationViewModel
@State var showprompt = false
@AppStorage("icon") var iconused = 1
var body: some View {
NavigationStack {
}
}
}
struct GameIconView: View {
var game: EmulationGame
@Binding var selectedGame: EmulationGame?
@State var startgame: Bool = false
@State var timesTapped: Int = 0
var isSelected: Bool {
selectedGame == game
}
var body: some View {
NavigationLink(
destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar),
isActive: $startgame,
label: {
EmptyView()
}
)
VStack(spacing: 5) {
if isSelected {
Text(game.title)
.foregroundColor(.blue)
.font(.title2)
}
if let uiImage = UIImage(data: game.imageData) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
.frame(width: isSelected ? 200 : 180, height: isSelected ? 200 : 180)
.cornerRadius(10)
.overlay(
isSelected ? RoundedRectangle(cornerRadius: 10)
.stroke(Color.blue, lineWidth: 5)
: nil
)
.onTapGesture {
if isSelected {
startgame = true
print(isSelected)
}
if !isSelected {
selectedGame = game
}
}
} else {
Image(systemName: "questionmark")
.resizable()
.scaledToFit()
.frame(width: 200, height: 200)
.cornerRadius(10)
.onTapGesture { selectedGame = game }
}
}
.frame(width: 200, height: 250)
}
}
// Remove or refactor HomeView and GameCarouselView to not use Core or BottomMenuView(core: core) with Core.
// If these are not used in the iOS UI, comment them out to avoid build errors.
// struct HomeView: View {
// @State private var selectedGame: EmulationGame? = nil
// @State var core: Core
// init(selectedGame: EmulationGame? = nil, core: Core) {
// _core = State(wrappedValue: core)
// self.selectedGame = selectedGame
// refreshcore()
// }
// var body: some View {
// NavigationStack {
// GeometryReader { geometry in
// VStack {
// GameCarouselView(core: core, selectedGame: $selectedGame)
// Spacer()
// BottomMenuView(core: core)
// }
// }
// }
// .background(Color.gray.opacity(0.1))
// .edgesIgnoringSafeArea(.all)
// .onAppear {
// refreshcore()
// if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
// let romsFolderURL = documentsDirectory.appendingPathComponent("roms")
// let folderMonitor = FolderMonitor(folderURL: romsFolderURL) {
// do {
// core = Core(games: [], root: documentsDirectory)
// core = try LibraryManager.shared.library()
// } catch {
// print("Error refreshing core: \(error)")
// }
// }
// }
// }
// }
// func refreshcore() {
// print("Loading library...")
// do {
// core = try LibraryManager.shared.library()
// print(core.games)
// } catch {
// print("Failed to fetch library: \(error)")
// return
// }
// }
// }
// struct GameCarouselView: View {
// // let games: [EmulationGame]
// @State var core: Core
// @Binding var selectedGame: EmulationGame?
// var body: some View {
// ScrollView(.horizontal, showsIndicators: false) {
// HStack(spacing: 20) {
// ForEach(core.games) { game in
// GameIconView(game: game, selectedGame: $selectedGame)
// }
// }
// }
// }
// }
+171
View File
@@ -0,0 +1,171 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import Foundation
import UIKit
import UniformTypeIdentifiers
struct GameButtonListView: View {
var game: EmulationGame
@Environment(\.colorScheme) var colorScheme
var body: some View {
HStack(spacing: 15) {
if let image = UIImage(data: game.imageData) {
Image(uiImage: image)
.resizable()
.frame(width: 60, height: 60)
.cornerRadius(8)
} else {
Image(systemName: "photo")
.resizable()
.frame(width: 60, height: 60)
.cornerRadius(8)
}
VStack(alignment: .leading, spacing: 4) {
Text(game.title)
.font(.headline)
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
Text(game.developer)
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
}
.padding(.vertical, 8)
}
}
struct GameListView: View {
@State var core: Core
@State private var searchText = ""
@State var game: Int = 1
@State var startgame: Bool = false
@Binding var isGridView: Bool
@State var showAlert = false
@State var alertMessage: Alert? = nil
var body: some View {
let filteredGames = core.games.filter { game in
guard let EmulationGame = game as? EmulationGame else { return false }
return searchText.isEmpty || EmulationGame.title.localizedCaseInsensitiveContains(searchText)
}
ScrollView {
VStack {
VStack(alignment: .leading) {
if isGridView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) {
ForEach(0..<filteredGames.count, id: \.self) { index in
let game = filteredGames[index] // Use filteredGames here
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
// GameButtonView(game: game)
// .frame(maxWidth: .infinity, minHeight: 200)
}
.contextMenu {
Button(action: {
do {
try LibraryManager.shared.removerom(filteredGames[index])
} catch {
showAlert = true
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription))
}
}) {
Text("Remove")
}
Button(action: {
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") {
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil)
}
}) {
if ProcessInfo.processInfo.isMacCatalystApp {
Text("Open in Finder")
} else {
Text("Open in Files")
}
}
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
Text("Launch")
}
}
}
}
} else {
LazyVStack() {
ForEach(0..<filteredGames.count, id: \.self) { index in
let game = filteredGames[index] // Use filteredGames here
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
GameButtonListView(game: game)
.frame(maxWidth: .infinity, minHeight: 75)
}
.contextMenu {
Button(action: {
do {
try LibraryManager.shared.removerom(filteredGames[index])
try FileManager.default.removeItem(atPath: game.fileURL.path)
} catch {
showAlert = true
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription))
}
}) {
Text("Remove")
}
Button(action: {
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") {
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil)
}
}) {
if ProcessInfo.processInfo.isMacCatalystApp {
Text("Open in Finder")
} else {
Text("Open in Files")
}
}
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
Text("Launch")
}
}
}
}
}
}
.searchable(text: $searchText)
.padding()
}
.onAppear {
refreshcore()
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
let romsFolderURL = documentsDirectory.appendingPathComponent("roms")
let folderMonitor = FolderMonitor(folderURL: romsFolderURL) {
do {
core = Core(games: [], root: documentsDirectory)
core = try LibraryManager.shared.library()
} catch {
print("Error refreshing core: \(error)")
}
}
}
}
.alert(isPresented: $showAlert) {
alertMessage ?? Alert(title: Text("Error Not Found"))
}
}
}
func refreshcore() {
do {
core = try LibraryManager.shared.library()
} catch {
print("Failed to fetch library: \(error)")
return
}
}
}
+54
View File
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
public struct Joystick: View {
@State var iscool: Bool? = nil
var id: Int {
if onscreenjoy {
return 8
}
return 0
}
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
let appui = AppUI.shared
@ObservedObject public var joystickMonitor = JoystickMonitor()
private let dragDiameter: CGFloat = 160
private let shape: JoystickShape = .circle
public var body: some View {
VStack{
JoystickBuilder(
monitor: self.joystickMonitor,
width: self.dragDiameter,
shape: .circle,
background: {
// Example Background
RoundedRectangle(cornerRadius: 8).fill(Color.gray.opacity(0))
},
foreground: {
// Example Thumb
Circle().fill(Color.gray)
},
locksInPlace: false)
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
let scaledX = Float(newValue.x)
let scaledY = Float(-newValue.y) // my dumbass broke this by having -y instead of y :/ (well it appears that with the new joystick code, its supposed to be -y)
joystickMonitor.objectWillChange
print("Joystick Position: (\(scaledX), \(scaledY))")
if iscool != nil {
appui.thumbstickMoved(analog: .right, x: scaledX, y: scaledY, controllerid: id)
} else {
appui.thumbstickMoved(analog: .left, x: scaledX, y: scaledY, controllerid: id)
}
}
}
}
}
+74
View File
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import UIKit
class KeyboardHostingController<Content: View>: UIHostingController<Content> {
override var canBecomeFirstResponder: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
becomeFirstResponder() // Make sure the view can become the first responder
}
override var keyCommands: [UIKeyCommand]? {
return [
UIKeyCommand(input: UIKeyInputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: UIKeyInputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: UIKeyInputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: UIKeyInputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: "w", modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: "s", modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: "a", modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: "d", modifierFlags: [], action: #selector(handleKeyCommand))
]
}
@objc func handleKeyCommand(_ sender: UIKeyCommand) {
if let input = sender.input {
switch input {
case UIKeyInputUpArrow:
print("Up Arrow Pressed")
case UIKeyInputDownArrow:
print("Down Arrow Pressed")
case UIKeyInputLeftArrow:
print("Left Arrow Pressed")
case UIKeyInputRightArrow:
print("Right Arrow Pressed")
case "w":
print("W Key Pressed")
case "s":
print("S Key Pressed")
case "a":
print("A Key Pressed")
case "d":
print("D Key Pressed")
default:
break
}
}
}
}
struct KeyboardSupportView: UIViewControllerRepresentable {
let content: Text
func makeUIViewController(context: Context) -> KeyboardHostingController<Text> {
return KeyboardHostingController(rootView: content)
}
func updateUIViewController(_ uiViewController: KeyboardHostingController<Text>, context: Context) {
// Handle any updates needed
}
}
struct KeyboardView: View {
var body: some View {
KeyboardSupportView(content: Text(""))
}
}

Some files were not shown because too many files have changed in this diff Show More