Compare commits

...

54 commits

Author SHA1 Message Date
lizzie
d084f59e91 pomelo scraps 2026-03-31 01:53:36 +00:00
lizzie
6986084ef4 remove toolchain file + remove multibuild thingy 2026-03-31 01:53:36 +00:00
crueter
1a5ca6f4aa Fix build script
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-31 01:53:36 +00:00
lizzie
7624914c35 AAAAAA 2026-03-31 01:53:36 +00:00
crueter
d7b9c9638e Build fixes
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-31 01:53:36 +00:00
lizzie
5a2fc19c0f add basic ios shit 2026-03-31 01:53:36 +00:00
crueter
028e8f6676 Revert stbi
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-31 01:53:36 +00:00
crueter
fc5eb3a3de fix libs
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-31 01:53:36 +00:00
crueter
3f90add01f Fix most build errors
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-31 01:53:36 +00:00
crueter
cdcb0febc1 Update CPMUtil, and fix script
Signed-off-by: crueter <crueter@eden-emu.dev>
2026-03-31 01:53:36 +00:00
lizzie
65c32d9f72 DISABLE BY DEFAULT ON IOS FFS 2026-03-31 01:53:36 +00:00
lizzie
559bc87cdd fix life 2026-03-31 01:53:36 +00:00
lizzie
9a99bed017 fix sirit i think, add ios-aarch64 2026-03-31 01:53:36 +00:00
lizzie
635785c017 changes? 2026-03-31 01:53:36 +00:00
lizzie
ff4bceb2f3 fix shit? 2026-03-31 01:53:36 +00:00
lizzie
080c359048 fix #include "common/logging.h" 2026-03-31 01:53:36 +00:00
lizzie
75a04e8aa3 fix IOS again fucking objc bridge 2026-03-31 01:53:36 +00:00
lizzie
ebed7a7204 fix? 2026-03-31 01:53:36 +00:00
lizzie
c5a3fade43 properly use bridging header, fix headers 2026-03-31 01:53:36 +00:00
lizzie
5296cdf75e fix swift driver I HOPE 2026-03-31 01:53:36 +00:00
lizzie
353839cfef license 2026-03-31 01:53:36 +00:00
lizzie
fdc42e7e1a [temporary c++ shit] 2026-03-31 01:53:36 +00:00
lizzie
2479d32b22 bit of cmake fuckery 2026-03-31 01:53:36 +00:00
lizzie
c7b9af30fa use language generator exprs 2026-03-31 01:53:36 +00:00
lizzie
b8a132110a $<$<COMPILE_LANGUAGE:C,CXX>:-Werror=missing-declarations> 2026-03-31 01:53:36 +00:00
lizzie
2151c2d37a proper linkings?! 2026-03-31 01:53:36 +00:00
lizzie
89da1c6b9b bridge changes/fixes 2026-03-31 01:53:36 +00:00
lizzie
3d298567ca fix license 2026-03-31 01:53:36 +00:00
lizzie
100d3cd26d fix xcode 2? 2026-03-31 01:53:36 +00:00
lizzie
b26c0ba2f6 fix xcode paths? 2026-03-31 01:53:36 +00:00
lizzie
5766888cbf fx 2026-03-31 01:53:36 +00:00
lizzie
83eea9ba63 fx 2026-03-31 01:53:36 +00:00
lizzie
373277af05 fix boost 2026-03-31 01:53:36 +00:00
lizzie
2b82878af3 fx 2026-03-31 01:53:35 +00:00
lizzie
14d8ff5378 fix stuff 2026-03-31 01:53:35 +00:00
lizzie
29e77a20f1 stupid macos 2026-03-31 01:53:35 +00:00
lizzie
b55b3dc852 fix1 2026-03-31 01:53:35 +00:00
lizzie
5b8cd60309 fx 2026-03-31 01:53:35 +00:00
lizzie
ecd2485fae fix spirv-tools 2026-03-31 01:53:35 +00:00
lizzie
2aad053510 fixes for ios spirv tools 2026-03-31 01:53:35 +00:00
lizzie
87fb06454a fix license 2026-03-31 01:53:35 +00:00
lizzie
a8e1a12d2d fix ffmpeg 2026-03-31 01:53:35 +00:00
lizzie
c1b4112aab fx 2026-03-31 01:53:35 +00:00
lizzie
deaa54e123 fx 2026-03-31 01:53:35 +00:00
lizzie
dec74b7ebc license 2026-03-31 01:53:35 +00:00
lizzie
5c983b417d ios toolchain cmake 2026-03-31 01:53:35 +00:00
lizzie
40c0ae7434 license 2026-03-31 01:53:35 +00:00
lizzie
3df9e8da21 license headers 2026-03-31 01:53:35 +00:00
lizzie
d29cc2949f flatten + cmake 2026-03-31 01:53:35 +00:00
lizzie
1709fb1ad2 flatten 2026-03-31 01:53:35 +00:00
lizzie
71d22bb979 loicense 2026-03-31 01:53:35 +00:00
lizzie
1252f7203e modernize #1 2026-03-31 01:53:35 +00:00
lizzie
9b19d17c9c sudachi ios stuff 2026-03-31 01:53:35 +00:00
lizzie
7a8176f63f
[dynarmic] implement missing SSE3 implementations (#3301)
Implementations for SSE3 CPUs (prescott)

Instead of fixing some of the bugs with HostCall when paired with vectors, i'll simply remove as many host calls as I can within the most used vector instructions - then just minimize their usage to memory read/writes.
Emitting the raw assembly code is faster than doing a HostCall, HostCalls are VERY expensive. So this is the desired output anyways.

Signed-off-by: lizzie <lizzie@eden-emu.dev>

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3301
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-31 02:53:51 +02:00
94 changed files with 5928 additions and 1130 deletions

19
.ci/ios/build.sh Executable file
View file

@ -0,0 +1,19 @@
#!/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 \
"$@"
cmake --build build/ios -t eden-ios --config Release

View file

@ -115,7 +115,7 @@ for file in $FILES; do
*.cmake|*.sh|*CMakeLists.txt)
begin="#"
;;
*.kt*|*.cpp|*.h|*.qml)
*.kt*|*.cpp|*.h|*.qml|*.swift|*.mm)
begin="//"
;;
*)

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_()
{

View file

@ -0,0 +1,33 @@
diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt
index 7ab2319..333e325 100644
--- a/source/CMakeLists.txt
+++ b/source/CMakeLists.txt
@@ -151,9 +151,11 @@ add_custom_command(OUTPUT ${SPIRV_TOOLS_BUILD_VERSION_INC}
COMMENT "Update build-version.inc in the SPIRV-Tools build directory (if necessary).")
# Convenience target for standalone generation of the build-version.inc file.
# This is not required for any dependence chain.
-add_custom_target(spirv-tools-build-version
- DEPENDS ${SPIRV_TOOLS_BUILD_VERSION_INC})
-set_property(TARGET spirv-tools-build-version PROPERTY FOLDER "SPIRV-Tools build")
+if (NOT IOS)
+ add_custom_target(spirv-tools-build-version
+ DEPENDS ${SPIRV_TOOLS_BUILD_VERSION_INC})
+ set_property(TARGET spirv-tools-build-version PROPERTY FOLDER "SPIRV-Tools build")
+endif()
list(APPEND PCH_DEPENDS
${CORE_TABLES_HEADER_INC_FILE}
@@ -338,8 +340,11 @@ function(spirv_tools_default_target_options target)
)
set_property(TARGET ${target} PROPERTY FOLDER "SPIRV-Tools libraries")
spvtools_check_symbol_exports(${target})
- add_dependencies(${target}
- spirv-tools-build-version core_tables extinst_tables)
+ if (IOS)
+ add_dependencies(${target} core_tables extinst_tables)
+ else ()
+ add_dependencies(${target} spirv-tools-build-version core_tables extinst_tables)
+ endif()
endfunction()
if (SPIRV_TOOLS_BUILD_SHARED)

View file

@ -21,6 +21,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()
@ -42,7 +65,7 @@ if (PLATFORM_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 "PLATFORM_LINUX" OFF)
cmake_dependent_option(YUZU_STATIC_ROOM "Build a static room executable only (CI only)" OFF "PLATFORM_LINUX OR WIN32 OR (APPLE AND NOT IOS)" OFF)
if (YUZU_STATIC_ROOM)
set(YUZU_ROOM ON)
set(YUZU_ROOM_STANDALONE ON)
@ -67,9 +90,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")
@ -170,31 +199,32 @@ if (MSVC AND NOT CXX_CLANG)
set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /W3 /WX-")
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" "${MSVC}" "NOT ANDROID" OFF)
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" "${PLATFORM_SUN}" "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 PLATFORM_LINUX OR PLATFORM_FREEBSD OR APPLE" OFF)
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR PLATFORM_LINUX OR PLATFORM_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)
@ -212,10 +242,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 PLATFORM_LINUX" OFF)
@ -283,7 +313,7 @@ if (YUZU_ROOM)
add_compile_definitions(YUZU_ROOM)
endif()
if ((ANDROID OR APPLE OR UNIX) AND (NOT PLATFORM_LINUX OR ANDROID) AND NOT WIN32)
if ((ANDROID OR APPLE OR UNIX OR IOS) AND (NOT PLATFORM_LINUX OR ANDROID) AND NOT WIN32)
if(CXX_APPLE OR CXX_CLANG)
# libc++ has stop_token and jthread as experimental
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library")
@ -359,7 +389,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)
@ -484,9 +517,15 @@ endfunction()
# Platform-specific library requirements
# Put these BEFORE EXTERNALS or Boost WILL die
# =============================================
if (APPLE)
foreach(fw Carbon Metal Cocoa IOKit CoreVideo CoreMedia)
set(_libs Metal IOKit CoreVideo CoreMedia)
if (IOS)
list(APPEND _libs objc)
else()
list(APPEND _libs Carbon Cocoa)
endif()
foreach(fw ${_libs})
find_library(${fw}_LIBRARY ${fw} REQUIRED)
list(APPEND PLATFORM_LIBRARIES ${${fw}_LIBRARY})
endforeach()

