Compare commits

...

24 commits

Author SHA1 Message Date
lizzie
7e88092998 fix dynarmic i hope 2026-02-28 06:28:23 +00:00
lizzie
8d99782267 fx 2026-02-28 06:28:23 +00:00
lizzie
93f69afe37 fx 2026-02-28 06:28:22 +00:00
lizzie
f23af26096 fix boost 2026-02-28 06:28:22 +00:00
lizzie
2649b82ff5 fx 2026-02-28 06:28:22 +00:00
lizzie
e7717708f1 fix stuff 2026-02-28 06:28:22 +00:00
lizzie
ab77cb7537 stupid macos 2026-02-28 06:28:22 +00:00
lizzie
94dcf09617 fix1 2026-02-28 06:28:22 +00:00
lizzie
4a065abff2 fx 2026-02-28 06:28:22 +00:00
lizzie
dbd0c65df0 fix spirv-tools 2026-02-28 06:28:22 +00:00
lizzie
94cc8727e8 fixes for ios spirv tools 2026-02-28 06:28:22 +00:00
lizzie
2089d508a2 fix license 2026-02-28 06:28:22 +00:00
lizzie
d001c9bcba fix ffmpeg 2026-02-28 06:28:22 +00:00
lizzie
74a0456a5f fx 2026-02-28 06:28:22 +00:00
lizzie
3f1f3db6d8 fx 2026-02-28 06:28:22 +00:00
lizzie
7f97290516 license 2026-02-28 06:28:22 +00:00
lizzie
a751088abf ios toolchain cmake 2026-02-28 06:28:22 +00:00
lizzie
cfab359439 license 2026-02-28 06:28:22 +00:00
lizzie
56e207210c license headers 2026-02-28 06:28:22 +00:00
lizzie
a89c8ba5fc flatten + cmake 2026-02-28 06:28:22 +00:00
lizzie
70620eb635 flatten 2026-02-28 06:28:22 +00:00
lizzie
9e16918f3c loicense 2026-02-28 06:28:22 +00:00
lizzie
0d80fccbcd modernize #1 2026-02-28 06:28:22 +00:00
lizzie
feaedd02f7 sudachi ios stuff 2026-02-28 06:28:22 +00:00
29 changed files with 3167 additions and 25 deletions

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

@ -0,0 +1,34 @@
#!/bin/sh -ex
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
WORK_DIR="$PWD"
export IOS_SDK="$(xcrun --sdk iphoneos --show-sdk-path)"
[ ! -z "$IOS_SDK" ]
cmake -G Xcode -B build \
-DCMAKE_TOOLCHAIN_FILE="$WORK_DIR/.ci/ios/ios-toolchain.cmake" \
-DPLATFORM=OS64 \
-DDEPLOYMENT_TARGET=16.0 \
-DCOCOA_LIBRARY="$IOS_SDK/System/Library/Frameworks/Cocoa.framework" \
-DCMAKE_C_COMPILER="$(xcrun --sdk iphoneos clang -arch arm64)" \
-DCMAKE_CXX_COMPILER="$(xcrun --sdk iphoneos clang++ -arch arm64)" \
-DENABLE_LIBUSB=OFF \
-DENABLE_UPDATE_CHECKER=OFF \
-DENABLE_QT=OFF \
-DENABLE_OPENSSL=OFF \
-DENABLE_WEB_SERVICE=OFF \
-DENABLE_CUBEB=OFF \
-DYUZU_ROOM=OFF \
-DYUZU_ROOM_STANDALONE=OFF \
-DYUZU_STATIC_ROOM=OFF \
-DYUZU_CMD=OFF \
-DUSE_DISCORD_PRESENCE=OFF \
-DYUZU_USE_EXTERNAL_FFMPEG=ON \
-DYUZU_USE_EXTERNAL_SDL2=ON \
-DCPMUTIL_FORCE_BUNDLED=ON \
-DCMAKE_BUILD_TYPE=Release
cmake --build build

1180
.ci/ios/ios-toolchain.cmake Normal file

File diff suppressed because it is too large Load diff

View file

@ -115,7 +115,7 @@ for file in $FILES; do
*.cmake|*.sh|*CMakeLists.txt)
begin="#"
;;
*.kt*|*.cpp|*.h)
*.kt*|*.cpp|*.h|*.swift|*.mm)
begin="//"
;;
*)
@ -193,7 +193,7 @@ if [ "$UPDATE" = "true" ]; then
begin="#"
shell=true
;;
*.kt*|*.cpp|*.h)
*.kt*|*.cpp|*.h|*.swift|*.mm)
begin="//"
shell="false"
;;

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

@ -212,7 +212,7 @@ 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)
@ -283,7 +283,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")
@ -487,7 +487,12 @@ if (APPLE)
# Umbrella framework for everything GUI-related
find_library(COCOA_LIBRARY Cocoa REQUIRED)
find_library(IOKIT_LIBRARY IOKit REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY})
if (IOS)
find_library(OBJC_LIBRARY objc REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY} ${OBJC_LIBRARY})
else()
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY})
endif()
elseif (WIN32)
# Target Windows 10
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00)

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

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

@ -38,13 +38,18 @@ This file is based off of Yuzu and Dynarmic.
if (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 PARENT_SCOPE)
add_definitions(-DARCHITECTURE_arm64=1)
else ()
# 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()
endif()
return()
endif()

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

@ -110,7 +110,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,22 @@ 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
)
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

@ -255,4 +255,8 @@ if (ANDROID)
target_include_directories(yuzu-android PRIVATE android/app/src/main)
endif()
if (IOS)
add_subdirectory(ios)
endif()
include(GenerateDepHashes)