View file

@ -3,7 +3,7 @@
set(CPM_SOURCE_CACHE "${PROJECT_SOURCE_DIR}/.cache/cpm" CACHE STRING "" FORCE)
if(MSVC OR ANDROID)
if(MSVC OR ANDROID OR IOS)
set(BUNDLED_DEFAULT ON)
else()
set(BUNDLED_DEFAULT OFF)
@ -690,8 +690,10 @@ function(AddCIPackage)
set(pkgname linux-amd64)
elseif(PLATFORM_LINUX AND ARCHITECTURE_arm64)
set(pkgname linux-aarch64)
elseif(APPLE)
elseif(APPLE AND NOT IOS)
set(pkgname macos-universal)
elseif(IOS AND ARCHITECTURE_arm64)
set(pkgname ios-aarch64)
endif()
if (DEFINED pkgname AND NOT "${pkgname}" IN_LIST DISABLED_PLATFORMS)

View file

@ -17,7 +17,8 @@
"version": "1.57",
"find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem",
"patches": [
"0001-clang-cl.patch"
"0001-clang-cl.patch",
"0002-ios-fix.patch"
]
},
"fmt": {

View file

@ -18,3 +18,4 @@
- `linux-amd64`
- `linux-aarch64`
- `macos-universal`
- `ios-aarch64`

View file

@ -61,7 +61,8 @@ In order: OpenSSL CI, Boost (tag + artifact), Opus (options + find_args), discor
"version": "3.6.0",
"min_version": "1.1.1",
"disabled_platforms": [
"macos-universal"
"macos-universal",
"ios-aarch64"
]
},
"boost": {

View file

@ -4,6 +4,7 @@
- [Arch Linux](#arch-linux)
- [Gentoo Linux](#gentoo-linux)
- [macOS](#macos)
- [iOS](#ios)
- [Solaris](#solaris)
- [HaikuOS](#haikuos)
- [OpenBSD](#openbsd)
@ -31,6 +32,16 @@ If you're having issues with building, always consult that ebuild.
macOS is largely untested. Expect crashes, significant Vulkan issues, and other fun stuff.
## 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
```
## Solaris
Always consult [the OpenIndiana package list](https://pkg.openindiana.org/hipster/en/index.shtml) to cross-verify availability.

View file

@ -228,6 +228,10 @@ if (VulkanMemoryAllocator_ADDED)
endif()
# httplib
if (IOS)
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF)
endif()
AddJsonPackage(httplib)
# cpp-jwt

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}")

View file

@ -51,6 +51,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)

View file

@ -23,7 +23,7 @@
"package": "sirit",
"name": "sirit",
"repo": "eden-emulator/sirit",
"version": "1.0.4"
"version": "1.0.5"
},
"httplib": {
"repo": "yhirose/cpp-httplib",
@ -36,7 +36,8 @@
"0002-fix-zstd.patch"
],
"options": [
"HTTPLIB_REQUIRE_OPENSSL ON"
"HTTPLIB_REQUIRE_OPENSSL ON",
"HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES ON"
]
},
"cpp-jwt": {
@ -111,7 +112,8 @@
],
"patches": [
"0001-netbsd-fix.patch",
"0002-allow-static-only.patch"
"0002-allow-static-only.patch",
"0003-ios-fix.patch"
]
},
"spirv-headers": {

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
# SPDX-FileCopyrightText: 2021 yuzu Emulator Project
@ -11,9 +11,9 @@ set(FFmpeg_HWACCEL_FLAGS)
set(FFmpeg_HWACCEL_INCLUDE_DIRS)
set(FFmpeg_HWACCEL_LDFLAGS)
if (UNIX AND NOT ANDROID)
if (UNIX AND NOT ANDROID AND NOT IOS)
find_package(PkgConfig REQUIRED)
if (NOT ANDROID)
if (NOT ANDROID AND NOT IOS)
pkg_check_modules(LIBVA libva)
pkg_check_modules(CUDA cuda)
pkg_check_modules(FFNVCODEC ffnvcodec)
@ -182,6 +182,10 @@ else()
find_program(BASH_PROGRAM bash REQUIRED)
set(FFmpeg_CROSS_COMPILE_FLAGS "")
# `configure` parameters builds only exactly what yuzu needs from FFmpeg
# `--disable-vdpau` is needed to avoid linking issues
set(FFmpeg_CC ${CMAKE_C_COMPILER_LAUNCHER} ${CMAKE_C_COMPILER})
set(FFmpeg_CXX ${CMAKE_CXX_COMPILER_LAUNCHER} ${CMAKE_CXX_COMPILER})
if (ANDROID)
string(TOLOWER "${CMAKE_HOST_SYSTEM_NAME}" FFmpeg_HOST_SYSTEM_NAME)
set(TOOLCHAIN "${ANDROID_NDK}/toolchains/llvm/prebuilt/${FFmpeg_HOST_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}")
@ -197,12 +201,23 @@ else()
--extra-ldflags="--ld-path=${TOOLCHAIN}/bin/ld.lld"
--extra-ldflags="-nostdlib"
)
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
)
endif()
# `configure` parameters builds only exactly what yuzu needs from FFmpeg
# `--disable-vdpau` is needed to avoid linking issues
set(FFmpeg_CC ${CMAKE_C_COMPILER_LAUNCHER} ${CMAKE_C_COMPILER})
set(FFmpeg_CXX ${CMAKE_CXX_COMPILER_LAUNCHER} ${CMAKE_CXX_COMPILER})
add_custom_command(
OUTPUT
${FFmpeg_MAKEFILE}

View file

@ -24,7 +24,8 @@ if (MINGW OR PLATFORM_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()

View file

@ -127,13 +127,15 @@ else()
add_compile_options(
$<$<COMPILE_LANGUAGE:C,CXX>:-Werror=all>
$<$<COMPILE_LANGUAGE:C,CXX>:-Werror=extra>
$<$<COMPILE_LANGUAGE:C,CXX>:-Werror=missing-declarations>
$<$<COMPILE_LANGUAGE:C,CXX>:-Werror=shadow>
$<$<COMPILE_LANGUAGE:C,CXX>:-Werror=unused>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-attributes>
$<$<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)
@ -249,4 +251,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)

View file

@ -144,7 +144,8 @@ add_library(
zstd_compression.cpp
zstd_compression.h
fs/ryujinx_compat.h fs/ryujinx_compat.cpp
fs/symlink.h fs/symlink.cpp)
fs/symlink.h fs/symlink.cpp
httplib.h)
if(WIN32)
target_sources(common PRIVATE windows/timer_resolution.cpp
@ -242,7 +243,7 @@ else()
target_link_libraries(common PUBLIC Boost::headers)
endif()
target_link_libraries(common PUBLIC Boost::filesystem Boost::context)
target_link_libraries(common PUBLIC Boost::filesystem Boost::context httplib::httplib)
if (lz4_ADDED)
target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib)

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;
}
}

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__)

9
src/common/httplib.h Normal file
View file

@ -0,0 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#define CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES
#define CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>

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
// SPDX-FileCopyrightText: 2013 Dolphin Emulator Project
@ -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

View file

@ -1264,12 +1264,15 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
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)
target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(core PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
# TODO

View file

@ -1,10 +1,10 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <dynarmic/interface/halt_reason.h>
#include "dynarmic/src/dynarmic/interface/halt_reason.h"
#include "core/arm/arm_interface.h"

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"

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;

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 {

View file

@ -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"

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

View file

@ -15,9 +15,7 @@
#include <fmt/format.h>
#include <nlohmann/json.hpp>
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#include <httplib.h>
#endif
#include "common/httplib.h"
#include <chrono>
#include <cstring>
@ -37,7 +35,7 @@
namespace Service::News {
namespace {
constexpr const char* GitHubAPI_EdenReleases = "/repos/eden-emulator/Releases/releases";
[[maybe_unused]] constexpr const char* GitHubAPI_EdenReleases = "/repos/eden-emulator/Releases/releases";
// Cached logo data
std::vector<u8> default_logo_small;
@ -104,7 +102,6 @@ std::vector<u8> TryLoadFromDisk(const std::filesystem::path& path) {
std::vector<u8> DownloadImage(const std::string& url_path, const std::filesystem::path& cache_path) {
LOG_INFO(Service_BCAT, "Downloading image: https://eden-emu.dev{}", url_path);
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
try {
httplib::Client cli("https://eden-emu.dev");
cli.set_follow_location(true);
@ -128,7 +125,6 @@ std::vector<u8> DownloadImage(const std::string& url_path, const std::filesystem
} catch (...) {
LOG_WARNING(Service_BCAT, "Failed to download: {}", url_path);
}
#endif
return {};
}
@ -233,7 +229,6 @@ void WriteCachedJson(std::string_view json) {
std::optional<std::string> DownloadReleasesJson() {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
try {
httplib::SSLClient cli{"api.github.com", 443};
cli.set_connection_timeout(10);
@ -255,7 +250,7 @@ std::optional<std::string> DownloadReleasesJson() {
} catch (...) {
LOG_WARNING(Service_BCAT, " failed to download releases");
}
#endif
return std::nullopt;
}

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"

View file

@ -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()

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
@ -146,7 +145,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
@ -207,7 +206,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

File diff suppressed because it is too large Load diff

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
/* 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 {

View file

@ -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;

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
/* 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 {

View file

@ -14,7 +14,7 @@
#include <memory>
#include <optional>
#include "dynarmic/interface/optimization_flags.h"
#include "../optimization_flags.h"
namespace Dynarmic {
class ExclusiveMonitor;

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/* This file is part of the dynarmic project.
* Copyright (c) 2018 MerryMage
* SPDX-License-Identifier: 0BSD
@ -11,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 {

View file

@ -415,6 +415,105 @@ TEST_CASE("A64: URSHL", "[a64]") {
CHECK(jit.GetVector(9) == Vector{0x0000000000000002, 0x12db8b8280e0ba});
}
TEST_CASE("A64: SQSHLU", "[a64]") {
A64TestEnv env;
A64::UserConfig jit_user_config{};
jit_user_config.callbacks = &env;
A64::Jit jit{jit_user_config};
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
code.SQSHLU(V8.B16(), V0.B16(), 1);
code.SQSHLU(V9.H8(), V1.H8(), 2);
code.SQSHLU(V10.S4(), V2.S4(), 28);
code.SQSHLU(V11.D2(), V3.D2(), 4);
code.SQSHLU(V12.S4(), V0.S4(), 1);
code.SQSHLU(V13.S4(), V1.S4(), 3);
code.SQSHLU(V14.S4(), V2.S4(), 0);
code.SQSHLU(V15.S4(), V3.S4(), 0);
jit.SetVector(0, Vector{0xffffffff'18ba6a6a, 0x7fffffff'943b954f});
jit.SetVector(1, Vector{0x0000000b'0000000f, 0xffffffff'ffffffff});
jit.SetVector(2, Vector{0x00000001'000000ff, 0x00000010'0000007f});
jit.SetVector(3, Vector{0xffffffffffffffff, 0x96dc5c140705cd04});
env.ticks_left = env.code_mem.size();
CheckedRun([&]() { jit.Run(); });
CHECK(jit.GetVector(8) == Vector{0x3000d4d4, 0xfe0000000076009e});
CHECK(jit.GetVector(9) == Vector{0x2c0000003c, 0});
CHECK(jit.GetVector(10) == Vector{0x10000000'ffffffff, 0xffffffff'ffffffff});
CHECK(jit.GetVector(11) == Vector{0, 0});
CHECK(jit.GetVector(12) == Vector{0x3174d4d4, 0xfffffffe00000000});
CHECK(jit.GetVector(13) == Vector{0x5800000078, 0});
CHECK(jit.GetVector(14) == Vector{0x1000000ff, 0x100000007f});
CHECK(jit.GetVector(15) == Vector{0, 0x705cd04});
}
TEST_CASE("A64: SMIN", "[a64]") {
A64TestEnv env;
A64::UserConfig jit_user_config{};
jit_user_config.callbacks = &env;
A64::Jit jit{jit_user_config};
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
code.SMIN(V8.B16(), V0.B16(), V3.B16());
code.SMIN(V9.H8(), V1.H8(), V2.H8());
code.SMIN(V10.S4(), V2.S4(), V3.S4());
code.SMIN(V11.S4(), V3.S4(), V3.S4());
code.SMIN(V12.S4(), V0.S4(), V3.S4());
code.SMIN(V13.S4(), V1.S4(), V2.S4());
code.SMIN(V14.S4(), V2.S4(), V1.S4());
code.SMIN(V15.S4(), V3.S4(), V0.S4());
jit.SetPC(0);
jit.SetVector(0, Vector{0xffffffff'18ba6a6a, 0x7fffffff'943b954f});
jit.SetVector(1, Vector{0x0000000b'0000000f, 0xffffffff'ffffffff});
jit.SetVector(2, Vector{0x00000001'000000ff, 0x00000010'0000007f});
jit.SetVector(3, Vector{0xffffffff'ffffffff, 0x96dc5c14'0705cd04});
env.ticks_left = 4;
CheckedRun([&]() { jit.Run(); });
REQUIRE(jit.GetVector(8) == Vector{0xffffffffffbaffff, 0x96dcffff94059504});
REQUIRE(jit.GetVector(9) == Vector{0x10000000f, 0xffffffffffffffff});
REQUIRE(jit.GetVector(10) == Vector{0xffffffffffffffff, 0x96dc5c140000007f});
}
TEST_CASE("A64: SMINP", "[a64]") {
A64TestEnv env;
A64::UserConfig jit_user_config{};
jit_user_config.callbacks = &env;
A64::Jit jit{jit_user_config};
oaknut::VectorCodeGenerator code{env.code_mem, nullptr};
code.SMINP(V8.B16(), V0.B16(), V3.B16());
code.SMINP(V9.H8(), V1.H8(), V2.H8());
code.SMINP(V10.S4(), V2.S4(), V1.S4());
code.SMINP(V11.S4(), V3.S4(), V3.S4());
code.SMINP(V12.S4(), V0.S4(), V3.S4());
code.SMINP(V13.S4(), V1.S4(), V2.S4());
code.SMINP(V14.S4(), V2.S4(), V1.S4());
code.SMINP(V15.S4(), V3.S4(), V0.S4());
jit.SetPC(0);
jit.SetVector(0, Vector{0xffffffff'18ba6a6a, 0x7fffffff'943b954f});
jit.SetVector(1, Vector{0x0000000b'0000000f, 0xffffffff'ffffffff});
jit.SetVector(2, Vector{0x00000001'000000ff, 0x00000010'0000007f});
jit.SetVector(3, Vector{0xffffffff'ffffffff, 0x96dc5c14'0705cd04});
env.ticks_left = 4;
CheckedRun([&]() { jit.Run(); });
REQUIRE(jit.GetVector(8) == Vector{0xffff9495ffffba6a, 0x961405cdffffffff});
REQUIRE(jit.GetVector(9) == Vector{0xffffffff00000000, 0});
REQUIRE(jit.GetVector(10) == Vector{0x1000000001, 0xffffffff0000000b});
REQUIRE(jit.GetVector(11) == Vector{0x96dc5c14ffffffff, 0x96dc5c14ffffffff});
REQUIRE(jit.GetVector(12) == Vector{0x943b954fffffffff, 0x96dc5c14ffffffff});
REQUIRE(jit.GetVector(13) == Vector{0xffffffff0000000b, 0x1000000001});
REQUIRE(jit.GetVector(14) == Vector{0x1000000001, 0xffffffff0000000b});
REQUIRE(jit.GetVector(15) == Vector{0x96dc5c14ffffffff, 0x943b954fffffffff});
}
TEST_CASE("A64: XTN", "[a64]") {
A64TestEnv env;
A64::UserConfig jit_user_config{};

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
)

View file

@ -22,8 +22,6 @@ if (ENABLE_UPDATE_CHECKER)
target_sources(frontend_common PRIVATE
update_checker.cpp
update_checker.h)
target_compile_definitions(frontend_common PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries(frontend_common PRIVATE OpenSSL::SSL OpenSSL::Crypto)
endif()

View file

@ -13,7 +13,7 @@
#include "common/scm_rev.h"
#include "update_checker.h"
#include <httplib.h>
#include "common/httplib.h"
#ifdef YUZU_BUNDLED_OPENSSL
#include <openssl/cert.h>

View file

@ -0,0 +1,77 @@
// 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 UniformTypeIdentifiers
struct AdvancedSettingsView: View {
@AppStorage("exitgame") var exitgame: Bool = false
@AppStorage("ClearBackingRegion") var kpagetable: Bool = false
@AppStorage("WaitingforJIT") var waitingJIT: Bool = false
@AppStorage("cangetfullpath") var canGetFullPath: Bool = false
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
var body: some View {
ScrollView {
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Exit Game Button", isOn: $exitgame)
.padding()
}
}
Text("This is very unstable and can lead to game freezing and overall bad preformance after you exit a game")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Memory Usage Increase", isOn: $kpagetable)
.padding()
}
}
Text("This makes games way more stable but a lot of games will crash as you will run out of Memory way quicker. (Don't Enable this on devices with less then 8GB of memory as most games will crash)")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Check for Booting OS", isOn: $canGetFullPath)
.padding()
}
}
Text("If you do not have the neccesary files for Booting the Switch OS, it will just crash almost instantly.")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
Rectangle()
.fill(Color(uiColor: UIColor.secondarySystemBackground))
.cornerRadius(10)
.frame(width: .infinity, height: 50)
.overlay() {
HStack {
Toggle("Set OnScreen Controls to Handheld", isOn: $onscreenjoy)
.padding()
}
}
Text("You need in Core Settings to set \"use_docked_mode = 0\"")
.padding(.bottom)
.font(.footnote)
.foregroundColor(.gray)
}
}
}

0
src/ios/Air.swift Normal file
View file

0
src/ios/AirPlay.swift Normal file
View file

View file

@ -0,0 +1,19 @@
// 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
enum AppIconProvider {
static func appIcon(in bundle: Bundle = .main) -> String {
guard let icons = bundle.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any],
let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any],
let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String],
let iconFileName = iconFiles.last else {
print("Could not find icons in bundle")
return ""
}
return iconFileName
}
}

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 */

103
src/ios/AppUI.swift Normal file
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
import UIKit
import Foundation
import QuartzCore.CAMetalLayer
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()
}
}

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

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
src/ios/AppUIObjC.h Normal file
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
src/ios/AppUIObjC.mm Normal file
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

26
src/ios/BootOSView.swift Normal file
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 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import AppUI
struct BootOSView: View {
@Binding var core: Core
@Binding var currentnavigarion: Int
@State var appui = AppUI.shared
@AppStorage("cangetfullpath") var canGetFullPath: Bool = false
var body: some View {
if (appui.canGetFullPath() -- canGetFullPath) {
EmulationView(game: nil)
} else {
VStack {
Text("Unable Launch Switch OS")
.font(.largeTitle)
.padding()
Text("You do not have the Switch Home Menu Files Needed to launch the Ηome Menu")
}
}
}
}

98
src/ios/CMakeLists.txt Normal file
View file

@ -0,0 +1,98 @@
# 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
EnableJIT.swift
EmulationGame.swift
JoystickView.swift
CoreSettingsView.swift
ContentView.swift
EmulationHandler.swift
DetectServer.swift
NavView.swift
PomeloApp.swift
SettingsView.swift
FileManager.swift
EmulationView.swift
LibraryView.swift
GameButtonListView.swift
KeyboardHostingController.swift
MetalView.swift
BootOSView.swift
ControllerView.swift
AppUI.swift
InfoView.swift
FolderMonitor.swift
AdvancedSettingsView.swift
GameButtonView.swift
AppIconProvider.swift
Haptics.swift
EmulationScreenView.swift
GameListView.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
src/ios/Config.h Normal file
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
src/ios/Config.mm Normal file
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

15
src/ios/ContentView.swift Normal file
View file

@ -0,0 +1,15 @@
// 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 AppUI
struct ContentView: View {
@State var core = Core(games: [], root: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0])
var body: some View {
HomeView(core: core).onAppear() {
}
}
}

View file

@ -0,0 +1,420 @@
// 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 GameController
import AppUI
import SwiftUIJoystick
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
let deltaTimestamp = Int32((currentTimestamp - lastTimestamp) * 1000) // Difference in milliseconds
// 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)
}
}
struct ButtonView: View {
var button: VirtualControllerButtonType
@StateObject private var viewModel: EmulationViewModel = EmulationViewModel(game: nil)
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 ""
}
}
}

View file

@ -0,0 +1,88 @@
// 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 AppUI
struct CoreSettingsView: View {
@State private var text: String = ""
@State private var isLoading: Bool = true
@Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
} else {
TextEditor(text: $text)
.padding()
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let configfolder = documentDirectory.appendingPathComponent("config", conformingTo: .folder)
let fileURL = configfolder.appendingPathComponent("config.ini")
presentationMode.wrappedValue.dismiss()
do {
try FileManager.default.removeItem(at: fileURL)
} catch {
print("\(error.localizedDescription)")
}
AppUI.shared.settingsSaved()
} label: {
Text("Reset File")
}
}
}
.onAppear {
loadFile()
}
.onDisappear() {
saveFile()
}
}
private func loadFile() {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let configfolder = documentDirectory.appendingPathComponent("config", conformingTo: .folder)
let fileURL = configfolder.appendingPathComponent("config.ini")
if fileManager.fileExists(atPath: fileURL.path) {
do {
text = try String(contentsOf: fileURL, encoding: .utf8)
} catch {
print("Error reading file: \(error)")
}
} else {
text = "" // Initialize with empty text if file doesn't exist
}
isLoading = false
}
private func saveFile() {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let configfolder = documentDirectory.appendingPathComponent("config", conformingTo: .folder)
let fileURL = configfolder.appendingPathComponent("config.ini")
do {
try text.write(to: fileURL, atomically: true, encoding: .utf8)
AppUI.shared.settingsSaved()
print("File saved successfully!")
} catch {
print("Error saving file: \(error)")
}
}
}

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 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import Foundation
func isSideJITServerDetected(completion: @escaping (Result<Void, Error>) -> Void) {
let address = UserDefaults.standard.string(forKey: "sidejitserver") ?? ""
var SJSURL = address
if (address).isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
// Create a network operation at launch to Refresh SideJITServer
let url = URL(string: SJSURL)!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
print("No SideJITServer on Network")
completion(.failure(error))
return
}
completion(.success(()))
}
task.resume()
return
}

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: Yuzu) -> Bool {
lhs.title < rhs.title
}
static func == (lhs: EmulationGame, rhs: Yuzu) -> Bool {
lhs.title == rhs.title
}
}

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 AppUI
import Metal
import Foundation
class EmulationViewModel: 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
}
}
}

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 AppUI
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
src/ios/EmulationSession.h Normal file
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
src/ios/EmulationSession.mm Normal file
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();
}

137
src/ios/EmulationView.swift Normal file
View file

@ -0,0 +1,137 @@
// 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 AppUI
import Foundation
import GameController
import UIKit
import SwiftUIIntrospect
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(game: game))
}
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
src/ios/EmulationWindow.h Normal file
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

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();
}

52
src/ios/EnableJIT.swift Normal file
View file

@ -0,0 +1,52 @@
// 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
enum SideJITServerErrorType: Error {
case invalidURL
case errorConnecting
case deviceNotFound
case other(String)
}
func sendrequestsidejit(url: String, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
let url = URL(string: url)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
if datastring == "Enabled JIT" {
completion(.success(()))
} else {
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
completion(.failure(errorType))
}
}
task.resume()
}
func sendrefresh(url: String, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
let url = URL(string: url)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
let inputText = "{\"OK\":\"Refreshed!\"}"
if datastring == inputText {
completion(.success(()))
} else {
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
completion(.failure(errorType))
}
}
task.resume()
}

254
src/ios/FileManager.swift Normal file
View file

@ -0,0 +1,254 @@
// 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 AppUI
import Zip
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)")
}
}
}
class YuzuFileManager {
static var shared = YuzuFileManager()
func directories() -> [String : [String : String]] {
[
"themes" : [:],
"amiibo" : [:],
"cache" : [:],
"config" : [:],
"crash_dumps" : [:],
"dump" : [:],
"keys" : [:],
"load" : [:],
"log" : [:],
"nand" : [:],
"play_time" : [:],
"roms" : [:],
"screenshots" : [:],
"sdmc" : [:],
"shader" : [:],
"tas" : [:],
"icons" : [:]
]
}
func createdirectories() throws {
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
try directories().forEach() { directory, filename in
let directoryURL = documentdir.appendingPathComponent(directory)
if !FileManager.default.fileExists(atPath: directoryURL.path) {
print("creating dir at \(directoryURL.path)") // yippee
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: false, attributes: nil)
}
}
}
func DetectKeys() -> (Bool, Bool) {
var prodkeys = false
var titlekeys = false
let filemanager = FileManager.default
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let KeysFolderURL = documentdir.appendingPathComponent("keys")
prodkeys = filemanager.fileExists(atPath: KeysFolderURL.appendingPathComponent("prod.keys").path)
titlekeys = filemanager.fileExists(atPath: KeysFolderURL.appendingPathComponent("title.keys").path)
return (prodkeys, titlekeys)
}
}
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
}
}

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()
}
}

View file

@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, TechGuy
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import Foundation
import UIKit
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)
}
}

View file

@ -0,0 +1,182 @@
// 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 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)
}
}
struct BottomMenuView: View {
@State var core: Core
var body: some View {
HStack(spacing: 40) {
Button {
} label: {
Circle()
.overlay {
Image(systemName: "message").font(.system(size: 30)).foregroundColor(.red)
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .lightGray))
}
Button {
} label: {
Circle()
.overlay {
Image(systemName: "photo").font(.system(size: 30)).foregroundColor(.blue)
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .lightGray))
}
NavigationLink(destination: SettingsView(core: core)) {
Circle()
.overlay {
Image(systemName: "gearshape").foregroundColor(Color.init(uiColor: .darkGray)).font(.system(size: 30))
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .lightGray))
}
Button {
} label: {
Circle()
.overlay {
Image(systemName: "power").foregroundColor(Color.init(uiColor: .darkGray)).font(.system(size: 30))
}
.frame(width: 50, height: 50)
.foregroundColor(Color.init(uiColor: .lightGray))
}
}
.padding(.bottom, 20)
}
}
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)
}
}
}
}
}

140
src/ios/GameListView.swift Normal file
View file

@ -0,0 +1,140 @@
// 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 AppUI
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? PoYuzume 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
}
}
}

23
src/ios/Haptics.swift Normal file
View file

@ -0,0 +1,23 @@
// 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 UIKit
import SwiftUI
import AppUI
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)
}
}

46
src/ios/InfoView.swift Normal file
View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Yuzu, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
struct InfoView: View {
@AppStorage("entitlementNotExists") private var entitlementNotExists: Bool = false
@AppStorage("increaseddebugmem") private var increaseddebugmem: Bool = false
@AppStorage("extended-virtual-addressing") private var extended: Bool = false
let infoDictionary = Bundle.main.infoDictionary
var body: some View {
ScrollView {
VStack {
Text("Welcome").font(.largeTitle)
Divider()
Text("Entitlements:").font(.title).font(Font.headline.weight(.bold))
Spacer().frame(height: 10)
Group {
Text("Required:").font(.title2).font(Font.headline.weight(.bold))
Spacer().frame(height: 10)
Text("Limit: \(String(describing: !entitlementNotExists))")
Spacer().frame(height: 10)
}
Group {
Spacer().frame(height: 10)
Text("Reccomended:").font(.title2).font(Font.headline.weight(.bold))
Spacer().frame(height: 10)
Text("Limit: \(String(describing: increaseddebugmem))").padding()
Text("Extended: \(String(describing: extended))")
}
}
.padding()
Text("Version: \(getAppVersion())").foregroundColor(.gray)
}
}
func getAppVersion() -> String {
guard let s = infoDictionary?["CFBundleShortVersionString"] as? String else {
return "Unknown"
}
return s
}
}

View file

@ -0,0 +1,55 @@
// 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 SwiftUIJoystick
import AppUI
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)
}
}
}
}
}

View file

@ -0,0 +1,76 @@
// 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: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: UIKeyCommand.inputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
UIKeyCommand(input: UIKeyCommand.inputRightArrow, 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 UIKeyCommand.inputUpArrow:
print("Up Arrow Pressed")
case UIKeyCommand.inputDownArrow:
print("Down Arrow Pressed")
case UIKeyCommand.inputLeftArrow:
print("Left Arrow Pressed")
case UIKeyCommand.inputRightArrow:
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(""))
}
}

191
src/ios/LibraryView.swift Normal file
View file

@ -0,0 +1,191 @@
// 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 CryptoKit
import AppUI
struct LibraryView: View {
@Binding var core: Core
@State var isGridView: Bool = true
@State var doesitexist = (false, false)
@State var importedgame: EmulationGame? = nil
@State var importgame: Bool = false
@State var isimportingfirm: Bool = false
@State var launchGame: Bool = false
var body: some View {
NavigationStack {
if let importedgame = importedgame {
NavigationLink(
isActive: $launchGame,
destination: {
EmulationView(game: importedgame).toolbar(.hidden, for: .tabBar)
},
label: {
EmptyView() // This keeps the link hidden
}
)
}
VStack {
if doesitexist.0, doesitexist.1 {
HomeView(core: core)
} else {
let (doesKeyExist, doesProdExist) = doeskeysexist()
ScrollView {
Text("You Are Missing These Files:")
.font(.headline)
.foregroundColor(.red)
HStack {
if !doesProdExist {
Text("Prod.keys")
.font(.subheadline)
.foregroundColor(.red)
}
if !doesKeyExist {
Text("Title.keys")
.font(.subheadline)
.foregroundColor(.red)
}
}
Text("These goes into the Keys folder")
.font(.caption)
.foregroundColor(.red)
.padding(.bottom)
if !LibraryManager.shared.homebrewroms().isEmpty {
Text("Homebrew Roms:")
.font(.headline)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) {
ForEach(LibraryManager.shared.homebrewroms()) { game in
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
// GameButtonView(game: game)
// .frame(maxWidth: .infinity, minHeight: 200)
}
.contextMenu {
NavigationLink(destination: EmulationView(game: game)) {
Text("Launch")
}
}
}
}
}
}
.refreshable {
doesitexist = doeskeysexist()
}
}
}
.fileImporter(isPresented: $isimportingfirm, allowedContentTypes: [.zip], onCompletion: { result in
switch result {
case .success(let elements):
core.AddFirmware(at: elements)
case .failure(let error):
print(error.localizedDescription)
}
})
.fileImporter(isPresented: $importgame, allowedContentTypes: [.item], onCompletion: { result in
switch result {
case .success(let elements):
let iscustom = elements.startAccessingSecurityScopedResource()
let information = AppUI.shared.information(for: elements)
let game = EmulationGame(developer: information.developer, fileURL: elements,
imageData: information.iconData,
title: information.title)
importedgame = game
DispatchQueue.main.async {
if iscustom {
elements.stopAccessingSecurityScopedResource()
}
launchGame = true
}
case .failure(let error):
print(error.localizedDescription)
}
})
.onAppear() {
doesitexist = doeskeysexist()
}
.navigationBarTitle("Library", displayMode: .inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) { // why did this take me so long to figure out lmfao
Button(action: {
isGridView.toggle()
}) {
Image(systemName: isGridView ? "rectangle.grid.1x2" : "square.grid.2x2")
.imageScale(.large)
.padding()
}
}
ToolbarItem(placement: .navigationBarTrailing) { // funsies
Menu {
Button(action: {
importgame = true // this part took a while
}) {
Text("Launch Game")
}
Button(action: {
isimportingfirm = true
}) {
Text("Import Firmware")
}
} label: {
Image(systemName: "plus.circle.fill")
.imageScale(.large)
.padding()
}
}
}
}
}
func doeskeysexist() -> (Bool, Bool) {
var doesprodexist = false
var doestitleexist = false
let title = core.root.appendingPathComponent("keys").appendingPathComponent("title.keys")
let prod = core.root.appendingPathComponent("keys").appendingPathComponent("prod.keys")
let fileManager = FileManager.default
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
if fileManager.fileExists(atPath: prod.path) {
doesprodexist = true
} else {
print("File does not exist")
}
if fileManager.fileExists(atPath: title.path) {
doestitleexist = true
} else {
print("File does not exist")
}
return (doestitleexist, doesprodexist)
}
}
func getDeveloperNames() -> String {
guard let s = infoDictionary?["CFBundleIdentifier"] as? String else {
return "Unknown"
}
return s
}

23
src/ios/MetalView.swift Normal file
View file

@ -0,0 +1,23 @@
// 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 AppUI
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) {
//
}
}

26
src/ios/NavView.swift Normal file
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 Pomelo, Stossy11
// SPDX-License-Identifier: GPL-3.0-or-later
import SwiftUI
import AppUI
struct NavView: View {
@Binding var core: Core
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
LibraryView(core: $core)
.tabItem { Label("Library", systemImage: "rectangle.on.rectangle") }
.tag(0)
BootOSView(core: $core, currentnavigarion: $selectedTab)
.toolbar(.hidden, for: .tabBar)
.tabItem { Label("Boot OS", systemImage: "house") }
.tag(1)
SettingsView(core: core)
.tabItem { Label("Settings", systemImage: "gear") }
.tag(2)
}
}
}

19
src/ios/PomeloApp.swift Normal file
View file

@ -0,0 +1,19 @@
// 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
infix operator --: LogicalDisjunctionPrecedence
func --(lhs: Bool, rhs: Bool) -> Bool {
return lhs || rhs
}
@main
struct PomeloApp: App {
var body: some Scene {
WindowGroup { ContentView() }
}
}

View file

@ -0,0 +1,18 @@
// 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
struct SettingsView: View {
@State var core: Core
@State var showprompt = false
@AppStorage("icon") var iconused = 1
var body: some View {
NavigationStack {
}
}
}

5
src/ios/VMA.cpp Normal file
View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#define VMA_IMPLEMENTATION
#include "video_core/vulkan_common/vma.h"

View file

@ -50,7 +50,6 @@ if (USE_DISCORD_PRESENCE)
if (YUZU_USE_BUNDLED_OPENSSL)
target_link_libraries(qt_common PUBLIC OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(qt_common PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
endif()
target_compile_definitions(qt_common PUBLIC USE_DISCORD_PRESENCE)

View file

@ -9,7 +9,7 @@
#include <QEventLoop>
#include <boost/algorithm/string/replace.hpp>
#include <httplib.h>
#include "common/httplib.h"
#include <discord_rpc.h>
#include <fmt/format.h>

View file

@ -369,7 +369,6 @@ else()
else()
target_compile_options(video_core PRIVATE $<$<COMPILE_LANGUAGE:C,CXX>:-Werror=conversion>)
endif()
target_compile_options(video_core PRIVATE $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-sign-conversion>)
# xbyak

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
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@ -13,9 +13,14 @@
#ifdef _MSC_VER
#pragma warning( push )
#pragma warning( disable : 4189 )
#elif defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
#endif
#include "vk_mem_alloc.h"
#ifdef _MSC_VER
#pragma warning( pop )
#elif defined(__clang__)
#pragma clang diagnostic pop
#endif

View file

@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later

View file

@ -16,7 +16,7 @@
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
#endif
#include <httplib.h>
#include "common/httplib.h"
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif

View file

@ -369,7 +369,7 @@ if (APPLE)
if (CMAKE_GENERATOR MATCHES "Xcode")
set(_icons "${_dist}/eden.icon")
set_target_properties(eden PROPERTIES
set_target_properties(yuzu PROPERTIES
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME eden
MACOSX_BUNDLE_ICON_FILE eden
# Also force xcode to manage signing for us.

View file

@ -83,7 +83,7 @@ ci_package() {
android-aarch64 android-x86_64 \
solaris-amd64 freebsd-amd64 openbsd-amd64 \
linux-amd64 linux-aarch64 \
macos-universal; do
macos-universal ios-aarch64; do
echo "-- * platform $platform"
case $DISABLED in

View file

@ -151,7 +151,7 @@ mingw-amd64 mingw-arm64
android-aarch64 android-x86_64
solaris-amd64 freebsd-amd64 openbsd-amd64
linux-amd64 linux-aarch64
macos-universal"
macos-universal ios-aarch64"
DISABLED_PLATFORMS="$reply"
fi