View file

@ -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>
#endif

View file

@ -69,6 +69,7 @@ add_library(dynarmic STATIC
frontend/decoder/matcher.h
frontend/imm.cpp
frontend/imm.h
interface/halt_reason.h
interface/exclusive_monitor.h
interface/optimization_flags.h
ir/acc_type.h

View file

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUI-Bridging-Header.h - Sudachi
// Created by Jarrod Norwell on 4/3/2024.
//
#ifndef AppUI_Bridging_Header_h
#define AppUI_Bridging_Header_h
#import "Wrapper/AppUIObjC.h"
#endif /* AppUI_Bridging_Header_h */

107
src/ios/AppUI.swift Normal file
View file

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUI.swift - Sudachi
// Created by Jarrod Norwell on 4/3/2024.
//
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,29 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIGameInformation.h - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#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,442 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIGameInformation.mm - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#import <Foundation/Foundation.h>
#import "AppUIGameInformation.h"
#import "EmulationSession/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/yuzu_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/log.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

95
src/ios/AppUIObjC.h Normal file
View file

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIObjC.h - Sudachi
// Created by Jarrod Norwell on 1/8/24.
//
#import <Foundation/Foundation.h>
#import <MetalKit/MetalKit.h>
#import "AppUIGameInformation/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

263
src/ios/AppUIObjC.mm Normal file
View file

@ -0,0 +1,263 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIObjC.mm - Sudachi
// Created by Jarrod Norwell on 1/8/24.
//
#import "AppUIObjC.h"
#import "Config/Config.h"
#import "EmulationSession/EmulationSession.h"
#import "DirectoryManager/DirectoryManager.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/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.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/yuzu_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"
#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.shader_backend.SetValue(Settings::ShaderBackend::SpirV);
// 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((__bridge CA::MetalLayer*)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((__bridge CA::MetalLayer*)layer, size);
}
-(void) settingsChanged {
Config{"config", Config::ConfigType::GlobalConfig};
}
@end

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

@ -0,0 +1,29 @@
# 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
)
target_link_libraries(eden-ios PRIVATE common core input_common frontend_common video_core glad)
target_link_libraries(eden-ios PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
target_link_libraries(eden-ios PRIVATE SDL2::SDL2)
create_target_directory_groups(eden-ios)
target_compile_options(eden-ios PRIVATE
-Wno-conversion
-Wno-unused-variable
-Wno-unused-parameter
-Wno-missing-field-initializers)

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

101
src/ios/EmulationSession.h Normal file
View file

@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationSession.h - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#pragma once
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.hpp>
#import "EmulationWindow/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();
CA::MetalLayer* NativeWindow() const;
void SetNativeWindow(CA::MetalLayer* 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;
CA::MetalLayer* 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;
};

531
src/ios/EmulationSession.mm Normal file
View file

@ -0,0 +1,531 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationSession.m - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#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/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.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/yuzu_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;
}
CA::MetalLayer* EmulationSession::NativeWindow() const {
return m_native_window;
}
void EmulationSession::SetNativeWindow(CA::MetalLayer* 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({
nullptr, // Amiibo Settings
nullptr, // Controller Selector
nullptr, // Error Display
nullptr, // Mii Editor
nullptr, // Parental Controls
nullptr, // Photo Viewer
nullptr, // Profile Selector
nullptr, // std::move(android_keyboard), // Software Keyboard
nullptr, // Web Browser
});
// 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({
nullptr, // Amiibo Settings
nullptr, // Controller Selector
nullptr, // Error Display
nullptr, // Mii Editor
nullptr, // Parental Controls
nullptr, // Photo Viewer
nullptr, // Profile Selector
nullptr, // std::move(android_keyboard), // Software Keyboard
nullptr, // Web Browser
});
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;
}
}
static Core::SystemResultStatus RunEmulation(const std::string& filepath,
const size_t program_index,
const bool frontend_initiated) {
MicroProfileOnThreadCreate("EmuThread");
SCOPE_EXIT {
MicroProfileShutdown();
};
LOG_INFO(Frontend, "starting");
if (filepath.empty()) {
LOG_CRITICAL(Frontend, "failed to load: filepath empty!");
return Core::SystemResultStatus::ErrorLoader;
}
SCOPE_EXIT {
EmulationSession::GetInstance().ShutdownEmulation();
};
jconst result = EmulationSession::GetInstance().InitializeEmulation(filepath, program_index,
frontend_initiated);
if (result != Core::SystemResultStatus::Success) {
return result;
}
EmulationSession::GetInstance().RunEmulation();
return Core::SystemResultStatus::Success;
}
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();
}

83
src/ios/EmulationWindow.h Normal file
View file

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationWindow.h - Sudachi
// Created by Jarrod Norwell on 1/18/24.
//
#pragma once
#import <Metal/Metal.hpp>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.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, CA::MetalLayer* surface, CGSize size,
std::shared_ptr<Common::DynamicLibrary> driver_library);
~EmulationWindow() = default;
void OnSurfaceChanged(CA::MetalLayer* 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,84 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationWindow.mm - Sudachi
// Created by Jarrod Norwell on 1/18/24.
//
#import "EmulationWindow.h"
#import "EmulationSession/EmulationSession.h"
#include <SDL.h>
#include "common/logging/log.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(CA::MetalLayer* 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 = reinterpret_cast<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, CA::MetalLayer* surface, CGSize size, std::shared_ptr<Common::DynamicLibrary> driver_library) : m_input_subsystem{input_subsystem}, m_size{size}, m_driver_library{driver_library} {
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();
}