mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-03 11:13:54 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a72b9d622 | |||
| 1112f12af7 | |||
| ed7c256728 | |||
| bc96de8eb6 | |||
| 417d80fc42 | |||
| 0ea9b231da | |||
| bdcdb14fce | |||
| 9a8a30ba43 | |||
| 59384de777 | |||
| 9b20c6c609 | |||
| 1428abe1d1 | |||
| f069ff50a6 | |||
| ae395352a7 | |||
| 4ed084153f | |||
| 81f396e70b | |||
| 3725b4d46d | |||
| 42b8ca5f9d | |||
| 52ffc8a7ce | |||
| e26bf22d36 | |||
| 42c5a12577 | |||
| a18d2b5c05 | |||
| 30be8a5e18 | |||
| 44e3211d1c | |||
| 52e0b2a005 | |||
| 9a1258c59d | |||
| 232656df4e | |||
| 367e5084da | |||
| 7467506cfa | |||
| 61f3a1d135 | |||
| 4f4f4a3586 | |||
| 8470ad4b2a | |||
| 4b33a2746c |
@@ -1,20 +0,0 @@
|
||||
#!/bin/sh -ex
|
||||
|
||||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
WORK_DIR="$PWD"
|
||||
xcrun --sdk iphoneos --show-sdk-path
|
||||
|
||||
# TODO: support iphonesimulator sdk
|
||||
|
||||
cmake -G Xcode -B build/ios \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=16.0 \
|
||||
-DCMAKE_OSX_SYSROOT=iphoneos \
|
||||
-DCMAKE_SYSTEM_NAME=iOS \
|
||||
-DCMAKE_OSX_ARCHITECTURES="arm64" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DYUZU_USE_EXTERNAL_SDL2=ON \
|
||||
"$@"
|
||||
|
||||
cmake --build build/ios -t eden-ios --config Release
|
||||
@@ -115,7 +115,7 @@ for file in $FILES; do
|
||||
*.cmake|*.sh|*.ps1|*.py|*.rb|*.perl|*.pl|*.nix|*CMakeLists.txt)
|
||||
begin="#"
|
||||
;;
|
||||
*.kt|*.kts|*.cpp|*.h|*.qml|*.c|*.hpp|*.hxx|*.cxx|*.h.in|*.inc|*.mm)
|
||||
*.kt|*.kts|*.cpp|*.h|*.qml|*.c|*.hpp|*.hxx|*.cxx|*.h.in|*.inc)
|
||||
begin="//"
|
||||
;;
|
||||
*)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
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_()
|
||||
{
|
||||
+30
-60
@@ -23,29 +23,6 @@ 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()
|
||||
@@ -67,7 +44,7 @@ if (NETBSD)
|
||||
set(ENV{PKG_CONFIG_PATH} "${PKG_CONFIG_PATH}:${CMAKE_SYSROOT}/usr/pkg/lib/ffmpeg7/pkgconfig")
|
||||
endif()
|
||||
|
||||
cmake_dependent_option(YUZU_STATIC_ROOM "Build a static room executable only (CI only)" OFF "LINUX OR (APPLE AND NOT IOS)" OFF)
|
||||
cmake_dependent_option(YUZU_STATIC_ROOM "Build a static room executable only (CI only)" OFF "LINUX" OFF)
|
||||
if (YUZU_STATIC_ROOM)
|
||||
set(YUZU_ROOM ON)
|
||||
set(YUZU_ROOM_STANDALONE ON)
|
||||
@@ -93,15 +70,9 @@ if (YUZU_STATIC_ROOM)
|
||||
endif()
|
||||
|
||||
# qt stuff
|
||||
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 "Enable the Qt frontend" ON)
|
||||
option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
|
||||
cmake_dependent_option(ENABLE_UPDATE_CHECKER "Enable update checker (for Qt and Android)" OFF "ENABLE_QT OR ANDROID" OFF)
|
||||
option(ENABLE_UPDATE_CHECKER "Enable update checker (for Qt and 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")
|
||||
@@ -247,31 +218,24 @@ cmake_dependent_option(YUZU_USE_BUNDLED_SDL3 "Download bundled SDL3 build" "${MS
|
||||
option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
|
||||
|
||||
set(EXT_DEFAULT OFF)
|
||||
if (MSVC OR ANDROID OR IOS)
|
||||
if (MSVC OR ANDROID)
|
||||
set(EXT_DEFAULT ON)
|
||||
endif()
|
||||
|
||||
# TODO(crueter): Cleanup, each dep that has a bundled option should allow to choose between bundled, external, system
|
||||
cmake_dependent_option(YUZU_USE_EXTERNAL_SDL2 "Build SDL2 from external source" OFF "NOT MSVC;NOT ANDROID" OFF)
|
||||
cmake_dependent_option(YUZU_USE_BUNDLED_SDL2 "Download bundled SDL2 build" "${EXT_DEFAULT}" "NOT ANDROID" OFF)
|
||||
|
||||
# TODO(crueter): did not find header 'AudioHardware.h' in framework 'CoreAudio'
|
||||
cmake_dependent_option(ENABLE_CUBEB "Enables the cubeb audio backend" ON "NOT IOS" OFF)
|
||||
|
||||
# ffmpeg
|
||||
option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT})
|
||||
cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from external source" "${SOLARIS}" "NOT WIN32 AND NOT ANDROID" OFF)
|
||||
|
||||
# sirit
|
||||
set(BUNDLED_SIRIT_DEFAULT OFF)
|
||||
if ((MSVC AND NOT (CMAKE_BUILD_TYPE MATCHES "Deb")) OR ANDROID OR IOS)
|
||||
if (MSVC AND NOT (CMAKE_BUILD_TYPE MATCHES "Deb") OR ANDROID)
|
||||
set(BUNDLED_SIRIT_DEFAULT ON)
|
||||
endif()
|
||||
|
||||
option(YUZU_USE_BUNDLED_SIRIT "Download bundled sirit" ${BUNDLED_SIRIT_DEFAULT})
|
||||
|
||||
# FreeBSD 15+ has libusb, versions below should disable it
|
||||
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR LINUX OR FREEBSD OR (APPLE AND NOT IOS)" OFF)
|
||||
cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR LINUX OR FREEBSD OR APPLE" OFF)
|
||||
|
||||
cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT (WIN32 AND ARCHITECTURE_arm64) AND NOT APPLE" OFF)
|
||||
mark_as_advanced(FORCE ENABLE_OPENGL)
|
||||
@@ -293,10 +257,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 AND NOT IOS" OFF)
|
||||
cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" 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 AND NOT IOS" OFF)
|
||||
cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "NOT ANDROID" OFF)
|
||||
|
||||
cmake_dependent_option(YUZU_CRASH_DUMPS "Compile crash dump (Minidump) support" OFF "WIN32 OR LINUX" OFF)
|
||||
|
||||
@@ -340,7 +304,7 @@ if (YUZU_ROOM)
|
||||
add_compile_definitions(YUZU_ROOM)
|
||||
endif()
|
||||
|
||||
if ((UNIX OR IOS) AND NOT (LINUX OR WIN32))
|
||||
if (UNIX AND NOT (LINUX OR WIN32))
|
||||
if(CXX_APPLE OR CXX_CLANG)
|
||||
# libc++ has stop_token and jthread as experimental
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library")
|
||||
@@ -375,13 +339,26 @@ if (CXX_GCC OR CXX_CLANG)
|
||||
endif()
|
||||
elseif(ARCHITECTURE_arm64)
|
||||
# See https://gcc.gnu.org/onlinedocs/gcc/AArch64-Options.html
|
||||
set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, armv9, native")
|
||||
set(mtune generic)
|
||||
set(armv8_2_target armv8.2-a+fp16+dotprod)
|
||||
|
||||
if (${YUZU_BUILD_PRESET} STREQUAL "generic")
|
||||
set(march armv8-a)
|
||||
elseif (${YUZU_BUILD_PRESET} STREQUAL "armv9")
|
||||
set(march armv9-a)
|
||||
if (ANDROID)
|
||||
set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, armv8.2, armv9, native")
|
||||
set(march ${armv8_2_target})
|
||||
|
||||
if (${YUZU_BUILD_PRESET} STREQUAL "armv9")
|
||||
set(march armv9-a)
|
||||
endif()
|
||||
else()
|
||||
set(YUZU_BUILD_PRESET "custom" CACHE STRING "Build preset to use. One of: custom, generic, armv8.2, armv9, native")
|
||||
|
||||
if (${YUZU_BUILD_PRESET} STREQUAL "generic")
|
||||
set(march armv8-a)
|
||||
elseif (${YUZU_BUILD_PRESET} STREQUAL "armv8.2")
|
||||
set(march ${armv8_2_target})
|
||||
elseif (${YUZU_BUILD_PRESET} STREQUAL "armv9")
|
||||
set(march armv9-a)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -412,10 +389,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
find_package(RenderDoc MODULE QUIET)
|
||||
if (NOT RenderDoc_FOUND)
|
||||
message(WARNING "RenderDoc not found. Some debugging features may be disabled.")
|
||||
endif()
|
||||
find_package(RenderDoc MODULE)
|
||||
|
||||
# openssl funniness
|
||||
if (YUZU_USE_BUNDLED_OPENSSL)
|
||||
@@ -547,13 +521,9 @@ endfunction()
|
||||
# Platform-specific library requirements
|
||||
# Put these BEFORE EXTERNALS or Boost WILL die
|
||||
# =============================================
|
||||
|
||||
if (APPLE)
|
||||
if (IOS)
|
||||
set(_libs Metal IOKit CoreVideo CoreMedia Security UniformTypeIdentifiers)
|
||||
else()
|
||||
set(_libs Carbon Metal Cocoa IOKit CoreVideo CoreMedia Security UniformTypeIdentifiers)
|
||||
endif()
|
||||
foreach(fw ${_libs})
|
||||
foreach(fw Carbon Metal Cocoa IOKit CoreVideo CoreMedia Security UniformTypeIdentifiers)
|
||||
find_library(${fw}_LIBRARY ${fw} REQUIRED)
|
||||
list(APPEND PLATFORM_LIBRARIES ${${fw}_LIBRARY})
|
||||
endforeach()
|
||||
|
||||
+7
-2
@@ -12,8 +12,7 @@
|
||||
"min_version": "1.57",
|
||||
"package": "Boost",
|
||||
"patches": [
|
||||
"0001-clang-cl.patch",
|
||||
"0002-ios-fix.patch"
|
||||
"0001-clang-cl.patch"
|
||||
],
|
||||
"repo": "boostorg/boost",
|
||||
"version": "boost-1.90.0"
|
||||
@@ -66,6 +65,12 @@
|
||||
"repo": "eden-emulator/discord-rpc",
|
||||
"version": "0d8b2d6a37"
|
||||
},
|
||||
"dxbc": {
|
||||
"bundled": true,
|
||||
"hash": "196d26c07747d7aa2ced6fb1a5ae6f665e9b917de223a540038690b037c70a1eeca51c21d42aa5bec9fff7df0a6656a75ee8b4e2cb4008469f18a6812280f831",
|
||||
"repo": "PancakeTAS/dxbc",
|
||||
"version": "78ab59a8aaeb43cd1b0a5e91ba86722433a10b78"
|
||||
},
|
||||
"enet": {
|
||||
"find_args": "MODULE",
|
||||
"hash": "a0d2fa8c957704dd49e00a726284ac5ca034b50b00d2b20a94fa1bbfbb80841467834bfdc84aa0ed0d6aab894608fd6c86c3b94eee46343f0e6d9c22e391dbf9",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
- [macOS](#macos)
|
||||
- [OpenIndiana](#openindiana)
|
||||
- [OmniOS](#omnios)
|
||||
- [iOS](#ios)
|
||||
- [HaikuOS](#haikuos)
|
||||
- [OpenBSD](#openbsd)
|
||||
- [FreeBSD](#freebsd)
|
||||
@@ -93,16 +92,6 @@ You may also need to install `gmake` in order to properly build FFmpeg, this is
|
||||
|
||||
If it wasn't obvious already, you require a X11 server to properly run the emulator within OmniOS, [this guide](https://web.archive.org/web/20260424200928/https://geekblood.wordpress.com/2017/10/26/installing-x11-and-a-desktop-environment-on-omnios/) is a great starting point for that, the links to pkgsrc are outdated so follow [this exemplar](https://pkgsrc.smartos.org/install-on-illumos/) as well:
|
||||
|
||||
## iOS
|
||||
|
||||
iOS has a dedicated build script, we **highly** recommend using that instead of doing anything else, we don't support any other configuration than the one present in said build script.
|
||||
|
||||
To build, it's simply as easy as doing
|
||||
```sh
|
||||
chmod +x .ci/ios/build.sh
|
||||
.ci/ios/build.sh
|
||||
```
|
||||
|
||||
## HaikuOS
|
||||
|
||||
It's recommended to do a `pkgman full-sync` before installing. See [HaikuOS: Installing applications](https://www.haiku-os.org/guides/daily-tasks/install-applications/). Sometimes the process may be interrupted by an error like "Interrupted syscall". Simply firing the command again fixes the issue. By default `g++` is included on the default installation.
|
||||
|
||||
Vendored
+4
-6
@@ -92,6 +92,10 @@ AddDependentPackages(vulkan-headers vulkan-utility-libraries)
|
||||
# frozen
|
||||
AddJsonPackage(frozen)
|
||||
|
||||
# DXVK's DXBC compiler, used to translate the frame generation shaders
|
||||
# out of a user-supplied Lossless.dll into SPIR-V
|
||||
AddJsonPackage(dxbc)
|
||||
|
||||
# DiscordRPC
|
||||
if (USE_DISCORD_PRESENCE)
|
||||
if (ARCHITECTURE_arm64)
|
||||
@@ -233,12 +237,6 @@ if (VulkanMemoryAllocator_ADDED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# httplib
|
||||
if (IOS)
|
||||
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF)
|
||||
endif()
|
||||
AddJsonPackage(httplib)
|
||||
|
||||
# cpp-jwt
|
||||
if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
|
||||
AddJsonPackage(cpp-jwt)
|
||||
|
||||
+9
-14
@@ -35,21 +35,16 @@ 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 (DEFINED CMAKE_OSX_ARCHITECTURES)
|
||||
if (CMAKE_OSX_ARCHITECTURES)
|
||||
set(MULTIARCH_BUILD 1)
|
||||
set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}")
|
||||
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()
|
||||
|
||||
# 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()
|
||||
|
||||
return()
|
||||
endif()
|
||||
|
||||
@@ -223,4 +218,4 @@ if (NOT DEFINED ARCHITECTURE)
|
||||
add_definitions(-DARCHITECTURE_GENERIC=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
|
||||
message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
|
||||
@@ -54,12 +54,6 @@ 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)
|
||||
|
||||
Vendored
+1
-17
@@ -31,22 +31,6 @@ if (NOT YUZU_USE_BUNDLED_FFMPEG)
|
||||
--extra-ldflags="-nostdlib"
|
||||
)
|
||||
set(FFmpeg_IS_CROSS_COMPILING TRUE)
|
||||
elseif(IOS)
|
||||
execute_process(COMMAND xcrun --sdk iphoneos --show-sdk-path OUTPUT_VARIABLE SYSROOT)
|
||||
# Lovely extra newline apple adds that **we** must remove... thank you apple!
|
||||
string(STRIP "${SYSROOT}" SYSROOT)
|
||||
set(FFmpeg_CC xcrun --sdk iphoneos clang -arch arm64)
|
||||
set(FFmpeg_CXX xcrun --sdk iphoneos clang++ -arch arm64)
|
||||
list(APPEND FFmpeg_CROSS_COMPILE_FLAGS
|
||||
--arch=arm64
|
||||
--enable-cross-compile
|
||||
--sysroot="${SYSROOT}"
|
||||
--extra-ldflags="-miphoneos-version-min=16.0"
|
||||
--install-name-dir='@rpath'
|
||||
--disable-audiotoolbox
|
||||
--disable-videotoolbox
|
||||
)
|
||||
set(FFmpeg_IS_CROSS_COMPILING TRUE)
|
||||
# User attempts to do a FFmpeg cross compilation because...
|
||||
# Here we just quickly test against host/system processors not matching
|
||||
# TODO: Test for versions not matching as well?
|
||||
@@ -80,7 +64,7 @@ elseif (ANDROID)
|
||||
--enable-mediacodec
|
||||
--enable-jni
|
||||
)
|
||||
elseif (UNIX AND NOT DEFINED FFmpeg_IS_CROSS_COMPILING AND NOT ANDROID AND NOT IOS)
|
||||
elseif (UNIX AND NOT DEFINED FFmpeg_IS_CROSS_COMPILING AND NOT ANDROID)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBVA libva)
|
||||
pkg_check_modules(CUDA cuda)
|
||||
|
||||
Vendored
+1
-2
@@ -24,8 +24,7 @@ if (MINGW OR LINUX OR APPLE)
|
||||
message(FATAL_ERROR "Required program `autoconf` not found.")
|
||||
endif()
|
||||
|
||||
find_program(LIBTOOLIZE
|
||||
NAMES libtoolize glibtoolize)
|
||||
find_program(LIBTOOLIZE libtoolize)
|
||||
if ("${LIBTOOLIZE}" STREQUAL "LIBTOOLIZE-NOTFOUND")
|
||||
message(FATAL_ERROR "Required program `libtoolize` not found.")
|
||||
endif()
|
||||
|
||||
@@ -177,9 +177,6 @@ else()
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-invalid-offsetof>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-unused-parameter>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-missing-field-initializers>)
|
||||
if (NOT IOS)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Werror=missing-declarations>)
|
||||
endif()
|
||||
|
||||
if (CXX_CLANG OR CXX_ICC OR CXX_APPLE) # Clang, AppleClang, or Intel C++
|
||||
if (NOT MSVC)
|
||||
@@ -298,9 +295,4 @@ if (ANDROID)
|
||||
target_include_directories(yuzu-android PRIVATE android/app/src/main)
|
||||
endif()
|
||||
|
||||
if (IOS)
|
||||
add_subdirectory(ios)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error>)
|
||||
endif()
|
||||
|
||||
include(GenerateDepHashes)
|
||||
|
||||
@@ -539,6 +539,40 @@ object NativeLibrary {
|
||||
*/
|
||||
external fun installKeys(path: String, ext: String): Int
|
||||
|
||||
/**
|
||||
* @return Whether this GPU can run the Lossless Scaling frame generation shaders,
|
||||
* which are built against the Vulkan memory model.
|
||||
*/
|
||||
external fun supportsFrameGeneration(): Boolean
|
||||
|
||||
/**
|
||||
* @return Path the user-supplied Lossless Scaling library is expected at.
|
||||
*/
|
||||
external fun getLosslessDllPath(): String
|
||||
|
||||
/**
|
||||
* Parses the installed Lossless Scaling library and checks that every shader the
|
||||
* frame generation chain needs is present.
|
||||
*
|
||||
* @return The result code, matching the losslessDllResults array.
|
||||
*/
|
||||
external fun validateLosslessDll(): Int
|
||||
|
||||
/**
|
||||
* Translates the frame generation shaders out of the installed Lossless Scaling library
|
||||
* and writes them to the SPIR-V cache. Slow, so call it off the main thread.
|
||||
*
|
||||
* @return The result code, matching the losslessDllResults array.
|
||||
*/
|
||||
external fun prepareLosslessDll(): Int
|
||||
|
||||
/**
|
||||
* Deletes the installed Lossless Scaling library.
|
||||
*
|
||||
* @return Whether the library is gone after the call.
|
||||
*/
|
||||
external fun removeLosslessDll(): Boolean
|
||||
|
||||
/**
|
||||
* Checks the PatchManager for any addons that are available
|
||||
*
|
||||
|
||||
+4
@@ -37,6 +37,10 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
||||
RENDERER_PATCH_OLD_QCOM_DRIVERS("patch_old_qcom_drivers"),
|
||||
RENDERER_VERTEX_INPUT_DYNAMIC_STATE("vertex_input_dynamic_state"),
|
||||
RENDERER_SAMPLE_SHADING("sample_shading"),
|
||||
RENDERER_FRAME_GEN("frame_gen"),
|
||||
RENDERER_FRAME_GEN_FP16("frame_gen_fp16"),
|
||||
RENDERER_FRAME_GEN_FLOW_SCALE_AUTO("frame_gen_flow_scale_auto"),
|
||||
RENDERER_FRAME_GEN_DUMP_FLOW("frame_gen_dump_flow"),
|
||||
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
|
||||
PICTURE_IN_PICTURE("picture_in_picture"),
|
||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||
|
||||
@@ -19,6 +19,10 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
|
||||
RENDERER_ASTC_DECODE_METHOD("accelerate_astc"),
|
||||
RENDERER_ACCURACY("gpu_accuracy"),
|
||||
RENDERER_RESOLUTION("resolution_setup"),
|
||||
RENDERER_FRAME_GEN_MULTIPLIER("frame_gen_multiplier"),
|
||||
RENDERER_FRAME_GEN_TARGET_RATE("frame_gen_target_rate"),
|
||||
RENDERER_FRAME_GEN_QUEUE_TARGET("frame_gen_queue_target"),
|
||||
RENDERER_FRAME_GEN_FLOW_SCALE("frame_gen_flow_scale"),
|
||||
RENDERER_VSYNC("use_vsync"),
|
||||
RENDERER_SCALING_FILTER("scaling_filter"),
|
||||
RENDERER_ANTI_ALIASING("anti_aliasing"),
|
||||
|
||||
@@ -11,6 +11,7 @@ object Settings {
|
||||
SECTION_ROOT(R.string.advanced_settings),
|
||||
SECTION_SYSTEM(R.string.preferences_system),
|
||||
SECTION_RENDERER(R.string.preferences_graphics),
|
||||
SECTION_FRAME_GEN(R.string.frame_gen),
|
||||
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
|
||||
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
|
||||
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
|
||||
|
||||
+103
@@ -21,6 +21,7 @@ import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import org.yuzu.yuzu_emu.network.NetDataValidators
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
|
||||
/**
|
||||
@@ -65,6 +66,19 @@ abstract class SettingsItem(
|
||||
return NativeLibrary.isFirmwareAvailable()
|
||||
}
|
||||
|
||||
if (setting.key in frameGenKeys &&
|
||||
!(LosslessScalingHelper.isInstalled() && LosslessScalingHelper.isSupportedByGpu())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// A frame rate target moves the multiplier on its own
|
||||
if (setting.key == IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key &&
|
||||
frameGenTargetRate != 0
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Can't edit settings that aren't saveable in per-game config even if they are switchable
|
||||
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
|
||||
return false
|
||||
@@ -88,7 +102,31 @@ abstract class SettingsItem(
|
||||
val clearable: Boolean
|
||||
get() = !setting.global && NativeConfig.isPerGameConfigLoaded()
|
||||
|
||||
private val frameGenTargetRate: Int
|
||||
get() {
|
||||
val key = IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key
|
||||
val needsGlobal = if (NativeLibrary.isRunning() &&
|
||||
!NativeConfig.isPerGameConfigLoaded()
|
||||
) {
|
||||
!NativeConfig.usingGlobal(key)
|
||||
} else {
|
||||
NativeConfig.usingGlobal(key)
|
||||
}
|
||||
return IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.getInt(needsGlobal)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val frameGenKeys = setOf(
|
||||
BooleanSetting.RENDERER_FRAME_GEN.key,
|
||||
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key,
|
||||
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key,
|
||||
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key,
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key,
|
||||
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key,
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FP16.key,
|
||||
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key
|
||||
)
|
||||
|
||||
const val TYPE_HEADER = 0
|
||||
const val TYPE_SWITCH = 1
|
||||
const val TYPE_SINGLE_CHOICE = 2
|
||||
@@ -607,6 +645,71 @@ abstract class SettingsItem(
|
||||
valuesId = R.array.rendererAntiAliasingValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_FRAME_GEN,
|
||||
titleId = R.string.frame_gen,
|
||||
descriptionId = R.string.frame_gen_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.RENDERER_FRAME_GEN_MULTIPLIER,
|
||||
titleId = R.string.frame_gen_multiplier,
|
||||
descriptionId = R.string.frame_gen_multiplier_description,
|
||||
choicesId = R.array.frameGenMultiplierNames,
|
||||
valuesId = R.array.frameGenMultiplierValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.RENDERER_FRAME_GEN_TARGET_RATE,
|
||||
titleId = R.string.frame_gen_target_rate,
|
||||
descriptionId = R.string.frame_gen_target_rate_description,
|
||||
choicesId = R.array.frameGenTargetRateNames,
|
||||
valuesId = R.array.frameGenTargetRateValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET,
|
||||
titleId = R.string.frame_gen_queue_target,
|
||||
descriptionId = R.string.frame_gen_queue_target_description,
|
||||
choicesId = R.array.frameGenQueueTargetNames,
|
||||
valuesId = R.array.frameGenQueueTargetValues
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO,
|
||||
titleId = R.string.frame_gen_flow_scale_auto,
|
||||
descriptionId = R.string.frame_gen_flow_scale_auto_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SliderSetting(
|
||||
IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE,
|
||||
titleId = R.string.frame_gen_flow_scale,
|
||||
descriptionId = R.string.frame_gen_flow_scale_description,
|
||||
min = 25,
|
||||
max = 100,
|
||||
units = "%"
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_FRAME_GEN_FP16,
|
||||
titleId = R.string.frame_gen_fp16,
|
||||
descriptionId = R.string.frame_gen_fp16_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SwitchSetting(
|
||||
BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW,
|
||||
titleId = R.string.frame_gen_dump_flow,
|
||||
descriptionId = R.string.frame_gen_dump_flow_description
|
||||
)
|
||||
)
|
||||
put(
|
||||
SingleChoiceSetting(
|
||||
IntSetting.RENDERER_SCREEN_LAYOUT,
|
||||
|
||||
+3
-1
@@ -382,7 +382,9 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
|
||||
}
|
||||
scSetting.setSelectedValue(value)
|
||||
|
||||
if (scSetting.setting.key == IntSetting.RENDERER_SCALING_FILTER.key) {
|
||||
if (scSetting.setting.key == IntSetting.RENDERER_SCALING_FILTER.key ||
|
||||
scSetting.setting.key == IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key
|
||||
) {
|
||||
settingsViewModel.setShouldReloadSettingsList(true)
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -27,6 +27,7 @@ import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.*
|
||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.FullscreenHelper
|
||||
@@ -76,6 +77,41 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFrameGenSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.apply {
|
||||
if (!LosslessScalingHelper.isSupportedByGpu()) {
|
||||
add(
|
||||
RunnableSetting(
|
||||
titleId = R.string.frame_gen_unsupported,
|
||||
descriptionId = R.string.frame_gen_unsupported_description,
|
||||
isRunnable = false
|
||||
) {}
|
||||
)
|
||||
} else if (!LosslessScalingHelper.isInstalled()) {
|
||||
add(
|
||||
RunnableSetting(
|
||||
titleId = R.string.lossless_scaling_missing,
|
||||
descriptionId = R.string.lossless_scaling_missing_description,
|
||||
isRunnable = false
|
||||
) {}
|
||||
)
|
||||
}
|
||||
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN.key)
|
||||
add(IntSetting.RENDERER_FRAME_GEN_TARGET_RATE.key)
|
||||
add(IntSetting.RENDERER_FRAME_GEN_MULTIPLIER.key)
|
||||
add(IntSetting.RENDERER_FRAME_GEN_QUEUE_TARGET.key)
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
|
||||
if (!BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.getBoolean(
|
||||
getNeedsGlobalForKey(BooleanSetting.RENDERER_FRAME_GEN_FLOW_SCALE_AUTO.key)
|
||||
)
|
||||
) {
|
||||
add(IntSetting.RENDERER_FRAME_GEN_FLOW_SCALE.key)
|
||||
}
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN_FP16.key)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSharpnessScalingFilterSelected(): Boolean {
|
||||
val needsGlobal = getNeedsGlobalForKey(IntSetting.RENDERER_SCALING_FILTER.key)
|
||||
val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal)
|
||||
@@ -120,6 +156,7 @@ class SettingsFragmentPresenter(
|
||||
MenuTag.SECTION_ROOT -> addConfigSettings(sl)
|
||||
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
|
||||
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
|
||||
MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
|
||||
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
|
||||
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
|
||||
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
|
||||
@@ -302,7 +339,6 @@ class SettingsFragmentPresenter(
|
||||
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
|
||||
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
|
||||
add(BooleanSetting.EMULATE_BGR565.key)
|
||||
add(BooleanSetting.RESCALE_HACK.key)
|
||||
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
|
||||
add(IntSetting.ANDROID_PIPELINE_WORKERS.key)
|
||||
add(BooleanSetting.RENDERER_ASYNCHRONOUS_GPU_EMULATION.key)
|
||||
@@ -1296,6 +1332,7 @@ class SettingsFragmentPresenter(
|
||||
add(BooleanSetting.DUMP_GUEST_SHADERS.key)
|
||||
add(BooleanSetting.GPU_LOG_SHADER_DUMPS.key)
|
||||
add(BooleanSetting.DUMP_MACROS.key)
|
||||
add(BooleanSetting.RENDERER_FRAME_GEN_DUMP_FLOW.key)
|
||||
add(BooleanSetting.GPU_LOG_MEMORY_TRACKING.key)
|
||||
add(BooleanSetting.GPU_LOG_DRIVER_DEBUG.key)
|
||||
add(IntSetting.GPU_LOG_RING_BUFFER_SIZE.key)
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ enum class SettingsSubscreen {
|
||||
DRIVER_MANAGER,
|
||||
DRIVER_FETCHER,
|
||||
FREEDRENO_SETTINGS,
|
||||
LOSSLESS_MANAGER,
|
||||
APPLET_LAUNCHER,
|
||||
INSTALLABLE,
|
||||
GAME_FOLDERS,
|
||||
@@ -126,6 +127,7 @@ class SettingsSubscreenActivity : AppCompatActivity() {
|
||||
SettingsSubscreen.DRIVER_MANAGER -> R.id.driverManagerFragment
|
||||
SettingsSubscreen.DRIVER_FETCHER -> R.id.driverFetcherFragment
|
||||
SettingsSubscreen.FREEDRENO_SETTINGS -> R.id.freedrenoSettingsFragment
|
||||
SettingsSubscreen.LOSSLESS_MANAGER -> R.id.losslessManagerFragment
|
||||
SettingsSubscreen.APPLET_LAUNCHER -> R.id.appletLauncherFragment
|
||||
SettingsSubscreen.INSTALLABLE -> R.id.installableFragment
|
||||
SettingsSubscreen.GAME_FOLDERS -> R.id.gameFoldersFragment
|
||||
|
||||
@@ -369,6 +369,21 @@ class GamePropertiesFragment : Fragment() {
|
||||
)
|
||||
)
|
||||
}
|
||||
add(
|
||||
SubmenuProperty(
|
||||
R.string.frame_gen,
|
||||
R.string.frame_gen_per_game_description,
|
||||
R.drawable.ic_duck,
|
||||
action = {
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
args.game,
|
||||
Settings.MenuTag.SECTION_FRAME_GEN
|
||||
)
|
||||
binding.root.findNavController().navigate(action)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (GpuDriverHelper.isAdrenoGpu()) {
|
||||
add(
|
||||
SubmenuProperty(
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.FileUtil
|
||||
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
|
||||
|
||||
class HomeSettingsFragment : Fragment() {
|
||||
@@ -170,6 +171,24 @@ class HomeSettingsFragment : Fragment() {
|
||||
)
|
||||
)
|
||||
}
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.lossless_scaling,
|
||||
R.string.lossless_scaling_description,
|
||||
R.drawable.ic_duck,
|
||||
{
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity(
|
||||
SettingsSubscreen.LOSSLESS_MANAGER,
|
||||
null
|
||||
)
|
||||
binding.root.findNavController().navigate(action)
|
||||
},
|
||||
{ true },
|
||||
0,
|
||||
0,
|
||||
LosslessScalingHelper.statusText
|
||||
)
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.multiplayer,
|
||||
@@ -337,6 +356,7 @@ class HomeSettingsFragment : Fragment() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
driverViewModel.updateDriverNameForGame(null)
|
||||
LosslessScalingHelper.refreshStatus()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
|
||||
import org.yuzu.yuzu_emu.databinding.FragmentLosslessManagerBinding
|
||||
import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration
|
||||
import org.yuzu.yuzu_emu.model.HomeSetting
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins
|
||||
import org.yuzu.yuzu_emu.utils.collect
|
||||
|
||||
class LosslessManagerFragment : Fragment() {
|
||||
private var _binding: FragmentLosslessManagerBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = FragmentLosslessManagerBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.toolbarLossless.setNavigationOnClickListener {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
|
||||
binding.losslessOptionsList.apply {
|
||||
layoutManager =
|
||||
GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
|
||||
addItemDecoration(
|
||||
SpacingItemDecoration(resources.getDimensionPixelSize(R.dimen.spacing_small))
|
||||
)
|
||||
}
|
||||
|
||||
LosslessScalingHelper.statusText.collect(viewLifecycleOwner) { refreshOptions() }
|
||||
|
||||
setInsets()
|
||||
}
|
||||
|
||||
private fun refreshOptions() {
|
||||
binding.losslessOptionsList.adapter = HomeSettingAdapter(
|
||||
requireActivity() as AppCompatActivity,
|
||||
viewLifecycleOwner,
|
||||
buildOptions()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildOptions(): List<HomeSetting> {
|
||||
val installed = LosslessScalingHelper.isInstalled()
|
||||
return listOf(
|
||||
HomeSetting(
|
||||
if (installed) R.string.lossless_scaling_replace else R.string.lossless_scaling_install,
|
||||
if (installed) {
|
||||
R.string.lossless_scaling_replace_description
|
||||
} else {
|
||||
R.string.lossless_scaling_install_description
|
||||
},
|
||||
R.drawable.ic_install,
|
||||
{ dllPickerLauncher.launch(arrayOf("*/*")) },
|
||||
{ !NativeLibrary.isRunning() },
|
||||
R.string.lossless_scaling_locked,
|
||||
R.string.lossless_scaling_locked_description,
|
||||
LosslessScalingHelper.statusText
|
||||
),
|
||||
HomeSetting(
|
||||
R.string.lossless_scaling_remove,
|
||||
R.string.lossless_scaling_remove_description,
|
||||
R.drawable.ic_delete,
|
||||
{ confirmRemoval() },
|
||||
{ installed && !NativeLibrary.isRunning() },
|
||||
if (installed) {
|
||||
R.string.lossless_scaling_locked
|
||||
} else {
|
||||
R.string.lossless_scaling_remove_unavailable
|
||||
},
|
||||
if (installed) {
|
||||
R.string.lossless_scaling_locked_description
|
||||
} else {
|
||||
R.string.lossless_scaling_remove_unavailable_description
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun confirmRemoval() {
|
||||
MessageDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
titleId = R.string.lossless_scaling_remove,
|
||||
descriptionId = R.string.lossless_scaling_remove_confirmation,
|
||||
positiveButtonTitleId = R.string.lossless_scaling_remove,
|
||||
positiveAction = { LosslessScalingHelper.remove() },
|
||||
showNegativeButton = true,
|
||||
negativeAction = {}
|
||||
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
|
||||
private val dllPickerLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val resultStrings = resources.getStringArray(R.array.losslessDllResults)
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.lossless_scaling_installing,
|
||||
false
|
||||
) { _, _ ->
|
||||
val installResult = LosslessScalingHelper.install(result)
|
||||
if (installResult == LosslessScalingHelper.RESULT_OK) {
|
||||
getString(R.string.lossless_scaling_install_success)
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.lossless_scaling_install_failed,
|
||||
descriptionString = resultStrings[installResult]
|
||||
)
|
||||
}
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private fun setInsets() =
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
|
||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||
|
||||
binding.appbarLossless.updateMargins(
|
||||
left = barInsets.left + cutoutInsets.left,
|
||||
right = barInsets.right + cutoutInsets.right
|
||||
)
|
||||
|
||||
binding.scrollViewLossless.updatePadding(bottom = barInsets.bottom)
|
||||
|
||||
binding.losslessOptionsList.updatePadding(
|
||||
left = barInsets.left + cutoutInsets.left,
|
||||
right = barInsets.right + cutoutInsets.right
|
||||
)
|
||||
|
||||
windowInsets
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
|
||||
import org.yuzu.yuzu_emu.model.PageState
|
||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
@@ -202,6 +203,24 @@ class SetupFragment : Fragment() {
|
||||
R.string.install_firmware_warning_help,
|
||||
)
|
||||
)
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_duck,
|
||||
R.string.lossless_scaling,
|
||||
R.string.lossless_scaling_setup_description,
|
||||
{
|
||||
pageButtonCallback = it
|
||||
getLosslessDll.launch(arrayOf("*/*"))
|
||||
},
|
||||
{
|
||||
if (LosslessScalingHelper.isInstalled()) {
|
||||
ButtonState.BUTTON_ACTION_COMPLETE
|
||||
} else {
|
||||
ButtonState.BUTTON_ACTION_INCOMPLETE
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
PageButton(
|
||||
R.drawable.ic_controller,
|
||||
@@ -446,6 +465,32 @@ class SetupFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
val getLosslessDll =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
val resultStrings = resources.getStringArray(R.array.losslessDllResults)
|
||||
ProgressDialogFragment.newInstance(
|
||||
requireActivity(),
|
||||
R.string.lossless_scaling_installing,
|
||||
false
|
||||
) { _, _ ->
|
||||
val installResult = LosslessScalingHelper.install(result)
|
||||
if (installResult == LosslessScalingHelper.RESULT_OK) {
|
||||
getString(R.string.lossless_scaling_install_success)
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
titleId = R.string.lossless_scaling_install_failed,
|
||||
descriptionString = resultStrings[installResult]
|
||||
)
|
||||
}
|
||||
}.apply {
|
||||
onDialogComplete = { checkForButtonState.invoke() }
|
||||
}.show(parentFragmentManager, ProgressDialogFragment.TAG)
|
||||
}
|
||||
|
||||
val getGamesDirectory =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||
if (result != null) {
|
||||
|
||||
@@ -72,6 +72,18 @@ class SystemInfoDialogFragment : DialogFragment() {
|
||||
|
||||
val vulkanDriver = NativeLibrary.getVulkanDriverVersion()
|
||||
appendLine("${getString(R.string.vulkan_driver_version)}: $vulkanDriver")
|
||||
|
||||
val frameGen = NativeLibrary.supportsFrameGeneration()
|
||||
appendLine(
|
||||
"${getString(R.string.frame_generation_support)}: " +
|
||||
getString(
|
||||
if (frameGen) {
|
||||
R.string.frame_generation_supported
|
||||
} else {
|
||||
R.string.frame_generation_unsupported
|
||||
}
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
appendLine("${getString(R.string.error_getting_emulator_info)}: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import android.net.Uri
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.yuzu.yuzu_emu.NativeLibrary
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.YuzuApplication
|
||||
import java.io.File
|
||||
|
||||
object LosslessScalingHelper {
|
||||
const val RESULT_OK = 0
|
||||
const val RESULT_NOT_INSTALLED = 1
|
||||
|
||||
private val _statusText = MutableStateFlow("")
|
||||
val statusText: StateFlow<String> = _statusText.asStateFlow()
|
||||
|
||||
private var installed: Boolean? = null
|
||||
private var gpuSupported: Boolean? = null
|
||||
|
||||
fun isInstalled(): Boolean = installed ?: refreshStatus()
|
||||
|
||||
fun isSupportedByGpu(): Boolean {
|
||||
val cached = gpuSupported
|
||||
if (cached != null) {
|
||||
return cached
|
||||
}
|
||||
val result = NativeLibrary.supportsFrameGeneration()
|
||||
gpuSupported = result
|
||||
return result
|
||||
}
|
||||
|
||||
fun refreshStatus(): Boolean {
|
||||
val result = NativeLibrary.validateLosslessDll() == RESULT_OK
|
||||
installed = result
|
||||
|
||||
val context = YuzuApplication.appContext
|
||||
_statusText.value = if (result) {
|
||||
context.getString(R.string.lossless_scaling_installed)
|
||||
} else {
|
||||
context.getString(R.string.lossless_scaling_not_installed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun install(source: Uri): Int {
|
||||
val destination = File(NativeLibrary.getLosslessDllPath())
|
||||
destination.parentFile?.mkdirs()
|
||||
|
||||
val copied = FileUtil.copyUriToInternalStorage(
|
||||
source,
|
||||
destination.parent!!,
|
||||
destination.name
|
||||
)
|
||||
if (copied == null) {
|
||||
refreshStatus()
|
||||
return RESULT_NOT_INSTALLED
|
||||
}
|
||||
|
||||
val result = NativeLibrary.prepareLosslessDll()
|
||||
if (result != RESULT_OK) {
|
||||
NativeLibrary.removeLosslessDll()
|
||||
}
|
||||
refreshStatus()
|
||||
return result
|
||||
}
|
||||
|
||||
fun remove(): Boolean {
|
||||
val removed = NativeLibrary.removeLosslessDll()
|
||||
refreshStatus()
|
||||
return removed
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,8 @@ void AndroidConfig::SaveAllValues() {
|
||||
}
|
||||
|
||||
void AndroidConfig::ReadAndroidValues() {
|
||||
ReadAndroidUIValues();
|
||||
if (global) {
|
||||
ReadAndroidUIValues();
|
||||
ReadUIValues();
|
||||
BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));
|
||||
Settings::values.ext_content_from_game_dirs = ReadBooleanSetting(
|
||||
@@ -223,8 +223,8 @@ void AndroidConfig::ReadAndroidControlValues() {
|
||||
}
|
||||
|
||||
void AndroidConfig::SaveAndroidValues() {
|
||||
SaveAndroidUIValues();
|
||||
if (global) {
|
||||
SaveAndroidUIValues();
|
||||
SaveUIValues();
|
||||
SaveOverlayValues();
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ namespace AndroidSettings {
|
||||
&show_performance_overlay};
|
||||
|
||||
|
||||
Settings::Setting<s32> pipeline_worker_count{linkage, 4, "pipeline_worker_count",
|
||||
Settings::SwitchableSetting<s32> pipeline_worker_count{linkage, 2, "pipeline_worker_count",
|
||||
Settings::Category::Android,
|
||||
Settings::Specialization::Default,
|
||||
true,
|
||||
|
||||
@@ -124,9 +124,18 @@ float EmuWindow_Android::GetFrameTimeVerifiedHint() const {
|
||||
return QuantizeFrameRateHint(verified_rate);
|
||||
}
|
||||
|
||||
float EmuWindow_Android::GetPresentedFrameMultiplier() {
|
||||
if (!Settings::values.frame_gen.GetValue()) {
|
||||
return 1.0f;
|
||||
}
|
||||
return static_cast<float>(std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(), 2, 4));
|
||||
}
|
||||
|
||||
float EmuWindow_Android::GetFrameRateHint() const {
|
||||
const float observed_rate = std::clamp(m_smoothed_present_rate, 0.0f, 240.0f);
|
||||
const float frame_time_verified_hint = GetFrameTimeVerifiedHint();
|
||||
const float presented_multiplier = GetPresentedFrameMultiplier();
|
||||
const float observed_rate =
|
||||
std::clamp(m_smoothed_present_rate * presented_multiplier, 0.0f, 240.0f);
|
||||
const float frame_time_verified_hint = GetFrameTimeVerifiedHint() * presented_multiplier;
|
||||
|
||||
if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) {
|
||||
const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f);
|
||||
@@ -150,9 +159,9 @@ float EmuWindow_Android::GetFrameRateHint() const {
|
||||
return frame_time_verified_hint;
|
||||
}
|
||||
|
||||
constexpr float NominalFrameRate = 60.0f;
|
||||
const float nominal_rate = 60.0f * presented_multiplier;
|
||||
if (!Settings::values.use_speed_limit.GetValue()) {
|
||||
return NominalFrameRate;
|
||||
return QuantizeFrameRateHint(nominal_rate);
|
||||
}
|
||||
|
||||
const u16 speed_limit = Settings::SpeedLimit();
|
||||
@@ -161,7 +170,7 @@ float EmuWindow_Android::GetFrameRateHint() const {
|
||||
}
|
||||
|
||||
const float speed_limited_rate =
|
||||
NominalFrameRate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
|
||||
nominal_rate * (static_cast<float>(std::min<u16>(speed_limit, 100)) / 100.0f);
|
||||
return QuantizeFrameRateHint(speed_limited_rate);
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ private:
|
||||
void UpdateObservedFrameRate();
|
||||
[[nodiscard]] float GetFrameRateHint() const;
|
||||
[[nodiscard]] float GetFrameTimeVerifiedHint() const;
|
||||
[[nodiscard]] static float GetPresentedFrameMultiplier();
|
||||
[[nodiscard]] static float QuantizeFrameRateHint(float frame_rate);
|
||||
|
||||
float m_window_width{};
|
||||
|
||||
@@ -44,6 +44,7 @@ extern "C" {
|
||||
#include "common/android/android_common.h"
|
||||
#include "common/android/id_cache.h"
|
||||
#include "common/dynamic_library.h"
|
||||
#include "common/fs/fs_util.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "common/scm_rev.h"
|
||||
@@ -90,6 +91,7 @@ extern "C" {
|
||||
#include "hid_core/hid_types.h"
|
||||
#include "input_common/drivers/virtual_amiibo.h"
|
||||
#include "jni/native.h"
|
||||
#include "video_core/frame_gen/lossless_dll.h"
|
||||
#include "video_core/renderer_base.h"
|
||||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||
#include "video_core/capture.h"
|
||||
@@ -1090,6 +1092,34 @@ VkPhysicalDeviceProperties GetVulkanDeviceProperties() {
|
||||
const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
|
||||
return physical_device.GetProperties();
|
||||
}
|
||||
|
||||
bool GetVulkanMemoryModelSupport() {
|
||||
Common::DynamicLibrary library;
|
||||
if (!library.Open("libvulkan.so")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Vulkan::vk::InstanceDispatch dld;
|
||||
const auto instance = Vulkan::CreateInstance(library, dld, VK_API_VERSION_1_1);
|
||||
const auto physical_devices = instance.EnumeratePhysicalDevices();
|
||||
if (physical_devices.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Vulkan::vk::PhysicalDevice physical_device(physical_devices[0], dld);
|
||||
|
||||
VkPhysicalDeviceVulkanMemoryModelFeatures memory_model{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES,
|
||||
.pNext = nullptr,
|
||||
};
|
||||
VkPhysicalDeviceFeatures2 features{
|
||||
.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2,
|
||||
.pNext = &memory_model,
|
||||
};
|
||||
physical_device.GetFeatures2(features);
|
||||
|
||||
return memory_model.vulkanMemoryModel == VK_TRUE;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanDriverVersion(JNIEnv* env, jobject jobj) {
|
||||
@@ -1165,6 +1195,14 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getVulkanApiVersion(JNIEnv* env, j
|
||||
}
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_supportsFrameGeneration(JNIEnv* env, jobject jobj) {
|
||||
try {
|
||||
return static_cast<jboolean>(GetVulkanMemoryModelSupport());
|
||||
} catch (...) {
|
||||
return static_cast<jboolean>(false);
|
||||
}
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getGpuModel(JNIEnv* env, jobject jobj) {
|
||||
const auto props = GetVulkanDeviceProperties();
|
||||
if (props.deviceID == 0) {
|
||||
@@ -1390,6 +1428,23 @@ jint Java_org_yuzu_yuzu_1emu_NativeLibrary_installKeys(JNIEnv* env, jclass clazz
|
||||
return static_cast<int>(FirmwareManager::InstallKeys(path, ext));
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getLosslessDllPath(JNIEnv* env, jclass clazz) {
|
||||
const auto path = VideoCore::FrameGen::GetLosslessDllPath();
|
||||
return Common::Android::ToJString(env, Common::FS::PathToUTF8String(path));
|
||||
}
|
||||
|
||||
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_validateLosslessDll(JNIEnv* env, jclass clazz) {
|
||||
return static_cast<jint>(VideoCore::FrameGen::GetInstalledLosslessStatus());
|
||||
}
|
||||
|
||||
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_prepareLosslessDll(JNIEnv* env, jclass clazz) {
|
||||
return static_cast<jint>(VideoCore::FrameGen::BuildShaderCache());
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_removeLosslessDll(JNIEnv* env, jclass clazz) {
|
||||
return static_cast<jboolean>(VideoCore::FrameGen::RemoveInstalledLosslessDll());
|
||||
}
|
||||
|
||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
|
||||
jstring jpath,
|
||||
jstring jprogramId) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M11,10.6a8,5.2 0 1,0 0,10.4a8,5.2 0 1,0 0,-10.4z" />
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M4.6,12.6L0.8,10.2L3.2,15.5z" />
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M15.3,3.5a4,4 0 1,0 0,8a4,4 0 1,0 0,-8zM16.6,4.9a1,1 0 1,0 0,2a1,1 0 1,0 0,-2z" />
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:pathData="M18.6,6.5L23.2,7.8L18.6,9.3z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_lossless"
|
||||
style="@style/Widget.Eden.TransparentTopAppBarLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar_lossless"
|
||||
style="@style/Widget.Eden.TransparentTopToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:touchscreenBlocksFocus="false"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:title="@string/lossless_scaling" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scroll_view_lossless"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:clipToPadding="false"
|
||||
android:defaultFocusHighlightEnabled="false"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar_lossless">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/lossless_options_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:nestedScrollingEnabled="false" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -47,6 +47,12 @@
|
||||
android:defaultValue="@null" />
|
||||
</fragment>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/losslessManagerFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.LosslessManagerFragment"
|
||||
android:label="@string/lossless_scaling"
|
||||
tools:layout="@layout/fragment_lossless_manager" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/appletLauncherFragment"
|
||||
android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
|
||||
|
||||
@@ -162,6 +162,48 @@
|
||||
<item>@string/resolution_four</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="frameGenMultiplierNames">
|
||||
<item>@string/frame_gen_multiplier_2x</item>
|
||||
<item>@string/frame_gen_multiplier_3x</item>
|
||||
<item>@string/frame_gen_multiplier_4x</item>
|
||||
</string-array>
|
||||
|
||||
<integer-array name="frameGenMultiplierValues">
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="frameGenTargetRateNames">
|
||||
<item>@string/frame_gen_target_rate_off</item>
|
||||
<item>@string/frame_gen_target_rate_60</item>
|
||||
<item>@string/frame_gen_target_rate_90</item>
|
||||
<item>@string/frame_gen_target_rate_120</item>
|
||||
<item>@string/frame_gen_target_rate_144</item>
|
||||
<item>@string/frame_gen_target_rate_165</item>
|
||||
</string-array>
|
||||
|
||||
<integer-array name="frameGenTargetRateValues">
|
||||
<item>0</item>
|
||||
<item>60</item>
|
||||
<item>90</item>
|
||||
<item>120</item>
|
||||
<item>144</item>
|
||||
<item>165</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="frameGenQueueTargetNames">
|
||||
<item>@string/frame_gen_queue_target_0</item>
|
||||
<item>@string/frame_gen_queue_target_1</item>
|
||||
<item>@string/frame_gen_queue_target_2</item>
|
||||
</string-array>
|
||||
|
||||
<integer-array name="frameGenQueueTargetValues">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="rendererVSyncNames">
|
||||
<item>@string/renderer_vsync_immediate</item>
|
||||
<item>@string/renderer_vsync_mailbox</item>
|
||||
@@ -638,6 +680,16 @@
|
||||
<item>@string/error_keys_failed_init</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="losslessDllResults">
|
||||
<item>""</item>
|
||||
<item>@string/error_lossless_copy_failed</item>
|
||||
<item>@string/error_lossless_unreadable</item>
|
||||
<item>@string/error_lossless_not_pe</item>
|
||||
<item>@string/error_lossless_missing_shaders</item>
|
||||
<item>@string/error_lossless_translation_failed</item>
|
||||
<item>@string/error_lossless_cache_failed</item>
|
||||
</string-array>
|
||||
|
||||
<!-- GPU Logging Arrays -->
|
||||
<string-array name="gpuLogLevelEntries">
|
||||
<item>Off</item>
|
||||
|
||||
@@ -298,6 +298,67 @@
|
||||
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
|
||||
<string name="gpu_driver_manager">GPU driver manager</string>
|
||||
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
|
||||
<string name="frame_gen">Frame generation</string>
|
||||
<string name="frame_gen_per_game_description">Configure frame generation for this game</string>
|
||||
<string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
|
||||
<string name="frame_gen_multiplier">Frame multiplier</string>
|
||||
<string name="frame_gen_multiplier_description">How many frames to display for each rendered frame. Higher values cost proportionally more GPU time. Asking for more than your display can present will slow emulation down.</string>
|
||||
<string name="frame_gen_multiplier_2x">2x</string>
|
||||
<string name="frame_gen_multiplier_3x">3x</string>
|
||||
<string name="frame_gen_multiplier_4x">4x</string>
|
||||
<string name="frame_gen_target_rate">Target frame rate</string>
|
||||
<string name="frame_gen_target_rate_description">Pick the rate your display can actually show. The multiplier then rises or falls on its own to hold it, and rolls back any step that makes the game itself run slower.</string>
|
||||
<string name="frame_gen_target_rate_off">Use a fixed multiplier</string>
|
||||
<string name="frame_gen_target_rate_60">60 FPS</string>
|
||||
<string name="frame_gen_target_rate_90">90 FPS</string>
|
||||
<string name="frame_gen_target_rate_120">120 FPS</string>
|
||||
<string name="frame_gen_target_rate_144">144 FPS</string>
|
||||
<string name="frame_gen_target_rate_165">165 FPS</string>
|
||||
<string name="frame_gen_queue_target">Frame queue target</string>
|
||||
<string name="frame_gen_queue_target_description">How many finished frames may wait ahead of the display. Larger queues absorb GPU spikes at the cost of input latency.</string>
|
||||
<string name="frame_gen_queue_target_0">Lowest latency (Unbuffered)</string>
|
||||
<string name="frame_gen_queue_target_1">Balanced (1 frame)</string>
|
||||
<string name="frame_gen_queue_target_2">Smoothest (2 frames)</string>
|
||||
<string name="frame_gen_flow_scale_auto">Match motion estimation to the game</string>
|
||||
<string name="frame_gen_flow_scale_auto_description">Estimate motion at the resolution the game actually renders instead of the upscaled output. Costs nothing in accuracy, since upscaling adds no motion detail.</string>
|
||||
<string name="frame_gen_flow_scale">Motion estimation resolution</string>
|
||||
<string name="frame_gen_flow_scale_description">Resolution of the optical flow pass, as a fraction of the output. Lowering it is the cheapest way to reclaim performance.</string>
|
||||
<string name="frame_gen_fp16">Half precision shaders</string>
|
||||
<string name="frame_gen_fp16_description">Use the 16-bit shader variant. Falls back automatically if the driver or the file lacks it.</string>
|
||||
<string name="frame_gen_dump_flow">Dump generated frame</string>
|
||||
<string name="frame_gen_dump_flow_description">Write the optical flow mip levels and the interpolated frame to the lossless/debug folder once, for troubleshooting</string>
|
||||
<string name="frame_gen_unsupported">Frame generation unavailable</string>
|
||||
<string name="frame_gen_unsupported_description">This GPU driver does not support the Vulkan memory model, which the Lossless Scaling shaders require.</string>
|
||||
<string name="lossless_scaling_setup_description">Optional. Provide your own Lossless.dll to enable frame generation later</string>
|
||||
<string name="lossless_scaling_install">Install Lossless.dll</string>
|
||||
<string name="lossless_scaling_install_description">Frame generation needs your own legal copy of Lossless.dll from Lossless Scaling</string>
|
||||
<string name="lossless_scaling_replace_description">Select a different copy of Lossless.dll</string>
|
||||
<string name="frame_generation_support">Frame generation</string>
|
||||
<string name="frame_generation_supported">Supported</string>
|
||||
<string name="frame_generation_unsupported">Unsupported (no Vulkan memory model)</string>
|
||||
<string name="lossless_scaling">Lossless Scaling</string>
|
||||
<string name="lossless_scaling_description">Provide your own copy of Lossless.dll to enable frame generation</string>
|
||||
<string name="lossless_scaling_installed">Installed</string>
|
||||
<string name="lossless_scaling_not_installed">Not installed</string>
|
||||
<string name="lossless_scaling_replace">Replace</string>
|
||||
<string name="lossless_scaling_remove">Remove</string>
|
||||
<string name="lossless_scaling_remove_description">Delete the installed Lossless.dll and its prepared shaders</string>
|
||||
<string name="lossless_scaling_remove_confirmation">Frame generation will stop working until you install Lossless.dll again. Your original file is not affected.</string>
|
||||
<string name="lossless_scaling_missing">Lossless.dll not installed</string>
|
||||
<string name="lossless_scaling_missing_description">Install it from Settings › Lossless Scaling to use frame generation.</string>
|
||||
<string name="lossless_scaling_locked">Close the game first</string>
|
||||
<string name="lossless_scaling_locked_description">Lossless.dll cannot be changed while a game is running.</string>
|
||||
<string name="lossless_scaling_remove_unavailable">Nothing to remove</string>
|
||||
<string name="lossless_scaling_remove_unavailable_description">Lossless.dll is not installed yet.</string>
|
||||
<string name="lossless_scaling_installing">Preparing frame generation shaders…</string>
|
||||
<string name="lossless_scaling_install_success">Lossless.dll installed successfully</string>
|
||||
<string name="lossless_scaling_install_failed">Could not install Lossless.dll</string>
|
||||
<string name="error_lossless_copy_failed">The selected file could not be copied.</string>
|
||||
<string name="error_lossless_unreadable">The selected file could not be read.</string>
|
||||
<string name="error_lossless_not_pe">The selected file is not a Windows library. Select Lossless.dll from your Lossless Scaling installation.</string>
|
||||
<string name="error_lossless_missing_shaders">This copy of Lossless.dll does not contain the frame generation shaders. Update Lossless Scaling and try again.</string>
|
||||
<string name="error_lossless_translation_failed">The frame generation shaders could not be translated. This version of Lossless Scaling is not supported yet.</string>
|
||||
<string name="error_lossless_cache_failed">The translated shaders could not be written to storage. Check that there is free space available.</string>
|
||||
<string name="advanced_settings">Advanced settings</string>
|
||||
<string name="settings_description">Configure emulator settings</string>
|
||||
<string name="search_recently_played">Recently played</string>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "device_power_state.h"
|
||||
@@ -14,14 +14,11 @@ extern std::atomic<bool> g_has_battery;
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#if defined(TARGET_OS_MAC) && TARGET_OS_MAC
|
||||
#if TARGET_OS_IPHONE
|
||||
// ios doesnt have this
|
||||
#else
|
||||
#if TARGET_OS_MAC
|
||||
#include <IOKit/ps/IOPSKeys.h>
|
||||
#include <IOKit/ps/IOPowerSources.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#elif defined(__linux__)
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
@@ -51,9 +48,7 @@ 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);
|
||||
@@ -101,6 +96,7 @@ namespace Common {
|
||||
#else
|
||||
info.has_battery = false;
|
||||
#endif
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#define KEYS_DIR "keys"
|
||||
#define LOAD_DIR "load"
|
||||
#define LOG_DIR "log"
|
||||
#define LOSSLESS_DIR "lossless"
|
||||
#define NAND_DIR "nand"
|
||||
#define PLAY_TIME_DIR "play_time"
|
||||
#define SCREENSHOTS_DIR "screenshots"
|
||||
@@ -37,3 +38,5 @@
|
||||
|
||||
// yuzu-specific files
|
||||
#define LOG_FILE "eden_log.txt"
|
||||
#define LOSSLESS_DLL_FILE "Lossless.dll"
|
||||
#define LOSSLESS_CACHE_FILE "lsfg_spirv.cache"
|
||||
|
||||
@@ -157,6 +157,7 @@ public:
|
||||
GenerateEdenPath(EdenPath::KeysDir, eden_path / KEYS_DIR);
|
||||
GenerateEdenPath(EdenPath::LoadDir, eden_path / LOAD_DIR);
|
||||
GenerateEdenPath(EdenPath::LogDir, eden_path / LOG_DIR);
|
||||
GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR);
|
||||
GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR);
|
||||
GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR);
|
||||
GenerateEdenPath(EdenPath::SaveDir, eden_path / NAND_DIR);
|
||||
|
||||
@@ -23,6 +23,7 @@ enum class EdenPath {
|
||||
KeysDir, // Where key files are stored.
|
||||
LoadDir, // Where cheat/mod files are stored.
|
||||
LogDir, // Where log files are stored.
|
||||
LosslessDir, // Where the user-supplied Lossless Scaling library is stored.
|
||||
NANDDir, // Where the emulated NAND is stored.
|
||||
PlayTimeDir, // Where play time data is stored.
|
||||
SaveDir, // Where save data is stored.
|
||||
|
||||
@@ -27,11 +27,7 @@
|
||||
#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__)
|
||||
|
||||
@@ -380,6 +380,28 @@ void UpdateRescalingInfo() {
|
||||
TranslateResolutionInfo(setup, info);
|
||||
}
|
||||
|
||||
u32 FrameGenMultiplier() {
|
||||
return std::clamp(values.frame_gen_multiplier.GetValue(), MIN_FRAME_GEN_MULTIPLIER,
|
||||
MAX_FRAME_GEN_MULTIPLIER);
|
||||
}
|
||||
|
||||
size_t FrameGenGenerations() {
|
||||
if (!values.frame_gen.GetValue()) {
|
||||
return 0;
|
||||
}
|
||||
return FrameGenMultiplier() - 1;
|
||||
}
|
||||
|
||||
size_t FrameGenMaxGenerations() {
|
||||
if (!values.frame_gen.GetValue()) {
|
||||
return 0;
|
||||
}
|
||||
if (values.frame_gen_target_rate.GetValue() != 0) {
|
||||
return MAX_FRAME_GEN_MULTIPLIER - 1;
|
||||
}
|
||||
return FrameGenMultiplier() - 1;
|
||||
}
|
||||
|
||||
void RestoreGlobalState(bool is_powered_on) {
|
||||
// If a game is running, DO NOT restore the global settings state
|
||||
if (is_powered_on) {
|
||||
|
||||
+76
-8
@@ -352,7 +352,7 @@ struct Values {
|
||||
true};
|
||||
|
||||
SwitchableSetting<ScalingFilter> scaling_filter{linkage,
|
||||
ScalingFilter::Bilinear,
|
||||
ScalingFilter::NearestNeighbor,
|
||||
"scaling_filter",
|
||||
Category::Renderer,
|
||||
Specialization::Default,
|
||||
@@ -388,6 +388,69 @@ struct Values {
|
||||
true,
|
||||
true};
|
||||
|
||||
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
|
||||
Specialization::Default, true, false};
|
||||
|
||||
SwitchableSetting<u32, true> frame_gen_multiplier{linkage,
|
||||
2,
|
||||
2,
|
||||
4,
|
||||
"frame_gen_multiplier",
|
||||
Category::Renderer,
|
||||
Specialization::Countable,
|
||||
true,
|
||||
false,
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<u32, true> frame_gen_target_rate{linkage,
|
||||
0,
|
||||
0,
|
||||
240,
|
||||
"frame_gen_target_rate",
|
||||
Category::Renderer,
|
||||
Specialization::Countable,
|
||||
true,
|
||||
true,
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<bool> frame_gen_flow_scale_auto{linkage,
|
||||
true,
|
||||
"frame_gen_flow_scale_auto",
|
||||
Category::Renderer,
|
||||
Specialization::Default,
|
||||
true,
|
||||
false,
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<u32, true> frame_gen_flow_scale{linkage,
|
||||
75,
|
||||
25,
|
||||
100,
|
||||
"frame_gen_flow_scale",
|
||||
Category::Renderer,
|
||||
Specialization::Countable |
|
||||
Specialization::Percentage,
|
||||
true,
|
||||
true,
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<u32, true> frame_gen_queue_target{linkage,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
"frame_gen_queue_target",
|
||||
Category::Renderer,
|
||||
Specialization::Countable,
|
||||
true,
|
||||
false,
|
||||
&frame_gen};
|
||||
|
||||
SwitchableSetting<bool> frame_gen_fp16{linkage, true, "frame_gen_fp16", Category::Renderer,
|
||||
Specialization::Default, true, false, &frame_gen};
|
||||
|
||||
SwitchableSetting<bool> frame_gen_dump_flow{linkage, false, "frame_gen_dump_flow",
|
||||
Category::Renderer};
|
||||
|
||||
SwitchableSetting<bool> use_asynchronous_gpu_emulation{linkage,
|
||||
#ifdef __ANDROID__
|
||||
false,
|
||||
@@ -569,13 +632,8 @@ struct Values {
|
||||
SwitchableSetting<bool> emulate_bgr565{linkage, false, "emulate_bgr565",
|
||||
Category::RendererHacks};
|
||||
|
||||
SwitchableSetting<bool> rescale_hack{linkage,
|
||||
#ifdef __ANDROID__
|
||||
true,
|
||||
#else
|
||||
false,
|
||||
#endif
|
||||
"rescale_hack", Category::RendererHacks};
|
||||
SwitchableSetting<bool> rescale_hack{linkage, false, "rescale_hack",
|
||||
Category::RendererHacks};
|
||||
SwitchableSetting<bool> enable_gpu_buffer_readback{linkage,
|
||||
false,
|
||||
"enable_gpu_buffer_readback",
|
||||
@@ -875,10 +933,20 @@ struct Values {
|
||||
|
||||
// Per-game overrides
|
||||
bool use_squashed_iterated_blend;
|
||||
|
||||
};
|
||||
|
||||
extern Values values;
|
||||
|
||||
constexpr u32 MIN_FRAME_GEN_MULTIPLIER = 2;
|
||||
constexpr u32 MAX_FRAME_GEN_MULTIPLIER = 4;
|
||||
|
||||
[[nodiscard]] u32 FrameGenMultiplier();
|
||||
|
||||
[[nodiscard]] size_t FrameGenGenerations();
|
||||
|
||||
[[nodiscard]] size_t FrameGenMaxGenerations();
|
||||
|
||||
bool getDebugKnobAt(u8 i);
|
||||
|
||||
void UpdateGPUAccuracy();
|
||||
|
||||
+6
-107
@@ -116,119 +116,18 @@ std::string ReplaceAll(std::string result, const std::string& src, const std::st
|
||||
}
|
||||
|
||||
std::string UTF16ToUTF8(std::u16string_view input) {
|
||||
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::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
||||
return convert.to_bytes(input.data(), input.data() + input.size());
|
||||
}
|
||||
|
||||
std::u16string UTF8ToUTF16(std::string_view input) {
|
||||
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::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> convert;
|
||||
return convert.from_bytes(input.data(), input.data() + input.size());
|
||||
}
|
||||
|
||||
std::u32string UTF8ToUTF32(std::string_view input) {
|
||||
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;
|
||||
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> convert;
|
||||
return convert.from_bytes(input.data(), input.data() + input.size());
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -1269,10 +1269,6 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64 OR ARCHITECTURE_riscv64 OR ARCHITE
|
||||
hle/service/jit/jit.cpp
|
||||
hle/service/jit/jit.h)
|
||||
target_link_libraries(core PRIVATE dynarmic::dynarmic)
|
||||
# Quick hack for XCode generator...
|
||||
if (IOS)
|
||||
target_include_directories(core PRIVATE "${CMAKE_SOURCE_DIR}/dynarmic/src")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_sources(core PRIVATE hle/service/ssl/ssl_backend_openssl.cpp)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "dynarmic/src/dynarmic/interface/halt_reason.h"
|
||||
#include <dynarmic/interface/halt_reason.h>
|
||||
|
||||
#include "core/arm/arm_interface.h"
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "dynarmic/src/dynarmic/interface/A32/a32.h"
|
||||
#include "dynarmic/src/dynarmic/interface/code_page.h"
|
||||
#include <dynarmic/interface/A32/a32.h>
|
||||
#include <dynarmic/interface/code_page.h>
|
||||
|
||||
#include "core/arm/arm_interface.h"
|
||||
#include "core/arm/dynarmic/dynarmic_exclusive_monitor.h"
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
#include <memory>
|
||||
#include <ankerl/unordered_dense.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"
|
||||
#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"
|
||||
|
||||
namespace Core::Memory {
|
||||
class Memory;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// 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
|
||||
|
||||
@@ -8,7 +5,7 @@
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "dynarmic/interface/A32/coprocessor.h"
|
||||
#include <dynarmic/interface/A32/coprocessor.h>
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Core {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
// 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/src/dynarmic/interface/exclusive_monitor.h"
|
||||
#include <dynarmic/interface/exclusive_monitor.h>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "core/arm/exclusive_monitor.h"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
// 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
|
||||
@@ -10,9 +7,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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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()
|
||||
@@ -1,5 +1,6 @@
|
||||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
include(TargetArchitectureSpecificSources)
|
||||
|
||||
add_library(dynarmic STATIC
|
||||
mcl/bit.hpp
|
||||
@@ -142,7 +143,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_sources(dynarmic PRIVATE
|
||||
target_architecture_specific_sources(dynarmic "x86_64"
|
||||
backend/x64/abi.cpp
|
||||
backend/x64/abi.h
|
||||
backend/x64/block_of_code.cpp
|
||||
@@ -203,7 +204,7 @@ endif()
|
||||
if ("arm64" IN_LIST ARCHITECTURE)
|
||||
target_link_libraries(dynarmic PRIVATE merry::oaknut)
|
||||
|
||||
target_sources(dynarmic PRIVATE
|
||||
target_architecture_specific_sources(dynarmic "arm64"
|
||||
backend/arm64/a32_jitstate.cpp
|
||||
backend/arm64/a32_jitstate.h
|
||||
backend/arm64/a64_jitstate.h
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -14,8 +14,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "config.h"
|
||||
#include "dynarmic/src/dynarmic/interface/halt_reason.h"
|
||||
#include "dynarmic/interface/A32/config.h"
|
||||
#include "dynarmic/interface/halt_reason.h"
|
||||
|
||||
namespace Dynarmic {
|
||||
namespace A32 {
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "../../frontend/A32/translate/translate_callbacks.h"
|
||||
#include "arch_version.h"
|
||||
#include "../optimization_flags.h"
|
||||
#include "dynarmic/frontend/A32/translate/translate_callbacks.h"
|
||||
#include "dynarmic/interface/A32/arch_version.h"
|
||||
#include "dynarmic/interface/optimization_flags.h"
|
||||
|
||||
namespace Dynarmic {
|
||||
class ExclusiveMonitor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
/* This file is part of the dynarmic project.
|
||||
@@ -15,8 +15,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "config.h"
|
||||
#include "../halt_reason.h"
|
||||
#include "dynarmic/interface/A64/config.h"
|
||||
#include "dynarmic/interface/halt_reason.h"
|
||||
|
||||
namespace Dynarmic {
|
||||
namespace A64 {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
#include "../optimization_flags.h"
|
||||
#include "dynarmic/interface/optimization_flags.h"
|
||||
|
||||
namespace Dynarmic {
|
||||
class ExclusiveMonitor;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include <cstring>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
|
||||
#include "dynarmic/src/dynarmic/common/spin_lock.h"
|
||||
#include <dynarmic/common/spin_lock.h>
|
||||
|
||||
namespace Dynarmic {
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
include(TargetArchitectureSpecificSources)
|
||||
|
||||
add_executable(dynarmic_tests
|
||||
fp/FPToFixed.cpp
|
||||
@@ -49,7 +50,7 @@ endif()
|
||||
|
||||
if ("x86_64" IN_LIST ARCHITECTURE)
|
||||
target_link_libraries(dynarmic_tests PRIVATE xbyak::xbyak)
|
||||
target_sources(dynarmic PRIVATE
|
||||
target_architecture_specific_sources(dynarmic_tests "x86_64"
|
||||
x64_cpu_info.cpp
|
||||
native/preserve_xmm.cpp
|
||||
)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// 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 */
|
||||
@@ -1,113 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Jarrod Norwell
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
import QuartzCore.CAMetalLayer
|
||||
import SwiftUI
|
||||
|
||||
public struct AppUI {
|
||||
public static let shared = AppUI()
|
||||
fileprivate let appUIObjC = AppUIObjC.shared()
|
||||
|
||||
public func configure(layer: CAMetalLayer, with size: CGSize) {
|
||||
appUIObjC.configure(layer: layer, with: size)
|
||||
}
|
||||
|
||||
public func information(for url: URL) -> AppUIInformation {
|
||||
appUIObjC.gameInformation.information(for: url)
|
||||
}
|
||||
|
||||
public func insert(game url: URL) {
|
||||
appUIObjC.insert(game: url)
|
||||
}
|
||||
|
||||
public func insert(games urls: [URL]) {
|
||||
appUIObjC.insert(games: urls)
|
||||
}
|
||||
|
||||
public func bootOS() {
|
||||
appUIObjC.bootOS()
|
||||
}
|
||||
|
||||
public func pause() {
|
||||
appUIObjC.pause()
|
||||
}
|
||||
|
||||
public func play() {
|
||||
appUIObjC.play()
|
||||
}
|
||||
|
||||
public func ispaused() -> Bool {
|
||||
return appUIObjC.ispaused()
|
||||
}
|
||||
|
||||
public func FirstFrameShowed() -> Bool {
|
||||
return appUIObjC.hasfirstfame()
|
||||
}
|
||||
|
||||
public func canGetFullPath() -> Bool {
|
||||
return appUIObjC.canGetFullPath()
|
||||
}
|
||||
|
||||
|
||||
public func exit() {
|
||||
appUIObjC.quit()
|
||||
}
|
||||
|
||||
public func step() {
|
||||
appUIObjC.step()
|
||||
}
|
||||
|
||||
public func orientationChanged(orientation: UIInterfaceOrientation, with layer: CAMetalLayer, size: CGSize) {
|
||||
appUIObjC.orientationChanged(orientation: orientation, with: layer, size: size)
|
||||
}
|
||||
|
||||
public func touchBegan(at point: CGPoint, for index: UInt) {
|
||||
appUIObjC.touchBegan(at: point, for: index)
|
||||
}
|
||||
|
||||
public func touchEnded(for index: UInt) {
|
||||
appUIObjC.touchEnded(for: index)
|
||||
}
|
||||
|
||||
public func touchMoved(at point: CGPoint, for index: UInt) {
|
||||
appUIObjC.touchMoved(at: point, for: index)
|
||||
}
|
||||
|
||||
public func gyroMoved(x: Float, y: Float, z: Float, accelX: Float, accelY: Float, accelZ: Float, controllerId: Int32, deltaTimestamp: Int32) {
|
||||
// Calling the Objective-C function with both gyroscope and accelerometer data
|
||||
appUIObjC.virtualControllerGyro(controllerId,
|
||||
deltaTimestamp: deltaTimestamp,
|
||||
gyroX: x, gyroY: y, gyroZ: z,
|
||||
accelX: accelX, accelY: accelY, accelZ: accelZ)
|
||||
}
|
||||
|
||||
|
||||
public func thumbstickMoved(analog: VirtualControllerAnalogType, x: Float, y: Float, controllerid: Int) {
|
||||
appUIObjC.thumbstickMoved(analog, x: CGFloat(x), y: CGFloat(y), controllerId: Int32(controllerid))
|
||||
}
|
||||
|
||||
public func virtualControllerButtonDown(button: VirtualControllerButtonType, controllerid: Int) {
|
||||
appUIObjC.virtualControllerButtonDown(button, controllerId: Int32(controllerid))
|
||||
}
|
||||
|
||||
public func virtualControllerButtonUp(button: VirtualControllerButtonType, controllerid: Int) {
|
||||
appUIObjC.virtualControllerButtonUp(button, controllerId: Int32(controllerid))
|
||||
}
|
||||
|
||||
public func settingsSaved() {
|
||||
appUIObjC.settingsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
struct EdenApp: App {
|
||||
@StateObject private var core = EmulationViewModel()
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(core: core)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// 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
|
||||
@@ -1,436 +0,0 @@
|
||||
// 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
|
||||
@@ -1,93 +0,0 @@
|
||||
// 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
|
||||
@@ -1,251 +0,0 @@
|
||||
// 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
|
||||
@@ -1,86 +0,0 @@
|
||||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
enable_language(Swift OBJCXX)
|
||||
add_executable(eden-ios
|
||||
AppUI-Bridging-Header.h
|
||||
AppUI.swift
|
||||
AppUIGameInformation.h
|
||||
AppUIGameInformation.mm
|
||||
AppUIObjC.h
|
||||
AppUIObjC.mm
|
||||
Config.h
|
||||
Config.mm
|
||||
EmulationSession.h
|
||||
EmulationSession.mm
|
||||
EmulationWindow.h
|
||||
EmulationWindow.mm
|
||||
VMA.cpp
|
||||
|
||||
EmulationGame.swift
|
||||
JoystickView.swift
|
||||
EmulationHandler.swift
|
||||
PomeloApp.swift
|
||||
FileManager.swift
|
||||
EmulationView.swift
|
||||
LibraryView.swift
|
||||
KeyboardHostingController.swift
|
||||
FolderMonitor.swift
|
||||
GameButtonView.swift
|
||||
EmulationScreenView.swift
|
||||
GameListView.swift
|
||||
|
||||
# Extensions
|
||||
SwiftUIJoystick.swift
|
||||
)
|
||||
|
||||
set(MACOSX_BUNDLE_GUI_IDENTIFIER "dev.eden-emu.eden")
|
||||
set(MACOSX_BUNDLE_BUNDLE_NAME "Eden")
|
||||
set(MACOSX_BUNDLE_INFO_STRING "Eden: A high-performance Nintendo Switch emulator")
|
||||
|
||||
# TODO(crueter): Copyright, and versioning
|
||||
|
||||
# Keep bundle identifier as-is, for compatibility sake
|
||||
set_target_properties(eden-ios PROPERTIES
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/AppUI-Bridging-Header.h"
|
||||
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "eden-ios-Swift.h"
|
||||
XCODE_ATTRIBUTE_DERIVED_FILE_DIR "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
|
||||
target_link_libraries(eden-ios PRIVATE common core input_common frontend_common video_core sirit::sirit)
|
||||
target_link_libraries(eden-ios PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
|
||||
target_link_libraries(eden-ios PRIVATE SDL2::SDL2 glad stb::headers)
|
||||
create_target_directory_groups(eden-ios)
|
||||
|
||||
# FIXME(crueter): This should /all/ be in a module of some kind!
|
||||
|
||||
# Xcode will automatically generate the Assets.car and icns file for us.
|
||||
set(_dist "${CMAKE_SOURCE_DIR}/dist")
|
||||
if (CMAKE_GENERATOR MATCHES "Xcode")
|
||||
set(_icons "${_dist}/eden.icon")
|
||||
|
||||
set_target_properties(eden-ios PROPERTIES
|
||||
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME eden
|
||||
MACOSX_BUNDLE_ICON_FILE eden
|
||||
# Also force xcode to manage signing for us.
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED ON
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED ON
|
||||
XCODE_ATTRIBUTE_CODE_SIGN_STYLE Automatic)
|
||||
# Otherwise, we'll use our own.
|
||||
else()
|
||||
set(_icons "${_dist}/eden.icns" "${_dist}/Assets.car")
|
||||
endif()
|
||||
|
||||
set_source_files_properties(${_icons} PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Resources)
|
||||
target_sources(eden-ios PRIVATE ${_icons})
|
||||
|
||||
set_target_properties(eden-ios PROPERTIES MACOSX_BUNDLE TRUE)
|
||||
|
||||
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dylib")
|
||||
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
|
||||
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
|
||||
|
||||
set_source_files_properties(${MOLTENVK_LIBRARY} PROPERTIES
|
||||
MACOSX_PACKAGE_LOCATION Frameworks
|
||||
XCODE_FILE_ATTRIBUTES "CodeSignOnCopy")
|
||||
target_sources(eden-ios PRIVATE ${MOLTENVK_LIBRARY})
|
||||
@@ -1,19 +0,0 @@
|
||||
// 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
|
||||
@@ -1,11 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
#include "Config.h"
|
||||
|
||||
namespace IOSSettings {
|
||||
|
||||
Values values;
|
||||
|
||||
} // namespace IOSSettings
|
||||
@@ -1,31 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EmulationGame : Comparable, Hashable, Identifiable {
|
||||
var id = UUID()
|
||||
|
||||
let developer: String
|
||||
let fileURL: URL
|
||||
let imageData: Data
|
||||
let title: String
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(developer)
|
||||
hasher.combine(fileURL)
|
||||
hasher.combine(imageData)
|
||||
hasher.combine(title)
|
||||
}
|
||||
|
||||
static func < (lhs: EmulationGame, rhs: EmulationGame) -> Bool {
|
||||
lhs.title < rhs.title
|
||||
}
|
||||
|
||||
static func == (lhs: EmulationGame, rhs: EmulationGame) -> Bool {
|
||||
lhs.title == rhs.title
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import Metal
|
||||
import Foundation
|
||||
|
||||
class EmulationSessionViewModel: ObservableObject {
|
||||
@Published var isShowingCustomButton = true
|
||||
@State var should = false
|
||||
var device: MTLDevice?
|
||||
@State var mtkView: MTKView = MTKView()
|
||||
var CaLayer: CAMetalLayer?
|
||||
private var sudachiGame: EmulationGame?
|
||||
private let appui = AppUI.shared
|
||||
private var thread: Thread!
|
||||
private var isRunning = false
|
||||
var doesneedresources = false
|
||||
@State var iscustom: Bool = false
|
||||
|
||||
init(game: EmulationGame?) {
|
||||
self.device = MTLCreateSystemDefaultDevice()
|
||||
self.sudachiGame = game
|
||||
}
|
||||
|
||||
func configureAppUI(with mtkView: MTKView) {
|
||||
self.mtkView = mtkView
|
||||
device = self.mtkView.device
|
||||
guard !isRunning else { return }
|
||||
isRunning = true
|
||||
appui.configure(layer: mtkView.layer as! CAMetalLayer, with: mtkView.frame.size)
|
||||
|
||||
iscustom = ((sudachiGame?.fileURL.startAccessingSecurityScopedResource()) != nil)
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
if let sudachiGame = self.sudachiGame {
|
||||
self.appui.insert(game: sudachiGame.fileURL)
|
||||
} else {
|
||||
self.appui.bootOS()
|
||||
}
|
||||
}
|
||||
|
||||
thread = .init(block: self.step)
|
||||
thread.name = "Yuzu"
|
||||
thread.qualityOfService = .userInteractive
|
||||
thread.threadPriority = 0.9
|
||||
thread.start()
|
||||
}
|
||||
|
||||
private func step() {
|
||||
while true {
|
||||
appui.step()
|
||||
}
|
||||
}
|
||||
|
||||
func customButtonTapped() {
|
||||
stopEmulation()
|
||||
}
|
||||
|
||||
private func stopEmulation() {
|
||||
if isRunning {
|
||||
isRunning = false
|
||||
appui.exit()
|
||||
thread.cancel()
|
||||
if iscustom {
|
||||
sudachiGame?.fileURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleOrientationChange() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let interfaceOrientation = self.getInterfaceOrientation(from: UIDevice.current.orientation)
|
||||
self.appui.orientationChanged(orientation: interfaceOrientation, with: self.mtkView.layer as! CAMetalLayer, size: mtkView.frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
private func getInterfaceOrientation(from deviceOrientation: UIDeviceOrientation) -> UIInterfaceOrientation {
|
||||
switch deviceOrientation {
|
||||
case .portrait:
|
||||
return .portrait
|
||||
case .portraitUpsideDown:
|
||||
return .portraitUpsideDown
|
||||
case .landscapeLeft:
|
||||
return .landscapeRight
|
||||
case .landscapeRight:
|
||||
return .landscapeLeft
|
||||
default:
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import MetalKit
|
||||
|
||||
class EmulationScreenView: UIView {
|
||||
var primaryScreen: UIView!
|
||||
var portraitconstraints = [NSLayoutConstraint]()
|
||||
var landscapeconstraints = [NSLayoutConstraint]()
|
||||
var fullscreenconstraints = [NSLayoutConstraint]()
|
||||
let appui = AppUI.shared
|
||||
let userDefaults = UserDefaults.standard
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
setupAppUIScreenforiPad()
|
||||
} else {
|
||||
setupAppUIScreen()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
setupAppUIScreenforiPad()
|
||||
} else {
|
||||
setupAppUIScreen()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
guard let touch = touches.first else {
|
||||
return
|
||||
}
|
||||
|
||||
print("Location: \(touch.location(in: primaryScreen))")
|
||||
appui.touchBegan(at: touch.location(in: primaryScreen), for: 0)
|
||||
}
|
||||
|
||||
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
print("Touch Ended")
|
||||
appui.touchEnded(for: 0)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
guard let touch = touches.first else {
|
||||
return
|
||||
}
|
||||
let location = touch.location(in: primaryScreen)
|
||||
print("Location Moved: \(location)")
|
||||
appui.touchMoved(at: location, for: 0)
|
||||
}
|
||||
|
||||
func setupAppUIScreenforiPad() {
|
||||
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
|
||||
primaryScreen.translatesAutoresizingMaskIntoConstraints = false
|
||||
primaryScreen.clipsToBounds = true
|
||||
primaryScreen.layer.borderColor = UIColor.secondarySystemBackground.cgColor
|
||||
primaryScreen.layer.borderWidth = 3
|
||||
primaryScreen.layer.cornerCurve = .continuous
|
||||
primaryScreen.layer.cornerRadius = 10
|
||||
addSubview(primaryScreen)
|
||||
|
||||
|
||||
portraitconstraints = [
|
||||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
|
||||
primaryScreen.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10),
|
||||
primaryScreen.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10),
|
||||
primaryScreen.heightAnchor.constraint(equalTo: primaryScreen.widthAnchor, multiplier: 9 / 16),
|
||||
]
|
||||
|
||||
landscapeconstraints = [
|
||||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 50),
|
||||
primaryScreen.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -100),
|
||||
primaryScreen.widthAnchor.constraint(equalTo: primaryScreen.heightAnchor, multiplier: 16 / 9),
|
||||
primaryScreen.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||
]
|
||||
|
||||
|
||||
updateConstraintsForOrientation()
|
||||
}
|
||||
|
||||
|
||||
|
||||
func setupAppUIScreen() {
|
||||
primaryScreen = MTKView(frame: .zero, device: MTLCreateSystemDefaultDevice())
|
||||
primaryScreen.translatesAutoresizingMaskIntoConstraints = false
|
||||
primaryScreen.clipsToBounds = true
|
||||
primaryScreen.layer.borderColor = UIColor.secondarySystemBackground.cgColor
|
||||
primaryScreen.layer.borderWidth = 3
|
||||
primaryScreen.layer.cornerCurve = .continuous
|
||||
primaryScreen.layer.cornerRadius = 10
|
||||
addSubview(primaryScreen)
|
||||
|
||||
|
||||
portraitconstraints = [
|
||||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
|
||||
primaryScreen.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: 10),
|
||||
primaryScreen.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -10),
|
||||
primaryScreen.heightAnchor.constraint(equalTo: primaryScreen.widthAnchor, multiplier: 9 / 16),
|
||||
]
|
||||
|
||||
landscapeconstraints = [
|
||||
primaryScreen.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 10),
|
||||
primaryScreen.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor, constant: -10),
|
||||
primaryScreen.widthAnchor.constraint(equalTo: primaryScreen.heightAnchor, multiplier: 16 / 9),
|
||||
primaryScreen.centerXAnchor.constraint(equalTo: safeAreaLayoutGuide.centerXAnchor),
|
||||
]
|
||||
|
||||
updateConstraintsForOrientation()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
updateConstraintsForOrientation()
|
||||
}
|
||||
|
||||
private func updateConstraintsForOrientation() {
|
||||
removeConstraints(portraitconstraints)
|
||||
removeConstraints(landscapeconstraints)
|
||||
let isPortrait = UIApplication.shared.statusBarOrientation.isPortrait
|
||||
addConstraints(isPortrait ? portraitconstraints : landscapeconstraints)
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// 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;
|
||||
};
|
||||
@@ -1,474 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -1,558 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import Foundation
|
||||
import GameController
|
||||
import UIKit
|
||||
import Metal
|
||||
|
||||
struct MetalView: UIViewRepresentable {
|
||||
let device: MTLDevice?
|
||||
let configure: (UIView) -> Void
|
||||
func makeUIView(context: Context) -> EmulationScreenView {
|
||||
let view = EmulationScreenView()
|
||||
configure(view.primaryScreen)
|
||||
return view
|
||||
}
|
||||
func updateUIView(_ uiView: EmulationScreenView, context: Context) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
struct ControllerView: View {
|
||||
let appui = AppUI.shared
|
||||
@State var isPressed = false
|
||||
@State var controllerconnected = false
|
||||
@State private var x: CGFloat = 0.0
|
||||
@State private var y: CGFloat = 0.0
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if !controllerconnected {
|
||||
OnScreenController(geometry: geometry) // i did this to clean it up as it was quite long lmfao
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
print("checking for controller:")
|
||||
controllerconnected = false
|
||||
DispatchQueue.main.async {
|
||||
setupControllers() // i dont know what half of this shit does
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add a dictionary to track controller IDs
|
||||
@State var controllerIDs: [GCController: Int] = [:]
|
||||
|
||||
private func setupControllers() {
|
||||
NotificationCenter.default.addObserver(forName: .GCControllerDidConnect, object: nil, queue: .main) { notification in
|
||||
if let controller = notification.object as? GCController {
|
||||
print("wow controller onstart") // yippeeee
|
||||
self.setupController(controller)
|
||||
self.controllerconnected = true
|
||||
} else {
|
||||
print("not GCController :((((((") // wahhhhhhh
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .GCControllerDidDisconnect, object: nil, queue: .main) { notification in
|
||||
if let controller = notification.object as? GCController {
|
||||
print("wow controller gone")
|
||||
if self.controllerIDs.isEmpty {
|
||||
controllerconnected = false
|
||||
}
|
||||
self.controllerIDs.removeValue(forKey: controller) // Remove the controller ID
|
||||
}
|
||||
}
|
||||
|
||||
GCController.controllers().forEach { controller in
|
||||
print("wow controller")
|
||||
self.controllerconnected = true
|
||||
self.setupController(controller)
|
||||
}
|
||||
}
|
||||
|
||||
private func setupController(_ controller: GCController) {
|
||||
// Assign a unique ID to the controller, max 5 controllers
|
||||
if controllerIDs.count < 6, controllerIDs[controller] == nil {
|
||||
controllerIDs[controller] = controllerIDs.count
|
||||
}
|
||||
guard let controllerId = controllerIDs[controller] else { return }
|
||||
if let extendedGamepad = controller.extendedGamepad {
|
||||
// Handle extended gamepad
|
||||
extendedGamepad.dpad.up.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadUp, controllerId: controllerId) : self.touchUpInside(.directionalPadUp, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.dpad.down.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadDown, controllerId: controllerId) : self.touchUpInside(.directionalPadDown, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.dpad.left.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadLeft, controllerId: controllerId) : self.touchUpInside(.directionalPadLeft, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.dpad.right.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadRight, controllerId: controllerId) : self.touchUpInside(.directionalPadRight, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonOptions?.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.minus, controllerId: controllerId) : self.touchUpInside(.minus, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonMenu.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.plus, controllerId: controllerId) : self.touchUpInside(.plus, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonA.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.A, controllerId: controllerId) : self.touchUpInside(.A, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonB.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.B, controllerId: controllerId) : self.touchUpInside(.B, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonX.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.X, controllerId: controllerId) : self.touchUpInside(.X, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonY.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.Y, controllerId: controllerId) : self.touchUpInside(.Y, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.leftShoulder.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.triggerL, controllerId: controllerId) : self.touchUpInside(.L, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.leftTrigger.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.triggerZL, controllerId: controllerId) : self.touchUpInside(.triggerZL, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.rightShoulder.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.triggerR, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.leftThumbstickButton?.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.L, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.rightThumbstickButton?.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.R, controllerId: controllerId) : self.touchUpInside(.triggerR, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.rightTrigger.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.triggerZR, controllerId: controllerId) : self.touchUpInside(.triggerZR, controllerId: controllerId)
|
||||
}
|
||||
extendedGamepad.buttonHome?.pressedChangedHandler = { button, value, pressed in
|
||||
if pressed {
|
||||
appui.exit()
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
extendedGamepad.leftThumbstick.valueChangedHandler = { dpad, x, y in
|
||||
self.appui.thumbstickMoved(analog: .left, x: x, y: y, controllerid: controllerId)
|
||||
}
|
||||
extendedGamepad.rightThumbstick.valueChangedHandler = { dpad, x, y in
|
||||
self.appui.thumbstickMoved(analog: .right, x: x, y: y, controllerid: controllerId)
|
||||
}
|
||||
if let motion = controller.motion {
|
||||
var lastTimestamp = Date().timeIntervalSince1970 // Initialize timestamp when motion starts
|
||||
motion.valueChangedHandler = { motion in
|
||||
// Get current time
|
||||
let currentTimestamp = Date().timeIntervalSince1970
|
||||
|
||||
// Update last timestamp
|
||||
lastTimestamp = currentTimestamp
|
||||
|
||||
// Get gyroscope data
|
||||
let gyroX = motion.rotationRate.x
|
||||
let gyroY = motion.rotationRate.y
|
||||
let gyroZ = motion.rotationRate.z
|
||||
|
||||
// Get accelerometer data
|
||||
let accelX = motion.gravity.x + motion.userAcceleration.x
|
||||
let accelY = motion.gravity.y + motion.userAcceleration.y
|
||||
let accelZ = motion.gravity.z + motion.userAcceleration.z
|
||||
|
||||
print("\(gyroX), \(gyroY), \(gyroZ), \(accelX), \(accelY), \(accelZ)")
|
||||
|
||||
// Call your gyroMoved function with the motion data
|
||||
appui.gyroMoved(x: Float(gyroX), y: Float(gyroY), z: Float(gyroZ), accelX: Float(accelX), accelY: Float(accelY), accelZ: Float(accelZ), controllerId: Int32(controllerId), deltaTimestamp: Int32(lastTimestamp))
|
||||
}
|
||||
}
|
||||
} else if let microGamepad = controller.microGamepad {
|
||||
// Handle micro gamepad
|
||||
microGamepad.dpad.up.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadUp, controllerId: controllerId) : self.touchUpInside(.directionalPadUp, controllerId: controllerId)
|
||||
}
|
||||
microGamepad.dpad.down.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadDown, controllerId: controllerId) : self.touchUpInside(.directionalPadDown, controllerId: controllerId)
|
||||
}
|
||||
microGamepad.dpad.left.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadLeft, controllerId: controllerId) : self.touchUpInside(.directionalPadLeft, controllerId: controllerId)
|
||||
}
|
||||
microGamepad.dpad.right.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.directionalPadRight, controllerId: controllerId) : self.touchUpInside(.directionalPadRight, controllerId: controllerId)
|
||||
}
|
||||
microGamepad.buttonA.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.A, controllerId: controllerId) : self.touchUpInside(.A, controllerId: controllerId)
|
||||
}
|
||||
microGamepad.buttonX.pressedChangedHandler = { button, value, pressed in
|
||||
pressed ? self.touchDown(.X, controllerId: controllerId) : self.touchUpInside(.X, controllerId: controllerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func touchDown(_ button: VirtualControllerButtonType, controllerId: Int) {
|
||||
appui.virtualControllerButtonDown(button: button, controllerid: controllerId) }
|
||||
|
||||
private func touchUpInside(_ button: VirtualControllerButtonType, controllerId: Int) {
|
||||
appui.virtualControllerButtonUp(button: button, controllerid: controllerId)
|
||||
}
|
||||
}
|
||||
|
||||
struct OnScreenController: View {
|
||||
@State var geometry: GeometryProxy
|
||||
var body: some View {
|
||||
if geometry.size.height > geometry.size.width && UIDevice.current.userInterfaceIdiom != .pad {
|
||||
// portrait
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack {
|
||||
VStack {
|
||||
ShoulderButtonsViewLeft()
|
||||
ZStack {
|
||||
Joystick()
|
||||
DPadView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
VStack {
|
||||
ShoulderButtonsViewRight()
|
||||
ZStack {
|
||||
Joystick(iscool: true) // hope this works
|
||||
ABXYView()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
HStack {
|
||||
ButtonView(button: .plus).padding(.horizontal, 40)
|
||||
ButtonView(button: .minus).padding(.horizontal, 40)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, geometry.size.height / 3.2) // very broken
|
||||
}
|
||||
} else {
|
||||
// could be landscape
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
ButtonView(button: .home)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
HStack {
|
||||
|
||||
// gotta fuckin add + and - now
|
||||
VStack {
|
||||
ShoulderButtonsViewLeft()
|
||||
ZStack {
|
||||
Joystick()
|
||||
DPadView()
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
Spacer()
|
||||
VStack {
|
||||
Spacer()
|
||||
ButtonView(button: .plus) // Adding the + button
|
||||
}
|
||||
VStack {
|
||||
Spacer()
|
||||
ButtonView(button: .minus) // Adding the - button
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
VStack {
|
||||
ShoulderButtonsViewRight()
|
||||
ZStack {
|
||||
Joystick(iscool: true) // hope this work s
|
||||
ABXYView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, geometry.size.height / 11) // also extremally broken (
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShoulderButtonsViewLeft: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
ButtonView(button: .triggerZL).padding(.horizontal)
|
||||
ButtonView(button: .triggerL).padding(.horizontal)
|
||||
}
|
||||
.frame(width: 160, height: 20)
|
||||
}
|
||||
}
|
||||
|
||||
struct ShoulderButtonsViewRight: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
ButtonView(button: .triggerR).padding(.horizontal)
|
||||
ButtonView(button: .triggerZR).padding(.horizontal)
|
||||
}
|
||||
.frame(width: 160, height: 20)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct DPadView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
ButtonView(button: .directionalPadUp)
|
||||
HStack {
|
||||
ButtonView(button: .directionalPadLeft)
|
||||
Spacer(minLength: 20)
|
||||
ButtonView(button: .directionalPadRight)
|
||||
}
|
||||
ButtonView(button: .directionalPadDown).padding(.horizontal)
|
||||
}
|
||||
.frame(width: 145, height: 145)
|
||||
}
|
||||
}
|
||||
|
||||
struct ABXYView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
ButtonView(button: .X)
|
||||
HStack {
|
||||
ButtonView(button: .Y)
|
||||
Spacer(minLength: 20)
|
||||
ButtonView(button: .A)
|
||||
}
|
||||
ButtonView(button: .B).padding(.horizontal)
|
||||
}
|
||||
.frame(width: 145, height: 145)
|
||||
}
|
||||
}
|
||||
|
||||
class Haptics {
|
||||
static let shared = Haptics()
|
||||
private init() { }
|
||||
func play(_ feedbackStyle: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
print("haptics")
|
||||
UIImpactFeedbackGenerator(style: feedbackStyle).impactOccurred()
|
||||
}
|
||||
func notify(_ feedbackType: UINotificationFeedbackGenerator.FeedbackType) {
|
||||
UINotificationFeedbackGenerator().notificationOccurred(feedbackType)
|
||||
}
|
||||
}
|
||||
|
||||
struct ButtonView: View {
|
||||
var button: VirtualControllerButtonType
|
||||
@StateObject private var viewModel: EmulationViewModel = EmulationViewModel()
|
||||
let appui = AppUI.shared
|
||||
@State var mtkView: MTKView?
|
||||
@State var width: CGFloat = 45
|
||||
@State var height: CGFloat = 45
|
||||
@State var isPressed = false
|
||||
var id: Int {
|
||||
if onscreenjoy {
|
||||
return 8
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
Image(systemName: buttonText)
|
||||
.resizable()
|
||||
.frame(width: width, height: height)
|
||||
.foregroundColor(colorScheme == .dark ? Color.gray : Color.gray)
|
||||
.opacity(isPressed ? 0.5 : 1)
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0)
|
||||
.onChanged { _ in
|
||||
if !self.isPressed {
|
||||
self.isPressed = true
|
||||
DispatchQueue.main.async {
|
||||
if button == .home {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
appui.exit()
|
||||
} else {
|
||||
appui.virtualControllerButtonDown(button: button, controllerid: id)
|
||||
Haptics.shared.play(.heavy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onEnded { _ in
|
||||
self.isPressed = false
|
||||
DispatchQueue.main.async {
|
||||
if button != .home {
|
||||
appui.virtualControllerButtonUp(button: button, controllerid: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.onAppear() {
|
||||
if button == .triggerL || button == .triggerZL || button == .triggerZR || button == .triggerR {
|
||||
width = 65
|
||||
}
|
||||
|
||||
|
||||
if button == .minus || button == .plus || button == .home {
|
||||
width = 35
|
||||
height = 35
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var buttonText: String {
|
||||
switch button {
|
||||
case .A: return "a.circle.fill"
|
||||
case .B: return "b.circle.fill"
|
||||
case .X: return "x.circle.fill"
|
||||
case .Y: return "y.circle.fill"
|
||||
case .directionalPadUp: return "arrowtriangle.up.circle.fill"
|
||||
case .directionalPadDown: return "arrowtriangle.down.circle.fill"
|
||||
case .directionalPadLeft: return "arrowtriangle.left.circle.fill"
|
||||
case .directionalPadRight: return "arrowtriangle.right.circle.fill"
|
||||
case .triggerZL: return"zl.rectangle.roundedtop.fill"
|
||||
case .triggerZR: return "zr.rectangle.roundedtop.fill"
|
||||
case .triggerL: return "l.rectangle.roundedbottom.fill"
|
||||
case .triggerR: return "r.rectangle.roundedbottom.fill"
|
||||
case .plus: return "plus.circle.fill"
|
||||
case .minus: return "minus.circle.fill"
|
||||
case .home: return "house.circle.fill"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmulationView: View {
|
||||
@StateObject private var viewModel: EmulationViewModel
|
||||
@State var controllerconnected = false
|
||||
@State var appui = AppUI.shared
|
||||
var device: MTLDevice? = MTLCreateSystemDefaultDevice()
|
||||
@State var CaLayer: CAMetalLayer?
|
||||
@State var ShowPopup: Bool = false
|
||||
@State var mtkview: MTKView?
|
||||
@State private var thread: Thread!
|
||||
@State var uiTabBarController: UITabBarController?
|
||||
@State private var isFirstFrameShown = false
|
||||
@State private var timer: Timer?
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
|
||||
init(game: EmulationGame?) {
|
||||
_viewModel = StateObject(wrappedValue: EmulationViewModel())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
MetalView(device: device) { view in
|
||||
DispatchQueue.main.async {
|
||||
if let metalView = view as? MTKView {
|
||||
mtkview = metalView
|
||||
viewModel.configureAppUI(with: metalView)
|
||||
} else {
|
||||
print("Error: view is not of type MTKView")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onRotate { size in
|
||||
viewModel.handleOrientationChange()
|
||||
}
|
||||
ControllerView()
|
||||
}
|
||||
.overlay(
|
||||
// Loading screen overlay on top of MetalView
|
||||
Group {
|
||||
if !isFirstFrameShown {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
)
|
||||
.onAppear {
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
startPollingFirstFrameShowed()
|
||||
}
|
||||
.onDisappear {
|
||||
stopPollingFirstFrameShowed()
|
||||
uiTabBarController?.tabBar.isHidden = false
|
||||
viewModel.customButtonTapped()
|
||||
}
|
||||
.navigationBarBackButtonHidden(true)
|
||||
// .introspect(.tabView, on: .iOS(.v13, .v14, .v15, .v16, .v17)) { (tabBarController) in
|
||||
// tabBarController.tabBar.isHidden = true
|
||||
// uiTabBarController = tabBarController
|
||||
// }
|
||||
}
|
||||
|
||||
private func startPollingFirstFrameShowed() {
|
||||
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
|
||||
if appui.FirstFrameShowed() {
|
||||
withAnimation {
|
||||
isFirstFrameShown = true
|
||||
}
|
||||
stopPollingFirstFrameShowed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopPollingFirstFrameShowed() {
|
||||
timer?.invalidate()
|
||||
timer = nil
|
||||
print("Timer Invalidated")
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadingView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
ProgressView("Loading...")
|
||||
// .font(.system(size: 90))
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
.padding()
|
||||
Text("Please wait while the game loads.")
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color.black.opacity(0.8))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func onRotate(perform action: @escaping (CGSize) -> Void) -> some View {
|
||||
self.modifier(DeviceRotationModifier(action: action))
|
||||
}
|
||||
}
|
||||
|
||||
struct DeviceRotationModifier: ViewModifier {
|
||||
let action: (CGSize) -> Void
|
||||
@State var startedfirst: Bool = false
|
||||
|
||||
func body(content: Content) -> some View { content
|
||||
.background(GeometryReader { geometry in
|
||||
Color.clear
|
||||
.preference(key: SizePreferenceKey.self, value: geometry.size)
|
||||
})
|
||||
.onPreferenceChange(SizePreferenceKey.self) { newSize in
|
||||
if startedfirst {
|
||||
action(newSize)
|
||||
} else {
|
||||
startedfirst = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SizePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: CGSize = .zero
|
||||
|
||||
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
||||
value = nextValue()
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// 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
|
||||
@@ -1,82 +0,0 @@
|
||||
// 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();
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct Core : Comparable, Hashable {
|
||||
|
||||
let name = "Yuzu"
|
||||
var games: [EmulationGame]
|
||||
let root: URL
|
||||
|
||||
static func < (lhs: Core, rhs: Core) -> Bool {
|
||||
lhs.name < rhs.name
|
||||
}
|
||||
|
||||
func AddFirmware(at fileURL: URL) {
|
||||
do {
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let destinationURL = documentsDirectory.appendingPathComponent("nand/system/Contents/registered")
|
||||
if !fileManager.fileExists(atPath: destinationURL.path) {
|
||||
try fileManager.createDirectory(at: destinationURL, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
//try Zip.unzipFile(fileURL, destination: destinationURL, overwrite: true, password: nil)
|
||||
print("File unzipped successfully to \(destinationURL.path)")
|
||||
} catch {
|
||||
print("Failed to unzip file: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LibManError : Error {
|
||||
case ripenum, urlgobyebye
|
||||
}
|
||||
|
||||
class LibraryManager {
|
||||
static let shared = LibraryManager()
|
||||
let documentdir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("roms", conformingTo: .folder)
|
||||
|
||||
func removerom(_ game: EmulationGame) throws {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: game.fileURL)
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func homebrewroms() -> [EmulationGame] {
|
||||
// TODO(lizzie): this is horrible
|
||||
var urls: [URL] = []
|
||||
let sdmc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sdmc", conformingTo: .folder)
|
||||
let sdfolder = sdmc.appendingPathComponent("switch", conformingTo: .folder)
|
||||
if FileManager.default.fileExists(atPath: sdfolder.path) {
|
||||
if let dirContents = FileManager.default.enumerator(at: sdmc, includingPropertiesForKeys: nil, options: []) {
|
||||
do {
|
||||
try dirContents.forEach() { files in
|
||||
if let file = files as? URL {
|
||||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
|
||||
if let isfile = getaboutfile.isRegularFile, isfile {
|
||||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
|
||||
urls.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) {
|
||||
do {
|
||||
try dirContents.forEach() { files in
|
||||
if let file = files as? URL {
|
||||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
|
||||
if let isfile = getaboutfile.isRegularFile, isfile {
|
||||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
|
||||
urls.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("damn")
|
||||
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) {
|
||||
do {
|
||||
try dirContents.forEach() { files in
|
||||
if let file = files as? URL {
|
||||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
|
||||
if let isfile = getaboutfile.isRegularFile, isfile {
|
||||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
|
||||
urls.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) {
|
||||
do {
|
||||
try dirContents.forEach() { files in
|
||||
if let file = files as? URL {
|
||||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
|
||||
if let isfile = getaboutfile.isRegularFile, isfile {
|
||||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
|
||||
urls.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
func games(from urls: [URL]) -> [EmulationGame] {
|
||||
var pomelogames: [EmulationGame] = []
|
||||
pomelogames = urls.reduce(into: [EmulationGame]()) { partialResult, element in
|
||||
let iscustom = element.startAccessingSecurityScopedResource()
|
||||
let information = AppUI.shared.information(for: element)
|
||||
let game = EmulationGame(developer: information.developer, fileURL: element, imageData: information.iconData, title: information.title)
|
||||
if iscustom {
|
||||
element.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
partialResult.append(game)
|
||||
}
|
||||
return pomelogames
|
||||
}
|
||||
return games(from: urls)
|
||||
}
|
||||
|
||||
func library() throws -> Core {
|
||||
func getromsfromdir() throws -> [URL] {
|
||||
guard let dirContents = FileManager.default.enumerator(at: documentdir, includingPropertiesForKeys: nil, options: []) else {
|
||||
print("uhoh how unfortunate for some reason FileManager.default.enumerator aint workin")
|
||||
throw LibManError.ripenum
|
||||
}
|
||||
let appui = AppUI.shared
|
||||
var urls: [URL] = []
|
||||
try dirContents.forEach() { files in
|
||||
if let file = files as? URL {
|
||||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
|
||||
if let isfile = getaboutfile.isRegularFile, isfile {
|
||||
if ["nca", "nro", "nsp", "nso", "xci"].contains(file.pathExtension.lowercased()) {
|
||||
urls.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let sdmc = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("sdmc", conformingTo: .folder)
|
||||
let sdfolder = sdmc.appendingPathComponent("switch", conformingTo: .folder)
|
||||
if FileManager.default.fileExists(atPath: sdfolder.path) {
|
||||
if let dirContents = FileManager.default.enumerator(at: sdmc, includingPropertiesForKeys: nil, options: []) {
|
||||
try dirContents.forEach() { files in
|
||||
if let file = files as? URL {
|
||||
let getaboutfile = try file.resourceValues(forKeys: [.isRegularFileKey])
|
||||
if let isfile = getaboutfile.isRegularFile, isfile {
|
||||
if ["nso", "nro"].contains(file.pathExtension.lowercased()) {
|
||||
urls.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
appui.insert(games: urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
func games(from urls: [URL], core: inout Core) {
|
||||
core.games = urls.reduce(into: [EmulationGame]()) { partialResult, element in
|
||||
let iscustom = element.startAccessingSecurityScopedResource()
|
||||
let information = AppUI.shared.information(for: element)
|
||||
let game = EmulationGame(developer: information.developer, fileURL: element, imageData: information.iconData, title: information.title)
|
||||
if iscustom {
|
||||
element.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
partialResult.append(game)
|
||||
}
|
||||
}
|
||||
let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
var YuzuCore = Core(games: [], root: directory)
|
||||
games(from: try getromsfromdir(), core: &YuzuCore)
|
||||
return YuzuCore
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
import Combine
|
||||
|
||||
struct SettingsView: View {
|
||||
@ObservedObject var core: EmulationViewModel
|
||||
@State var showprompt = false
|
||||
|
||||
@AppStorage("icon") var iconused = 1
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GameIconView: View {
|
||||
var game: EmulationGame
|
||||
@Binding var selectedGame: EmulationGame?
|
||||
@State var startgame: Bool = false
|
||||
@State var timesTapped: Int = 0
|
||||
|
||||
var isSelected: Bool {
|
||||
selectedGame == game
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(
|
||||
destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar),
|
||||
isActive: $startgame,
|
||||
label: {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
VStack(spacing: 5) {
|
||||
if isSelected {
|
||||
Text(game.title)
|
||||
.foregroundColor(.blue)
|
||||
.font(.title2)
|
||||
}
|
||||
if let uiImage = UIImage(data: game.imageData) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: isSelected ? 200 : 180, height: isSelected ? 200 : 180)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
isSelected ? RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.blue, lineWidth: 5)
|
||||
: nil
|
||||
)
|
||||
.onTapGesture {
|
||||
if isSelected {
|
||||
startgame = true
|
||||
print(isSelected)
|
||||
}
|
||||
if !isSelected {
|
||||
selectedGame = game
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Image(systemName: "questionmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 200, height: 200)
|
||||
.cornerRadius(10)
|
||||
.onTapGesture { selectedGame = game }
|
||||
}
|
||||
}
|
||||
.frame(width: 200, height: 250)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove or refactor HomeView and GameCarouselView to not use Core or BottomMenuView(core: core) with Core.
|
||||
// If these are not used in the iOS UI, comment them out to avoid build errors.
|
||||
// struct HomeView: View {
|
||||
// @State private var selectedGame: EmulationGame? = nil
|
||||
|
||||
// @State var core: Core
|
||||
|
||||
// init(selectedGame: EmulationGame? = nil, core: Core) {
|
||||
// _core = State(wrappedValue: core)
|
||||
// self.selectedGame = selectedGame
|
||||
// refreshcore()
|
||||
// }
|
||||
|
||||
// var body: some View {
|
||||
// NavigationStack {
|
||||
// GeometryReader { geometry in
|
||||
// VStack {
|
||||
// GameCarouselView(core: core, selectedGame: $selectedGame)
|
||||
// Spacer()
|
||||
// BottomMenuView(core: core)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .background(Color.gray.opacity(0.1))
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// .onAppear {
|
||||
// refreshcore()
|
||||
// if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
// let romsFolderURL = documentsDirectory.appendingPathComponent("roms")
|
||||
// let folderMonitor = FolderMonitor(folderURL: romsFolderURL) {
|
||||
// do {
|
||||
// core = Core(games: [], root: documentsDirectory)
|
||||
// core = try LibraryManager.shared.library()
|
||||
// } catch {
|
||||
// print("Error refreshing core: \(error)")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// func refreshcore() {
|
||||
// print("Loading library...")
|
||||
// do {
|
||||
// core = try LibraryManager.shared.library()
|
||||
// print(core.games)
|
||||
// } catch {
|
||||
// print("Failed to fetch library: \(error)")
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// struct GameCarouselView: View {
|
||||
// // let games: [EmulationGame]
|
||||
// @State var core: Core
|
||||
// @Binding var selectedGame: EmulationGame?
|
||||
// var body: some View {
|
||||
// ScrollView(.horizontal, showsIndicators: false) {
|
||||
// HStack(spacing: 20) {
|
||||
// ForEach(core.games) { game in
|
||||
// GameIconView(game: game, selectedGame: $selectedGame)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -1,171 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
||||
struct GameButtonListView: View {
|
||||
var game: EmulationGame
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 15) {
|
||||
if let image = UIImage(data: game.imageData) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(8)
|
||||
} else {
|
||||
Image(systemName: "photo")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(game.title)
|
||||
.font(.headline)
|
||||
.foregroundColor(colorScheme == .dark ? Color.white : Color.black)
|
||||
Text(game.developer)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
|
||||
struct GameListView: View {
|
||||
@State var core: Core
|
||||
@State private var searchText = ""
|
||||
@State var game: Int = 1
|
||||
@State var startgame: Bool = false
|
||||
@Binding var isGridView: Bool
|
||||
@State var showAlert = false
|
||||
@State var alertMessage: Alert? = nil
|
||||
|
||||
var body: some View {
|
||||
let filteredGames = core.games.filter { game in
|
||||
guard let EmulationGame = game as? EmulationGame else { return false }
|
||||
return searchText.isEmpty || EmulationGame.title.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
ScrollView {
|
||||
VStack {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
if isGridView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) {
|
||||
ForEach(0..<filteredGames.count, id: \.self) { index in
|
||||
let game = filteredGames[index] // Use filteredGames here
|
||||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
|
||||
// GameButtonView(game: game)
|
||||
// .frame(maxWidth: .infinity, minHeight: 200)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
do {
|
||||
try LibraryManager.shared.removerom(filteredGames[index])
|
||||
} catch {
|
||||
showAlert = true
|
||||
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription))
|
||||
}
|
||||
}) {
|
||||
Text("Remove")
|
||||
}
|
||||
Button(action: {
|
||||
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") {
|
||||
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil)
|
||||
}
|
||||
}) {
|
||||
if ProcessInfo.processInfo.isMacCatalystApp {
|
||||
Text("Open in Finder")
|
||||
} else {
|
||||
Text("Open in Files")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
|
||||
Text("Launch")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyVStack() {
|
||||
ForEach(0..<filteredGames.count, id: \.self) { index in
|
||||
let game = filteredGames[index] // Use filteredGames here
|
||||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
|
||||
GameButtonListView(game: game)
|
||||
.frame(maxWidth: .infinity, minHeight: 75)
|
||||
}
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
do {
|
||||
try LibraryManager.shared.removerom(filteredGames[index])
|
||||
try FileManager.default.removeItem(atPath: game.fileURL.path)
|
||||
} catch {
|
||||
showAlert = true
|
||||
alertMessage = Alert(title: Text("Unable to Remove Game"), message: Text(error.localizedDescription))
|
||||
}
|
||||
}) {
|
||||
Text("Remove")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
if let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appending(path: "roms") {
|
||||
UIApplication.shared.open(documentsURL, options: [:], completionHandler: nil)
|
||||
}
|
||||
}) {
|
||||
if ProcessInfo.processInfo.isMacCatalystApp {
|
||||
Text("Open in Finder")
|
||||
} else {
|
||||
Text("Open in Files")
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: EmulationView(game: game).toolbar(.hidden, for: .tabBar)) {
|
||||
Text("Launch")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText)
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
refreshcore()
|
||||
|
||||
if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
let romsFolderURL = documentsDirectory.appendingPathComponent("roms")
|
||||
|
||||
let folderMonitor = FolderMonitor(folderURL: romsFolderURL) {
|
||||
do {
|
||||
core = Core(games: [], root: documentsDirectory)
|
||||
core = try LibraryManager.shared.library()
|
||||
} catch {
|
||||
print("Error refreshing core: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showAlert) {
|
||||
alertMessage ?? Alert(title: Text("Error Not Found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshcore() {
|
||||
do {
|
||||
core = try LibraryManager.shared.library()
|
||||
} catch {
|
||||
print("Failed to fetch library: \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
public struct Joystick: View {
|
||||
@State var iscool: Bool? = nil
|
||||
var id: Int {
|
||||
if onscreenjoy {
|
||||
return 8
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@AppStorage("onscreenhandheld") var onscreenjoy: Bool = false
|
||||
|
||||
let appui = AppUI.shared
|
||||
|
||||
@ObservedObject public var joystickMonitor = JoystickMonitor()
|
||||
private let dragDiameter: CGFloat = 160
|
||||
private let shape: JoystickShape = .circle
|
||||
|
||||
public var body: some View {
|
||||
VStack{
|
||||
JoystickBuilder(
|
||||
monitor: self.joystickMonitor,
|
||||
width: self.dragDiameter,
|
||||
shape: .circle,
|
||||
background: {
|
||||
// Example Background
|
||||
RoundedRectangle(cornerRadius: 8).fill(Color.gray.opacity(0))
|
||||
},
|
||||
foreground: {
|
||||
// Example Thumb
|
||||
Circle().fill(Color.gray)
|
||||
},
|
||||
locksInPlace: false)
|
||||
.onChange(of: self.joystickMonitor.xyPoint) { newValue in
|
||||
let scaledX = Float(newValue.x)
|
||||
let scaledY = Float(-newValue.y) // my dumbass broke this by having -y instead of y :/ (well it appears that with the new joystick code, its supposed to be -y)
|
||||
joystickMonitor.objectWillChange
|
||||
print("Joystick Position: (\(scaledX), \(scaledY))")
|
||||
|
||||
if iscool != nil {
|
||||
appui.thumbstickMoved(analog: .right, x: scaledX, y: scaledY, controllerid: id)
|
||||
} else {
|
||||
appui.thumbstickMoved(analog: .left, x: scaledX, y: scaledY, controllerid: id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2024 Pomelo, Stossy11
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
class KeyboardHostingController<Content: View>: UIHostingController<Content> {
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
becomeFirstResponder() // Make sure the view can become the first responder
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return [
|
||||
UIKeyCommand(input: UIKeyInputUpArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: UIKeyInputDownArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: UIKeyInputLeftArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: UIKeyInputRightArrow, modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: "w", modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: "s", modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: "a", modifierFlags: [], action: #selector(handleKeyCommand)),
|
||||
UIKeyCommand(input: "d", modifierFlags: [], action: #selector(handleKeyCommand))
|
||||
]
|
||||
}
|
||||
|
||||
@objc func handleKeyCommand(_ sender: UIKeyCommand) {
|
||||
if let input = sender.input {
|
||||
switch input {
|
||||
case UIKeyInputUpArrow:
|
||||
print("Up Arrow Pressed")
|
||||
case UIKeyInputDownArrow:
|
||||
print("Down Arrow Pressed")
|
||||
case UIKeyInputLeftArrow:
|
||||
print("Left Arrow Pressed")
|
||||
case UIKeyInputRightArrow:
|
||||
print("Right Arrow Pressed")
|
||||
case "w":
|
||||
print("W Key Pressed")
|
||||
case "s":
|
||||
print("S Key Pressed")
|
||||
case "a":
|
||||
print("A Key Pressed")
|
||||
case "d":
|
||||
print("D Key Pressed")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyboardSupportView: UIViewControllerRepresentable {
|
||||
let content: Text
|
||||
|
||||
func makeUIViewController(context: Context) -> KeyboardHostingController<Text> {
|
||||
return KeyboardHostingController(rootView: content)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: KeyboardHostingController<Text>, context: Context) {
|
||||
// Handle any updates needed
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyboardView: View {
|
||||
var body: some View {
|
||||
KeyboardSupportView(content: Text(""))
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
struct LibraryView: View {
|
||||
@State private var selectedGame: EmulationGame? = nil
|
||||
@Binding var urlgame: EmulationGame?
|
||||
@ObservedObject var core: EmulationViewModel
|
||||
|
||||
var binding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { urlgame != nil },
|
||||
set: { newValue in
|
||||
if !newValue {
|
||||
urlgame = nil
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
VStack {
|
||||
TopBarView()
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .pad {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [
|
||||
GridItem(.adaptive(minimum: 140), spacing: 10)
|
||||
], spacing: 10) {
|
||||
ForEach(core.games, id: \EmulationGame.id) { game in
|
||||
GameCardView(game: game)
|
||||
.frame(width: 140, height: 160)
|
||||
.onTapGesture {
|
||||
selectedGame = game
|
||||
urlgame = game
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
BottomMenuView(core: core)
|
||||
}
|
||||
|
||||
NavigationLink(
|
||||
destination: EmulationView(game: urlgame),
|
||||
isActive: binding,
|
||||
label: {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.hidden()
|
||||
}
|
||||
}
|
||||
.background(Color.gray.opacity(0.1))
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.onAppear {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct GameCardView: View {
|
||||
let game: EmulationGame
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
.frame(height: 120)
|
||||
|
||||
Text(game.title)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 4)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// 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
|
||||
|
||||
infix operator --: LogicalDisjunctionPrecedence
|
||||
|
||||
func --(lhs: Bool, rhs: Bool) -> Bool {
|
||||
return lhs || rhs
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct PomeloApp: App {
|
||||
@StateObject private var core = EmulationViewModel(game: nil)
|
||||
var body: some Scene {
|
||||
WindowGroup { ContentView(core: core) }
|
||||
}
|
||||
}
|
||||
@@ -1,309 +0,0 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// SPDX-FileCopyrightText: Copyright 2021 Eden Emulator Project
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public protocol PolarCoordinate {
|
||||
/// The direction the thumb handle is pointing, up is 0° and right is 90°
|
||||
var degrees: CGFloat { get set }
|
||||
/// The thumb handle's distance from the center/origin
|
||||
var distance: CGFloat { get set }
|
||||
}
|
||||
|
||||
public struct PolarPoint: PolarCoordinate {
|
||||
/// The direction from origin, up is 0° and right is 90°
|
||||
public var degrees: CGFloat
|
||||
/// The distance from center/origin
|
||||
public var distance: CGFloat
|
||||
|
||||
public static let zero: PolarPoint = PolarPoint(degrees: 0, distance: 0)
|
||||
|
||||
public init(degrees: CGFloat, distance: CGFloat) {
|
||||
self.degrees = degrees
|
||||
self.distance = distance
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of background shape used for the touch/click hitbox
|
||||
///
|
||||
/// Rect will allow every coordinate to be used by the joystick's thumb position
|
||||
/// Circle will limit the position output to the circular area defined by the midpoint and diameter/width
|
||||
public enum JoystickShape {
|
||||
/// Will allow the cursor to go from 0,0 to 0, width, and width, 0
|
||||
case rect
|
||||
/// Will limit the curser to a circular area surrounding the center of the joystick
|
||||
/// This cannot reach the corners but can reach min and max for both the x and y axis any edge's midpoint
|
||||
case circle
|
||||
}
|
||||
|
||||
public extension CGPoint {
|
||||
internal static func +(_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
|
||||
}
|
||||
|
||||
internal static func -(_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint {
|
||||
return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
|
||||
}
|
||||
|
||||
internal static func *(_ lhs: CGPoint, _ rhs: CGFloat) -> CGPoint {
|
||||
return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
|
||||
}
|
||||
|
||||
func distance(to point: CGPoint) -> CGFloat {
|
||||
return sqrt(pow((point.x - x), 2) + pow((point.y - y), 2))
|
||||
}
|
||||
|
||||
func getPointOnCircle(radius: CGFloat, radian: CGFloat) -> CGPoint {
|
||||
let x = self.x + radius * cos(radian)
|
||||
let y = self.y + radius * sin(radian)
|
||||
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
func getRadian(pointOnCircle: CGPoint) -> CGFloat {
|
||||
let originX = pointOnCircle.x - self.x
|
||||
let originY = pointOnCircle.y - self.y
|
||||
var radian = atan2(originY, originX)
|
||||
while radian < 0 {
|
||||
radian += CGFloat(2 * Double.pi)
|
||||
}
|
||||
return radian
|
||||
}
|
||||
|
||||
func getPolarPoint(from origin: CGPoint = CGPoint.zero) -> PolarPoint {
|
||||
let deltaX = self.x - origin.x
|
||||
let deltaY = self.y - origin.y
|
||||
let radians = -1 * atan2(deltaY, deltaX)
|
||||
let degrees = radians * (180.0 / CGFloat.pi)
|
||||
let distance = self.distance(to: origin)
|
||||
|
||||
guard degrees < 0 else {
|
||||
return PolarPoint(degrees: degrees, distance: distance)
|
||||
}
|
||||
return PolarPoint(degrees: degrees + 360.0, distance: distance)
|
||||
}
|
||||
}
|
||||
|
||||
public class JoystickMonitor: ObservableObject {
|
||||
@Published public var xyPoint: CGPoint = .zero
|
||||
@Published public var polarPoint: PolarPoint = .zero
|
||||
|
||||
public init() { }
|
||||
}
|
||||
|
||||
/// A convenience SwiftUI ViewModifier to make a view behave like a Joystick
|
||||
public struct JoystickGestureRecognizer: ViewModifier {
|
||||
@ObservedObject public var joystickMonitor: JoystickMonitor
|
||||
/// The size of the control area in which the drag gesture is monitored and reported, is diameter for a circular Joystick
|
||||
private var width: CGFloat
|
||||
/// The shape of the hitbox for the position output of the Joystick Thumb position
|
||||
private var shapeType: JoystickShape
|
||||
/// The center point of the Joystick where it goes to rest when not being used in `locksInPlace` is false
|
||||
private let midPoint: CGPoint
|
||||
/// Determines whether or not the Joystick Thumb control goes back to the center point when released
|
||||
private let locksInPlace: Bool
|
||||
@Binding private(set) public var thumbPosition: CGPoint
|
||||
|
||||
/// Creates a custom joystick with the following configuration
|
||||
///
|
||||
/// parameter joystickMonitor: An object used to monitor the valid position of the thumb on the Joystick
|
||||
/// parameter width: Width of the joystick control area, for a circular Joystick this is the diameter
|
||||
/// parameter type: Shape of the hitbox for the position output of the Joystick Thumb position
|
||||
/// parameter background: The view displayed as the Joystick background
|
||||
/// parameter foreground: The view displayed as the Joystick Thumb Control
|
||||
/// parameter locksInPlace: Determines if the thumb control returns to the center point when released
|
||||
public init(thumbPosition: Binding<CGPoint>, monitor: JoystickMonitor, width: CGFloat, type: JoystickShape, locksInPlace locks: Bool = false) {
|
||||
self.joystickMonitor = monitor
|
||||
self._thumbPosition = thumbPosition
|
||||
self.width = width
|
||||
self.midPoint = CGPoint(x: width / 2, y: width / 2)
|
||||
self.shapeType = type
|
||||
self.locksInPlace = locks
|
||||
}
|
||||
|
||||
/// Produce the correct shape of Joystick
|
||||
public func body(content: Content) -> some View {
|
||||
switch self.shapeType {
|
||||
case .rect:
|
||||
rectBody(content)
|
||||
case .circle:
|
||||
circleBody(content)
|
||||
}
|
||||
}
|
||||
|
||||
internal func getValidThumbCoordinate(for value: inout CGFloat) {
|
||||
if value <= 0 {
|
||||
value = 0
|
||||
} else if value > width {
|
||||
value = self.width
|
||||
}
|
||||
}
|
||||
|
||||
internal func validateCoordinate(_ emitPoint: inout CGPoint) {
|
||||
emitPoint = emitPoint * 2
|
||||
if emitPoint.x > width {
|
||||
emitPoint.x = width
|
||||
} else if emitPoint.x < -width {
|
||||
emitPoint.x = -width
|
||||
}
|
||||
if emitPoint.y > width {
|
||||
emitPoint.y = width
|
||||
} else if emitPoint.y < -width {
|
||||
emitPoint.y = -width
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the coordinates of the user's thumb to the JoystickMonitor, which emits an object change since it is an observable
|
||||
internal func emitPosition(for xyPoint: CGPoint) {
|
||||
var emitPoint = xyPoint
|
||||
validateCoordinate(&emitPoint)
|
||||
self.joystickMonitor.xyPoint = emitPoint
|
||||
self.joystickMonitor.polarPoint = emitPoint.getPolarPoint(from: self.midPoint)
|
||||
}
|
||||
|
||||
/// Provides a Rectangular area in which the Joystick control can move within and report values for
|
||||
///
|
||||
/// - parameter content: The view for which to apply the Joystick listener/DragGesture
|
||||
public func rectBody(_ content: Content) -> some View {
|
||||
content
|
||||
.contentShape(Rectangle())
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.onChanged({ value in
|
||||
var thumbX = value.location.x
|
||||
var thumbY = value.location.y
|
||||
self.getValidThumbCoordinate(for: &thumbX)
|
||||
self.getValidThumbCoordinate(for: &thumbY)
|
||||
self.thumbPosition = CGPoint(x: thumbX, y: thumbY)
|
||||
let position = value.location - self.midPoint
|
||||
self.emitPosition(for: position)
|
||||
})
|
||||
.onEnded({ value in
|
||||
if !locksInPlace {
|
||||
self.thumbPosition = self.midPoint
|
||||
self.emitPosition(for: .zero)
|
||||
}
|
||||
})
|
||||
.exclusively(
|
||||
before:
|
||||
LongPressGesture(minimumDuration: 0.0, maximumDistance: 0.0)
|
||||
.onEnded({ _ in
|
||||
if !locksInPlace {
|
||||
self.thumbPosition = self.midPoint
|
||||
self.emitPosition(for: .zero)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Provides a Circular area in which the Joystick control can move within and report values forr
|
||||
///
|
||||
/// - parameter content: The view for which to apply the Joystick listener/DragGesture
|
||||
public func circleBody(_ content: Content) -> some View {
|
||||
content
|
||||
.contentShape(Circle())
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 0, coordinateSpace: .local)
|
||||
.onChanged() { value in
|
||||
let distance = self.midPoint.distance(to: value.location)
|
||||
if distance > self.width / 2 {
|
||||
// Limit to radius
|
||||
let k = (self.width / 2) / distance
|
||||
let position = (value.location - self.midPoint) * k
|
||||
// Order matters
|
||||
self.thumbPosition = position + self.midPoint
|
||||
self.emitPosition(for: position)
|
||||
} else {
|
||||
self.thumbPosition = value.location
|
||||
let position = value.location - self.midPoint
|
||||
self.emitPosition(for: position)
|
||||
}
|
||||
}
|
||||
.onEnded({ value in
|
||||
if !locksInPlace {
|
||||
self.thumbPosition = self.midPoint
|
||||
self.emitPosition(for: .zero)
|
||||
}
|
||||
})
|
||||
.exclusively(
|
||||
before:
|
||||
LongPressGesture(minimumDuration: 0.0, maximumDistance: 0.0)
|
||||
.onEnded({ _ in
|
||||
if !locksInPlace {
|
||||
self.thumbPosition = self.midPoint
|
||||
self.emitPosition(for: .zero)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A convenience SwiftUI struct to make a Joystick control
|
||||
public struct JoystickBuilder<background: View, foreground: View>: View {
|
||||
/// The width of the joystick control area, for a circular Joystick this is the diameter
|
||||
private(set) public var width: CGFloat
|
||||
/// The shape of the hitbox for the position output of the Joystick Thumb position
|
||||
private(set) public var controlShape: JoystickShape
|
||||
|
||||
@ObservedObject private(set) public var joystickMonitor: JoystickMonitor
|
||||
@State private(set) public var thumbPosition: CGPoint = .zero
|
||||
/// The view displayed as the Joystick background, which also holds a Joystick DragGesture recognizer
|
||||
@ViewBuilder public var controlBackground: () -> background
|
||||
/// The view displayed as the Joystick Thumb Control, which also holds a Joystick DragGesture recognizer
|
||||
@ViewBuilder public var controlThumb: () -> foreground
|
||||
/// Determines whether or not the Joystick Thumb control goes back to the center point when released
|
||||
private let locksInPlace: Bool
|
||||
|
||||
/// Creates a custom joystick with two views that are passed to it
|
||||
///
|
||||
/// parameter position: Will output the valid position of the thumb on the Joystick, from 0 to width
|
||||
/// parameter width: Width of the joystick control area, for a circular Joystick this is the diameter
|
||||
/// parameter shape: Shape of the hitbox for the position output of the Joystick Thumb position
|
||||
/// parameter background: The view displayed as the Joystick background
|
||||
/// parameter foreground: The view displayed as the Joystick Thumb Control
|
||||
/// parameter locksInPlace: Determines if the thumb control returns to the center point when released
|
||||
public init(monitor: JoystickMonitor, width: CGFloat, shape: JoystickShape, @ViewBuilder background: @escaping () -> background, @ViewBuilder foreground: @escaping () -> foreground, locksInPlace locks: Bool) {
|
||||
self.joystickMonitor = monitor
|
||||
self.width = width
|
||||
self.controlShape = shape
|
||||
self.controlBackground = background
|
||||
self.controlThumb = foreground
|
||||
self.locksInPlace = locks
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
controlBackground()
|
||||
.frame(width: self.width, height: self.width)
|
||||
.joystickGestureRecognizer(thumbPosition: self.$thumbPosition, monitor: self.joystickMonitor, width: self.width, shape: self.controlShape, locksInPlace: self.locksInPlace)
|
||||
.overlay(
|
||||
controlThumb()
|
||||
.frame(width: self.width / 4, height: self.width / 4)
|
||||
.position(x: self.thumbPosition.x, y: self.thumbPosition.y)
|
||||
.joystickGestureRecognizer(thumbPosition: self.$thumbPosition, monitor: self.joystickMonitor, width: self.width, shape: self.controlShape, locksInPlace: self.locksInPlace)
|
||||
)
|
||||
.onAppear(perform: {
|
||||
let midPoint = self.width / 2
|
||||
self.thumbPosition = CGPoint(x: midPoint, y: midPoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public extension View {
|
||||
/// Convenience modifier for adding a Joystick recognizer
|
||||
/// Creates a custom joystick with the following configuration
|
||||
///
|
||||
/// parameter joystickMonitor: An object used to monitor the valid position of the thumb on the Joystick
|
||||
/// parameter width: Width of the joystick control area, for a circular Joystick this is the diameter
|
||||
/// parameter shape: (.rect || .circle) - Shape of the hitbox for the position output of the Joystick Thumb position
|
||||
/// parameter background: The view displayed as the Joystick background
|
||||
/// parameter foreground: The view displayed as the Joystick Thumb Control
|
||||
/// parameter locksInPlace: default false - Determines if the thumb control returns to the center point when released
|
||||
/// parameter locksInPlace: default false - Determines if the thumb control returns to the center point when released
|
||||
func joystickGestureRecognizer(thumbPosition: Binding<CGPoint>, monitor: JoystickMonitor, width: CGFloat, shape: JoystickShape, locksInPlace locks: Bool = false) -> some View {
|
||||
modifier(JoystickGestureRecognizer(thumbPosition: thumbPosition, monitor: monitor, width: width, type: shape, locksInPlace: locks))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// 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"
|
||||
@@ -59,6 +59,10 @@ add_library(video_core STATIC
|
||||
engines/maxwell_dma.h
|
||||
engines/puller.cpp
|
||||
engines/puller.h
|
||||
frame_gen/lossless_dll.cpp
|
||||
frame_gen/lossless_dll.h
|
||||
frame_gen/lsfg_translate.cpp
|
||||
frame_gen/lsfg_translate.h
|
||||
framebuffer_config.cpp
|
||||
framebuffer_config.h
|
||||
fsr.cpp
|
||||
@@ -121,6 +125,28 @@ add_library(video_core STATIC
|
||||
renderer_vulkan/present/anti_alias_pass.h
|
||||
renderer_vulkan/present/filters.cpp
|
||||
renderer_vulkan/present/filters.h
|
||||
renderer_vulkan/present/frame_gen.cpp
|
||||
renderer_vulkan/present/frame_gen.h
|
||||
renderer_vulkan/present/frame_gen_pacer.cpp
|
||||
renderer_vulkan/present/frame_gen_pacer.h
|
||||
renderer_vulkan/present/lsfg_alpha.cpp
|
||||
renderer_vulkan/present/lsfg_alpha.h
|
||||
renderer_vulkan/present/lsfg_beta.cpp
|
||||
renderer_vulkan/present/lsfg_beta.h
|
||||
renderer_vulkan/present/lsfg_chain.cpp
|
||||
renderer_vulkan/present/lsfg_chain.h
|
||||
renderer_vulkan/present/lsfg_common.cpp
|
||||
renderer_vulkan/present/lsfg_common.h
|
||||
renderer_vulkan/present/lsfg_delta.cpp
|
||||
renderer_vulkan/present/lsfg_delta.h
|
||||
renderer_vulkan/present/lsfg_gamma.cpp
|
||||
renderer_vulkan/present/lsfg_gamma.h
|
||||
renderer_vulkan/present/lsfg_generate.cpp
|
||||
renderer_vulkan/present/lsfg_generate.h
|
||||
renderer_vulkan/present/lsfg_mipmaps.cpp
|
||||
renderer_vulkan/present/lsfg_mipmaps.h
|
||||
renderer_vulkan/present/lsfg_shaders.cpp
|
||||
renderer_vulkan/present/lsfg_shaders.h
|
||||
renderer_vulkan/present/fsr.cpp
|
||||
renderer_vulkan/present/fsr.h
|
||||
renderer_vulkan/present/fxaa.cpp
|
||||
@@ -345,6 +371,7 @@ add_dependencies(video_core host_shaders)
|
||||
target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE})
|
||||
|
||||
target_link_libraries(video_core PRIVATE sirit::sirit)
|
||||
target_link_libraries(video_core PRIVATE dxbc)
|
||||
|
||||
# Header-only stuff needed by all dependent targets
|
||||
target_link_libraries(video_core PUBLIC Vulkan::Headers Vulkan::UtilityHeaders GPUOpen::VulkanMemoryAllocator)
|
||||
@@ -375,6 +402,7 @@ else()
|
||||
else()
|
||||
target_compile_options(video_core PRIVATE $<$<COMPILE_LANGUAGE:C,CXX>:-Wconversion>)
|
||||
endif()
|
||||
|
||||
target_compile_options(video_core PRIVATE $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-sign-conversion>)
|
||||
|
||||
# xbyak
|
||||
|
||||
@@ -0,0 +1,575 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
||||
#include "common/cityhash.h"
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/fs_paths.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "video_core/frame_gen/lossless_dll.h"
|
||||
#include "video_core/frame_gen/lsfg_translate.h"
|
||||
|
||||
namespace VideoCore::FrameGen {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u16 DOS_MAGIC = 0x5A4D;
|
||||
constexpr u32 PE_SIGNATURE = 0x00004550;
|
||||
constexpr u16 PE32_MAGIC = 0x010B;
|
||||
constexpr u16 PE32_PLUS_MAGIC = 0x020B;
|
||||
|
||||
constexpr size_t DOS_LFANEW_OFFSET = 0x3C;
|
||||
constexpr size_t COFF_HEADER_SIZE = 20;
|
||||
constexpr size_t OPTIONAL_HEADER_SIZE_OFFSET = 16;
|
||||
constexpr size_t SECTION_HEADER_SIZE = 40;
|
||||
constexpr size_t DATA_DIRECTORY_ENTRY_SIZE = 8;
|
||||
constexpr size_t DATA_DIRECTORY_OFFSET_PE32 = 96;
|
||||
constexpr size_t DATA_DIRECTORY_OFFSET_PE32_PLUS = 112;
|
||||
constexpr size_t RESOURCE_DATA_DIRECTORY_INDEX = 2;
|
||||
|
||||
constexpr size_t RESOURCE_DIRECTORY_SIZE = 16;
|
||||
constexpr size_t RESOURCE_NAMED_COUNT_OFFSET = 12;
|
||||
constexpr size_t RESOURCE_ID_COUNT_OFFSET = 14;
|
||||
constexpr size_t RESOURCE_ENTRY_SIZE = 8;
|
||||
constexpr u32 RESOURCE_SUBDIRECTORY_FLAG = 0x80000000;
|
||||
constexpr u32 RESOURCE_TYPE_RCDATA = 10;
|
||||
|
||||
constexpr u32 MIPMAPS_SHADER_ID = 255;
|
||||
constexpr u32 GENERATE_SHADER_ID = 256;
|
||||
constexpr u32 PERFORMANCE_SHADER_ID_FIRST = 280;
|
||||
constexpr u32 PERFORMANCE_SHADER_ID_LAST = 302;
|
||||
|
||||
constexpr u32 CACHE_MAGIC = 0x4746534C;
|
||||
constexpr u32 CACHE_VERSION = 2;
|
||||
|
||||
struct CacheHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u64 source_size;
|
||||
u64 source_hash;
|
||||
u32 module_count;
|
||||
u32 variant;
|
||||
};
|
||||
|
||||
struct Section {
|
||||
u32 virtual_address;
|
||||
u32 virtual_size;
|
||||
u32 raw_address;
|
||||
u32 raw_size;
|
||||
};
|
||||
|
||||
struct ResourceEntry {
|
||||
u32 id;
|
||||
u32 offset;
|
||||
bool is_directory;
|
||||
bool is_named;
|
||||
};
|
||||
|
||||
class ImageReader {
|
||||
public:
|
||||
explicit ImageReader(std::span<const u8> image_) : image{image_} {}
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] bool Read(size_t offset, T& out_value) const {
|
||||
if (offset > image.size() || image.size() - offset < sizeof(T)) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(&out_value, image.data() + offset, sizeof(T));
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool Slice(size_t offset, size_t size, std::span<const u8>& out_slice) const {
|
||||
if (offset > image.size() || image.size() - offset < size) {
|
||||
return false;
|
||||
}
|
||||
out_slice = image.subspan(offset, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<const u8> image;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::optional<size_t> FindPeHeader(const ImageReader& reader) {
|
||||
u16 dos_magic{};
|
||||
if (!reader.Read(0, dos_magic) || dos_magic != DOS_MAGIC) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
u32 pe_offset{};
|
||||
if (!reader.Read(DOS_LFANEW_OFFSET, pe_offset)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
u32 pe_signature{};
|
||||
if (!reader.Read(pe_offset, pe_signature) || pe_signature != PE_SIGNATURE) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return static_cast<size_t>(pe_offset);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<size_t> FindDataDirectory(const ImageReader& reader,
|
||||
size_t optional_header_offset) {
|
||||
u16 optional_magic{};
|
||||
if (!reader.Read(optional_header_offset, optional_magic)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (optional_magic) {
|
||||
case PE32_MAGIC:
|
||||
return optional_header_offset + DATA_DIRECTORY_OFFSET_PE32;
|
||||
case PE32_PLUS_MAGIC:
|
||||
return optional_header_offset + DATA_DIRECTORY_OFFSET_PE32_PLUS;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadSections(const ImageReader& reader, size_t pe_offset,
|
||||
std::vector<Section>& out_sections) {
|
||||
u16 section_count{};
|
||||
u16 optional_header_size{};
|
||||
if (!reader.Read(pe_offset + 4 + 2, section_count) ||
|
||||
!reader.Read(pe_offset + 4 + OPTIONAL_HEADER_SIZE_OFFSET, optional_header_size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t table_offset = pe_offset + 4 + COFF_HEADER_SIZE + optional_header_size;
|
||||
out_sections.reserve(section_count);
|
||||
for (size_t i = 0; i < section_count; ++i) {
|
||||
const size_t offset = table_offset + i * SECTION_HEADER_SIZE;
|
||||
Section section{};
|
||||
if (!reader.Read(offset + 8, section.virtual_size) ||
|
||||
!reader.Read(offset + 12, section.virtual_address) ||
|
||||
!reader.Read(offset + 16, section.raw_size) ||
|
||||
!reader.Read(offset + 20, section.raw_address)) {
|
||||
return false;
|
||||
}
|
||||
out_sections.push_back(section);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::optional<size_t> RvaToFileOffset(std::span<const Section> sections, u32 rva) {
|
||||
for (const Section& section : sections) {
|
||||
const u32 span = std::max(section.virtual_size, section.raw_size);
|
||||
if (span == 0 || rva < section.virtual_address) {
|
||||
continue;
|
||||
}
|
||||
const u32 relative = rva - section.virtual_address;
|
||||
if (relative < span) {
|
||||
return static_cast<size_t>(section.raw_address) + relative;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadResourceEntries(const ImageReader& reader, size_t directory_offset,
|
||||
std::vector<ResourceEntry>& out_entries) {
|
||||
u16 named_count{};
|
||||
u16 id_count{};
|
||||
if (!reader.Read(directory_offset + RESOURCE_NAMED_COUNT_OFFSET, named_count) ||
|
||||
!reader.Read(directory_offset + RESOURCE_ID_COUNT_OFFSET, id_count)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t total = size_t{named_count} + size_t{id_count};
|
||||
out_entries.clear();
|
||||
out_entries.reserve(total);
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
const size_t offset = directory_offset + RESOURCE_DIRECTORY_SIZE + i * RESOURCE_ENTRY_SIZE;
|
||||
u32 name{};
|
||||
u32 data{};
|
||||
if (!reader.Read(offset, name) || !reader.Read(offset + 4, data)) {
|
||||
return false;
|
||||
}
|
||||
out_entries.push_back(ResourceEntry{
|
||||
.id = name & ~RESOURCE_SUBDIRECTORY_FLAG,
|
||||
.offset = data & ~RESOURCE_SUBDIRECTORY_FLAG,
|
||||
.is_directory = (data & RESOURCE_SUBDIRECTORY_FLAG) != 0,
|
||||
.is_named = (name & RESOURCE_SUBDIRECTORY_FLAG) != 0,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadResourceLeaf(const ImageReader& reader, std::span<const Section> sections,
|
||||
size_t leaf_offset, std::span<const u8>& out_data) {
|
||||
u32 data_rva{};
|
||||
u32 data_size{};
|
||||
if (!reader.Read(leaf_offset, data_rva) || !reader.Read(leaf_offset + 4, data_size) ||
|
||||
data_size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::optional<size_t> data_offset = RvaToFileOffset(sections, data_rva);
|
||||
if (!data_offset) {
|
||||
return false;
|
||||
}
|
||||
return reader.Slice(*data_offset, data_size, out_data);
|
||||
}
|
||||
|
||||
using ResourceSpans = std::map<u32, std::span<const u8>>;
|
||||
|
||||
[[nodiscard]] bool CollectRcData(const ImageReader& reader, std::span<const Section> sections,
|
||||
size_t resource_base, ResourceSpans& out_resources) {
|
||||
std::vector<ResourceEntry> type_entries;
|
||||
if (!ReadResourceEntries(reader, resource_base, type_entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ResourceEntry& type_entry : type_entries) {
|
||||
if (type_entry.is_named || type_entry.id != RESOURCE_TYPE_RCDATA ||
|
||||
!type_entry.is_directory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<ResourceEntry> name_entries;
|
||||
if (!ReadResourceEntries(reader, resource_base + type_entry.offset, name_entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ResourceEntry& name_entry : name_entries) {
|
||||
if (name_entry.is_named || !name_entry.is_directory) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<ResourceEntry> language_entries;
|
||||
if (!ReadResourceEntries(reader, resource_base + name_entry.offset, language_entries)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ResourceEntry& language_entry : language_entries) {
|
||||
if (language_entry.is_directory) {
|
||||
continue;
|
||||
}
|
||||
std::span<const u8> data;
|
||||
if (!ReadResourceLeaf(reader, sections, resource_base + language_entry.offset,
|
||||
data)) {
|
||||
continue;
|
||||
}
|
||||
out_resources.insert_or_assign(name_entry.id, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<u32> PerformanceShaderIds() {
|
||||
std::vector<u32> ids{MIPMAPS_SHADER_ID, GENERATE_SHADER_ID};
|
||||
for (u32 id = PERFORMANCE_SHADER_ID_FIRST; id <= PERFORMANCE_SHADER_ID_LAST; ++id) {
|
||||
ids.push_back(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
template <typename Map>
|
||||
[[nodiscard]] bool HasPerformanceShaders(const Map& resources) {
|
||||
const std::vector<u32> ids = PerformanceShaderIds();
|
||||
return std::ranges::all_of(ids, [&](u32 id) { return resources.contains(id); });
|
||||
}
|
||||
|
||||
[[nodiscard]] u32 VariantOffset(ShaderVariant variant) {
|
||||
switch (variant) {
|
||||
case ShaderVariant::NativeFp16:
|
||||
return PerformanceShader::NATIVE_FP16_OFFSET;
|
||||
case ShaderVariant::NativeFp32:
|
||||
return PerformanceShader::NATIVE_FP32_OFFSET;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Map>
|
||||
[[nodiscard]] bool HasNativeVariant(const Map& resources, ShaderVariant variant) {
|
||||
const u32 offset = VariantOffset(variant);
|
||||
return std::ranges::all_of(PerformanceShaderIds(), [&](u32 id) {
|
||||
const auto hit = resources.find(id + offset);
|
||||
return hit != resources.end() && IsSpirvModule(hit->second);
|
||||
});
|
||||
}
|
||||
|
||||
[[nodiscard]] ShaderVariant SelectVariant(const ResourceSpans& resources, bool prefer_fp16) {
|
||||
if (prefer_fp16 && HasNativeVariant(resources, ShaderVariant::NativeFp16)) {
|
||||
return ShaderVariant::NativeFp16;
|
||||
}
|
||||
if (HasNativeVariant(resources, ShaderVariant::NativeFp32)) {
|
||||
return ShaderVariant::NativeFp32;
|
||||
}
|
||||
return ShaderVariant::TranslatedDxbc;
|
||||
}
|
||||
|
||||
[[nodiscard]] LosslessStatus TranslateAll(const ResourceSpans& resources,
|
||||
ShaderModules& out_modules,
|
||||
ShaderVariant variant) {
|
||||
const u32 offset = VariantOffset(variant);
|
||||
out_modules.clear();
|
||||
for (const u32 id : PerformanceShaderIds()) {
|
||||
const auto hit = resources.find(id + offset);
|
||||
if (hit == resources.end()) {
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
if (variant != ShaderVariant::TranslatedDxbc) {
|
||||
std::vector<u32> adopted = AdoptSpirvModule(hit->second);
|
||||
if (adopted.empty()) {
|
||||
return LosslessStatus::TranslationFailed;
|
||||
}
|
||||
out_modules.emplace(id, std::move(adopted));
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<u32> words = TranslateComputeShader(hit->second);
|
||||
if (words.empty()) {
|
||||
return LosslessStatus::TranslationFailed;
|
||||
}
|
||||
out_modules.emplace(id, std::move(words));
|
||||
}
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool WriteShaderCache(const std::filesystem::path& path, const CacheHeader& header,
|
||||
const ShaderModules& modules) {
|
||||
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
if (!file.IsOpen() || file.Write(header) != 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& [id, words] : modules) {
|
||||
const u32 word_count = static_cast<u32>(words.size());
|
||||
if (file.Write(id) != 1 || file.Write(word_count) != 1 ||
|
||||
file.Write(words) != words.size()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return file.Flush();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ReadShaderCache(const std::filesystem::path& path, u64 source_size,
|
||||
u64 source_hash, u32 variant, ShaderModules& out_modules) {
|
||||
if (!Common::FS::Exists(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
CacheHeader header{};
|
||||
if (!file.IsOpen() || file.Read(header) != 1) {
|
||||
return false;
|
||||
}
|
||||
if (header.magic != CACHE_MAGIC || header.version != CACHE_VERSION ||
|
||||
header.source_size != source_size || header.source_hash != source_hash ||
|
||||
header.variant != variant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out_modules.clear();
|
||||
for (u32 i = 0; i < header.module_count; ++i) {
|
||||
u32 id{};
|
||||
u32 word_count{};
|
||||
if (file.Read(id) != 1 || file.Read(word_count) != 1 || word_count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<u32> words(word_count);
|
||||
if (file.Read(words) != words.size()) {
|
||||
return false;
|
||||
}
|
||||
out_modules.emplace(id, std::move(words));
|
||||
}
|
||||
|
||||
return HasPerformanceShaders(out_modules);
|
||||
}
|
||||
|
||||
[[nodiscard]] LosslessStatus ReadImageFile(const std::filesystem::path& path,
|
||||
std::vector<u8>& out_image) {
|
||||
if (!Common::FS::Exists(path)) {
|
||||
return LosslessStatus::NotInstalled;
|
||||
}
|
||||
|
||||
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Read,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
if (!file.IsOpen()) {
|
||||
return LosslessStatus::UnreadableFile;
|
||||
}
|
||||
|
||||
out_image.resize(static_cast<size_t>(file.GetSize()));
|
||||
if (out_image.empty() || file.Read(out_image) != out_image.size()) {
|
||||
return LosslessStatus::UnreadableFile;
|
||||
}
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
[[nodiscard]] LosslessStatus ParseShaderSpans(std::span<const u8> image,
|
||||
ResourceSpans& out_resources) {
|
||||
const ImageReader reader{image};
|
||||
const std::optional<size_t> pe_offset = FindPeHeader(reader);
|
||||
if (!pe_offset) {
|
||||
return LosslessStatus::NotPortableExecutable;
|
||||
}
|
||||
|
||||
const std::optional<size_t> data_directory =
|
||||
FindDataDirectory(reader, *pe_offset + 4 + COFF_HEADER_SIZE);
|
||||
if (!data_directory) {
|
||||
return LosslessStatus::NotPortableExecutable;
|
||||
}
|
||||
|
||||
std::vector<Section> sections;
|
||||
if (!ReadSections(reader, *pe_offset, sections)) {
|
||||
return LosslessStatus::NotPortableExecutable;
|
||||
}
|
||||
|
||||
u32 resource_rva{};
|
||||
if (!reader.Read(*data_directory + RESOURCE_DATA_DIRECTORY_INDEX * DATA_DIRECTORY_ENTRY_SIZE,
|
||||
resource_rva) ||
|
||||
resource_rva == 0) {
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
|
||||
const std::optional<size_t> resource_base = RvaToFileOffset(sections, resource_rva);
|
||||
if (!resource_base) {
|
||||
return LosslessStatus::NotPortableExecutable;
|
||||
}
|
||||
|
||||
out_resources.clear();
|
||||
if (!CollectRcData(reader, sections, *resource_base, out_resources)) {
|
||||
return LosslessStatus::MissingShaders;
|
||||
}
|
||||
|
||||
return HasPerformanceShaders(out_resources) ? LosslessStatus::Ok
|
||||
: LosslessStatus::MissingShaders;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
std::filesystem::path GetLosslessDllPath() {
|
||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / LOSSLESS_DLL_FILE;
|
||||
}
|
||||
|
||||
std::filesystem::path GetShaderCachePath() {
|
||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / LOSSLESS_CACHE_FILE;
|
||||
}
|
||||
|
||||
LosslessStatus ReadShaderResources(const std::filesystem::path& path,
|
||||
ShaderResources& out_resources) {
|
||||
std::vector<u8> image;
|
||||
const LosslessStatus read_status = ReadImageFile(path, image);
|
||||
if (read_status != LosslessStatus::Ok) {
|
||||
return read_status;
|
||||
}
|
||||
|
||||
ResourceSpans spans;
|
||||
const LosslessStatus parse_status = ParseShaderSpans(image, spans);
|
||||
if (parse_status != LosslessStatus::Ok) {
|
||||
return parse_status;
|
||||
}
|
||||
|
||||
out_resources.clear();
|
||||
for (const auto& [id, data] : spans) {
|
||||
out_resources.emplace(id, std::vector<u8>{data.begin(), data.end()});
|
||||
}
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
LosslessStatus ValidateLosslessDll(const std::filesystem::path& path) {
|
||||
std::vector<u8> image;
|
||||
const LosslessStatus read_status = ReadImageFile(path, image);
|
||||
if (read_status != LosslessStatus::Ok) {
|
||||
return read_status;
|
||||
}
|
||||
|
||||
ResourceSpans spans;
|
||||
return ParseShaderSpans(image, spans);
|
||||
}
|
||||
|
||||
LosslessStatus GetInstalledLosslessStatus() {
|
||||
return ValidateLosslessDll(GetLosslessDllPath());
|
||||
}
|
||||
|
||||
ShaderVariant GetAvailableVariant(bool prefer_fp16) {
|
||||
std::vector<u8> image;
|
||||
if (ReadImageFile(GetLosslessDllPath(), image) != LosslessStatus::Ok) {
|
||||
return ShaderVariant::TranslatedDxbc;
|
||||
}
|
||||
|
||||
ResourceSpans spans;
|
||||
if (ParseShaderSpans(image, spans) != LosslessStatus::Ok) {
|
||||
return ShaderVariant::TranslatedDxbc;
|
||||
}
|
||||
|
||||
return SelectVariant(spans, prefer_fp16);
|
||||
}
|
||||
|
||||
LosslessStatus LoadShaderModules(ShaderModules& out_modules, bool prefer_fp16) {
|
||||
std::vector<u8> image;
|
||||
const LosslessStatus read_status = ReadImageFile(GetLosslessDllPath(), image);
|
||||
if (read_status != LosslessStatus::Ok) {
|
||||
return read_status;
|
||||
}
|
||||
|
||||
const u64 source_size = image.size();
|
||||
const u64 source_hash =
|
||||
Common::CityHash64(reinterpret_cast<const char*>(image.data()), image.size());
|
||||
const std::filesystem::path cache_path = GetShaderCachePath();
|
||||
|
||||
ResourceSpans spans;
|
||||
const LosslessStatus parse_status = ParseShaderSpans(image, spans);
|
||||
if (parse_status != LosslessStatus::Ok) {
|
||||
return parse_status;
|
||||
}
|
||||
|
||||
const ShaderVariant variant = SelectVariant(spans, prefer_fp16);
|
||||
|
||||
if (ReadShaderCache(cache_path, source_size, source_hash, static_cast<u32>(variant),
|
||||
out_modules)) {
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
const LosslessStatus translate_status = TranslateAll(spans, out_modules, variant);
|
||||
if (translate_status != LosslessStatus::Ok) {
|
||||
return translate_status;
|
||||
}
|
||||
|
||||
const CacheHeader header{
|
||||
.magic = CACHE_MAGIC,
|
||||
.version = CACHE_VERSION,
|
||||
.source_size = source_size,
|
||||
.source_hash = source_hash,
|
||||
.module_count = static_cast<u32>(out_modules.size()),
|
||||
.variant = static_cast<u32>(variant),
|
||||
};
|
||||
if (!WriteShaderCache(cache_path, header, out_modules)) {
|
||||
void(Common::FS::RemoveFile(cache_path));
|
||||
return LosslessStatus::CacheUnusable;
|
||||
}
|
||||
|
||||
return LosslessStatus::Ok;
|
||||
}
|
||||
|
||||
LosslessStatus BuildShaderCache() {
|
||||
ShaderModules modules;
|
||||
return LoadShaderModules(modules);
|
||||
}
|
||||
|
||||
bool RemoveInstalledLosslessDll() {
|
||||
const std::filesystem::path cache_path = GetShaderCachePath();
|
||||
if (Common::FS::Exists(cache_path)) {
|
||||
void(Common::FS::RemoveFile(cache_path));
|
||||
}
|
||||
|
||||
const std::filesystem::path path = GetLosslessDllPath();
|
||||
if (!Common::FS::Exists(path)) {
|
||||
return true;
|
||||
}
|
||||
return Common::FS::RemoveFile(path);
|
||||
}
|
||||
|
||||
} // namespace VideoCore::FrameGen
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace VideoCore::FrameGen {
|
||||
|
||||
enum class LosslessStatus : u32 {
|
||||
Ok,
|
||||
NotInstalled,
|
||||
UnreadableFile,
|
||||
NotPortableExecutable,
|
||||
MissingShaders,
|
||||
TranslationFailed,
|
||||
CacheUnusable,
|
||||
};
|
||||
|
||||
using ShaderResources = std::map<u32, std::vector<u8>>;
|
||||
using ShaderModules = std::map<u32, std::vector<u32>>;
|
||||
|
||||
enum class ShaderVariant : u32 {
|
||||
TranslatedDxbc,
|
||||
NativeFp32,
|
||||
NativeFp16,
|
||||
};
|
||||
|
||||
namespace PerformanceShader {
|
||||
constexpr u32 MIPMAPS = 255;
|
||||
constexpr u32 GENERATE = 256;
|
||||
constexpr std::array<u32, 4> ALPHA{290, 291, 292, 293};
|
||||
constexpr std::array<u32, 5> BETA{298, 299, 300, 301, 302};
|
||||
constexpr std::array<u32, 5> GAMMA{280, 282, 283, 284, 285};
|
||||
constexpr std::array<u32, 10> DELTA{280, 286, 287, 288, 289, 281, 294, 295, 296, 297};
|
||||
|
||||
constexpr u32 NATIVE_FP16_OFFSET = 49;
|
||||
constexpr u32 NATIVE_FP32_OFFSET = 98;
|
||||
} // namespace PerformanceShader
|
||||
|
||||
[[nodiscard]] std::filesystem::path GetLosslessDllPath();
|
||||
|
||||
[[nodiscard]] std::filesystem::path GetShaderCachePath();
|
||||
|
||||
[[nodiscard]] LosslessStatus ReadShaderResources(const std::filesystem::path& path,
|
||||
ShaderResources& out_resources);
|
||||
|
||||
[[nodiscard]] LosslessStatus ValidateLosslessDll(const std::filesystem::path& path);
|
||||
|
||||
[[nodiscard]] LosslessStatus GetInstalledLosslessStatus();
|
||||
|
||||
[[nodiscard]] LosslessStatus BuildShaderCache();
|
||||
|
||||
[[nodiscard]] ShaderVariant GetAvailableVariant(bool prefer_fp16);
|
||||
|
||||
[[nodiscard]] LosslessStatus LoadShaderModules(ShaderModules& out_modules,
|
||||
bool prefer_fp16 = false);
|
||||
|
||||
bool RemoveInstalledLosslessDll();
|
||||
|
||||
} // namespace VideoCore::FrameGen
|
||||
@@ -0,0 +1,129 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
|
||||
#include <dxbc_modinfo.h>
|
||||
#include <dxbc_module.h>
|
||||
#include <dxbc_reader.h>
|
||||
#include <thirdparty/spirv.hpp>
|
||||
|
||||
#include "video_core/frame_gen/lsfg_translate.h"
|
||||
|
||||
namespace VideoCore::FrameGen {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 DECORATION_LITERAL_WORD = 3;
|
||||
constexpr size_t SPIRV_HEADER_WORDS = 5;
|
||||
|
||||
void RenumberBindings(dxvk::SpirvCodeBuffer& code) {
|
||||
std::vector<u32> literal_offsets;
|
||||
for (const auto instruction : code) {
|
||||
if (instruction.opCode() == spv::OpFunction) {
|
||||
break;
|
||||
}
|
||||
if (instruction.opCode() == spv::OpDecorate &&
|
||||
instruction.arg(2) == spv::DecorationBinding) {
|
||||
literal_offsets.push_back(instruction.offset() + DECORATION_LITERAL_WORD);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < literal_offsets.size(); ++i) {
|
||||
code.data()[literal_offsets[i]] = static_cast<u32>(i);
|
||||
}
|
||||
}
|
||||
|
||||
void RenumberBindingsInOrder(std::vector<u32>& words) {
|
||||
struct Slot {
|
||||
u32 set;
|
||||
u32 binding;
|
||||
size_t literal_offset;
|
||||
};
|
||||
|
||||
std::map<u32, u32> sets;
|
||||
std::vector<Slot> slots;
|
||||
|
||||
size_t offset = SPIRV_HEADER_WORDS;
|
||||
while (offset + 1 <= words.size()) {
|
||||
const u32 length = words[offset] >> spv::WordCountShift;
|
||||
const u32 opcode = words[offset] & spv::OpCodeMask;
|
||||
if (length == 0 || offset + length > words.size()) {
|
||||
return;
|
||||
}
|
||||
if (opcode == spv::OpFunction) {
|
||||
break;
|
||||
}
|
||||
if (opcode == spv::OpDecorate && length >= 4) {
|
||||
if (words[offset + 2] == spv::DecorationDescriptorSet) {
|
||||
sets[words[offset + 1]] = words[offset + 3];
|
||||
} else if (words[offset + 2] == spv::DecorationBinding) {
|
||||
slots.push_back(Slot{0, words[offset + 3], offset + DECORATION_LITERAL_WORD});
|
||||
}
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
for (Slot& slot : slots) {
|
||||
const auto hit = sets.find(words[slot.literal_offset - 2]);
|
||||
slot.set = hit == sets.end() ? 0 : hit->second;
|
||||
}
|
||||
|
||||
std::ranges::stable_sort(slots, [](const Slot& lhs, const Slot& rhs) {
|
||||
return std::tie(lhs.set, lhs.binding) < std::tie(rhs.set, rhs.binding);
|
||||
});
|
||||
|
||||
for (size_t i = 0; i < slots.size(); ++i) {
|
||||
words[slots[i].literal_offset] = static_cast<u32>(i);
|
||||
}
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
bool IsSpirvModule(std::span<const u8> blob) {
|
||||
if (blob.size() < SPIRV_HEADER_WORDS * sizeof(u32) || blob.size() % sizeof(u32) != 0) {
|
||||
return false;
|
||||
}
|
||||
u32 magic{};
|
||||
std::memcpy(&magic, blob.data(), sizeof(magic));
|
||||
return magic == spv::MagicNumber;
|
||||
}
|
||||
|
||||
std::vector<u32> AdoptSpirvModule(std::span<const u8> blob) {
|
||||
if (!IsSpirvModule(blob)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<u32> words(blob.size() / sizeof(u32));
|
||||
std::memcpy(words.data(), blob.data(), blob.size());
|
||||
|
||||
RenumberBindingsInOrder(words);
|
||||
return words;
|
||||
}
|
||||
|
||||
std::vector<u32> TranslateComputeShader(std::span<const u8> dxbc) {
|
||||
if (dxbc.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
dxvk::DxbcReader reader{reinterpret_cast<const char*>(dxbc.data()), dxbc.size()};
|
||||
dxvk::DxbcModule module{reader};
|
||||
|
||||
const dxvk::DxbcModuleInfo module_info{};
|
||||
dxvk::SpirvCodeBuffer code = module.compile(module_info, "CS");
|
||||
if (code.dwords() == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
RenumberBindings(code);
|
||||
return std::vector<u32>{code.data(), code.data() + code.dwords()};
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace VideoCore::FrameGen
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace VideoCore::FrameGen {
|
||||
|
||||
[[nodiscard]] bool IsSpirvModule(std::span<const u8> blob);
|
||||
|
||||
[[nodiscard]] std::vector<u32> AdoptSpirvModule(std::span<const u8> blob);
|
||||
|
||||
[[nodiscard]] std::vector<u32> TranslateComputeShader(std::span<const u8> dxbc);
|
||||
|
||||
} // namespace VideoCore::FrameGen
|
||||
@@ -0,0 +1,361 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/settings.h"
|
||||
#include "video_core/renderer_vulkan/present/frame_gen.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
#include "video_core/renderer_vulkan/vk_present_manager.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
#include "video_core/vulkan_common/vulkan_device.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t COLOR_CHANNELS = 4;
|
||||
constexpr u64 LSFG_REQUIRED_FRAMES = 2;
|
||||
constexpr u32 LSFG_RECURRENCE_FRAMES = 2;
|
||||
|
||||
[[nodiscard]] f32 ManualFlowScale() {
|
||||
return static_cast<f32>(Settings::values.frame_gen_flow_scale.GetValue()) / 100.0f;
|
||||
}
|
||||
|
||||
[[nodiscard]] f32 ConfiguredFlowScale(VkExtent2D guest_extent, VkExtent2D presented_extent) {
|
||||
if (!Settings::values.frame_gen_flow_scale_auto.GetValue()) {
|
||||
return ManualFlowScale();
|
||||
}
|
||||
if (guest_extent.width == 0 || presented_extent.width == 0) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
const f32 rendered_width = static_cast<f32>(guest_extent.width) *
|
||||
Settings::values.resolution_info.up_factor;
|
||||
const f32 ratio = rendered_width / static_cast<f32>(presented_extent.width);
|
||||
|
||||
constexpr f32 FLOW_SCALE_STEPS = 20.0f;
|
||||
const f32 stepped = std::ceil(ratio * FLOW_SCALE_STEPS) / FLOW_SCALE_STEPS;
|
||||
return std::clamp(stepped, 0.25f, 1.0f);
|
||||
}
|
||||
|
||||
bool IsBlueFirst(VkFormat format) {
|
||||
return format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB;
|
||||
}
|
||||
|
||||
VkDeviceSize BytesPerTexel(VkFormat format) {
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8_UNORM:
|
||||
return 1;
|
||||
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
||||
return 8;
|
||||
default:
|
||||
return COLOR_CHANNELS;
|
||||
}
|
||||
}
|
||||
|
||||
void WritePortablePixmap(const std::filesystem::path& path, const std::string& magic,
|
||||
VkExtent2D extent, std::span<const u8> pixels) {
|
||||
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
if (!file.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string header = magic + "\n" + std::to_string(extent.width) + " " +
|
||||
std::to_string(extent.height) + "\n255\n";
|
||||
if (file.Write(header) != header.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
void(file.Write(pixels));
|
||||
void(file.Flush());
|
||||
}
|
||||
|
||||
void WriteGrayscalePgm(const std::filesystem::path& path, VkExtent2D extent,
|
||||
std::span<const u8> pixels) {
|
||||
const size_t expected = static_cast<size_t>(extent.width) * extent.height;
|
||||
WritePortablePixmap(path, "P5", extent, pixels.subspan(0, std::min(expected, pixels.size())));
|
||||
}
|
||||
|
||||
void WriteRaw(const std::filesystem::path& path, std::span<const u8> pixels) {
|
||||
Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write,
|
||||
Common::FS::FileType::BinaryFile};
|
||||
if (!file.IsOpen()) {
|
||||
return;
|
||||
}
|
||||
void(file.Write(pixels));
|
||||
void(file.Flush());
|
||||
}
|
||||
|
||||
void WriteColorPpm(const std::filesystem::path& path, VkExtent2D extent,
|
||||
std::span<const u8> pixels, bool blue_first) {
|
||||
const size_t pixel_count = static_cast<size_t>(extent.width) * extent.height;
|
||||
if (pixels.size() < pixel_count * COLOR_CHANNELS) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<u8> rgb(pixel_count * 3);
|
||||
for (size_t i = 0; i < pixel_count; ++i) {
|
||||
const u8 first = pixels[i * COLOR_CHANNELS];
|
||||
const u8 green = pixels[i * COLOR_CHANNELS + 1];
|
||||
const u8 third = pixels[i * COLOR_CHANNELS + 2];
|
||||
rgb[i * 3] = blue_first ? third : first;
|
||||
rgb[i * 3 + 1] = green;
|
||||
rgb[i * 3 + 2] = blue_first ? first : third;
|
||||
}
|
||||
|
||||
WritePortablePixmap(path, "P6", extent, rgb);
|
||||
}
|
||||
|
||||
VkImageMemoryBarrier MakeTransitionBarrier(VkImage image, VkAccessFlags src_access,
|
||||
VkAccessFlags dst_access, VkImageLayout old_layout,
|
||||
VkImageLayout new_layout) {
|
||||
return VkImageMemoryBarrier{
|
||||
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
|
||||
.pNext = nullptr,
|
||||
.srcAccessMask = src_access,
|
||||
.dstAccessMask = dst_access,
|
||||
.oldLayout = old_layout,
|
||||
.newLayout = new_layout,
|
||||
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
||||
.image = image,
|
||||
.subresourceRange{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.baseMipLevel = 0,
|
||||
.levelCount = 1,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
VkImageCopy MakeCopyRegion(VkExtent2D extent) {
|
||||
return VkImageCopy{
|
||||
.srcSubresource{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.srcOffset = {},
|
||||
.dstSubresource{
|
||||
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
|
||||
.mipLevel = 0,
|
||||
.baseArrayLayer = 0,
|
||||
.layerCount = 1,
|
||||
},
|
||||
.dstOffset = {},
|
||||
.extent = {.width = extent.width, .height = extent.height, .depth = 1},
|
||||
};
|
||||
}
|
||||
|
||||
void CopyPresentedFrame(vk::CommandBuffer cmdbuf, VkImage source, LsfgImage& destination,
|
||||
VkExtent2D extent) {
|
||||
const auto make_barrier = MakeTransitionBarrier;
|
||||
|
||||
const std::array before{
|
||||
make_barrier(source, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL),
|
||||
make_barrier(destination.Handle(), VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
destination.Layout(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL),
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before);
|
||||
|
||||
cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(),
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, MakeCopyRegion(extent));
|
||||
|
||||
const std::array after{
|
||||
make_barrier(source, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL),
|
||||
make_barrier(destination.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL),
|
||||
};
|
||||
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
|
||||
0, {}, {}, after);
|
||||
|
||||
destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL);
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
FrameGen::FrameGen(MemoryAllocator& memory_allocator_, Scheduler& scheduler_)
|
||||
: memory_allocator{memory_allocator_}, scheduler{scheduler_} {}
|
||||
|
||||
FrameGen::~FrameGen() = default;
|
||||
|
||||
void FrameGen::Process(const Device& device, Frame* frame, VkFormat format,
|
||||
VkExtent2D guest_extent) {
|
||||
generated = false;
|
||||
|
||||
if (unavailable || !Settings::values.frame_gen.GetValue()) {
|
||||
if (chain) {
|
||||
scheduler.Finish();
|
||||
chain.reset();
|
||||
}
|
||||
warm_streak = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame->storage_view) {
|
||||
unavailable = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shaders) {
|
||||
shaders.emplace(device);
|
||||
if (!shaders->IsValid()) {
|
||||
unavailable = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
peak_guest_extent.width = std::max(peak_guest_extent.width, guest_extent.width);
|
||||
peak_guest_extent.height = std::max(peak_guest_extent.height, guest_extent.height);
|
||||
|
||||
const VkExtent2D extent{.width = frame->width, .height = frame->height};
|
||||
const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent);
|
||||
if (!chain || built_extent.width != extent.width || built_extent.height != extent.height ||
|
||||
built_format != format || built_flow_scale != flow_scale) {
|
||||
Rebuild(device, extent, format, flow_scale);
|
||||
}
|
||||
|
||||
const u64 count = frame_count++;
|
||||
last_count = count;
|
||||
last_generations = plan.generations;
|
||||
|
||||
const bool warm = plan.warm && count + 1 >= LSFG_REQUIRED_FRAMES;
|
||||
warm_streak = warm ? warm_streak + 1 : 0;
|
||||
generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0;
|
||||
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
scheduler.Record([this, source = *frame->image, extent, count,
|
||||
dispatch = warm](vk::CommandBuffer cmdbuf) {
|
||||
CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent);
|
||||
if (dispatch) {
|
||||
chain->DispatchShared(cmdbuf, count);
|
||||
}
|
||||
});
|
||||
|
||||
const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue();
|
||||
if (!dump_requested) {
|
||||
dumped = false;
|
||||
} else if (!dumped) {
|
||||
DumpDebugImages(count);
|
||||
dumped = true;
|
||||
}
|
||||
}
|
||||
|
||||
size_t FrameGen::WantedGenerations(size_t capacity) {
|
||||
if (unavailable) {
|
||||
plan = {};
|
||||
return 0;
|
||||
}
|
||||
plan = pacer.Plan(capacity);
|
||||
return plan.generations;
|
||||
}
|
||||
|
||||
size_t FrameGen::GeneratedFrameCount() const {
|
||||
return generated ? last_generations : 0;
|
||||
}
|
||||
|
||||
void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) {
|
||||
chain->SetTarget(device, last_generations, generation, destination->index,
|
||||
*destination->storage_view);
|
||||
|
||||
const VkExtent2D extent{.width = destination->width, .height = destination->height};
|
||||
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
scheduler.Record([this, count = last_count, generation_count = last_generations, generation,
|
||||
target = destination->index, image = *destination->image,
|
||||
extent](vk::CommandBuffer cmdbuf) {
|
||||
chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image,
|
||||
extent);
|
||||
});
|
||||
}
|
||||
|
||||
void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) {
|
||||
scheduler.Finish();
|
||||
chain.reset();
|
||||
|
||||
built_flow_scale = flow_scale;
|
||||
|
||||
chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale);
|
||||
built_extent = extent;
|
||||
built_format = format;
|
||||
frame_count = 0;
|
||||
warm_streak = 0;
|
||||
generated = false;
|
||||
}
|
||||
|
||||
void FrameGen::DumpDebugImages(u64 count) {
|
||||
const std::filesystem::path directory =
|
||||
Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / "debug";
|
||||
if (!Common::FS::CreateDirs(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto dump = [&](const std::string& name, LsfgImage& image) {
|
||||
const VkExtent2D extent = image.Extent();
|
||||
const VkFormat format = image.Format();
|
||||
const VkDeviceSize texel_size = BytesPerTexel(format);
|
||||
const VkDeviceSize size =
|
||||
static_cast<VkDeviceSize>(extent.width) * extent.height * texel_size;
|
||||
|
||||
vk::Buffer buffer = CreateWrappedBuffer(memory_allocator, size, MemoryUsage::Download);
|
||||
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
scheduler.Record(
|
||||
[handle = image.Handle(), dst = *buffer, extent](vk::CommandBuffer cmdbuf) {
|
||||
DownloadColorImage(
|
||||
cmdbuf, handle, dst,
|
||||
VkExtent3D{.width = extent.width, .height = extent.height, .depth = 1});
|
||||
});
|
||||
scheduler.Finish();
|
||||
|
||||
buffer.Invalidate();
|
||||
const std::span<u8> mapped = buffer.Mapped();
|
||||
|
||||
if (format == LSFG_FLOW_FORMAT) {
|
||||
WriteGrayscalePgm(directory / (name + ".pgm"), extent, mapped);
|
||||
} else if (texel_size == COLOR_CHANNELS) {
|
||||
WriteColorPpm(directory / (name + ".ppm"), extent, mapped, IsBlueFirst(format));
|
||||
} else {
|
||||
WriteRaw(directory / (name + "_" + std::to_string(extent.width) + "x" +
|
||||
std::to_string(extent.height) + ".f16"),
|
||||
mapped.subspan(0, std::min<size_t>(size, mapped.size())));
|
||||
}
|
||||
};
|
||||
|
||||
dump("in0", chain->Input(0));
|
||||
dump("in1", chain->Input(1));
|
||||
|
||||
for (size_t level = 0; level < LSFG_MIP_LEVELS; ++level) {
|
||||
dump("flow_mip" + std::to_string(level), chain->FlowLevel(level));
|
||||
}
|
||||
for (size_t index = 0; index < 2; ++index) {
|
||||
dump("alpha0_" + std::to_string(index), chain->AlphaOutput(0, count, index));
|
||||
dump("alpha6_" + std::to_string(index), chain->AlphaOutput(LSFG_MIP_LEVELS - 1, count,
|
||||
index));
|
||||
}
|
||||
for (size_t level = 0; level < LSFG_BETA_OUTPUTS; ++level) {
|
||||
dump("beta_" + std::to_string(level), chain->BetaOutput(level));
|
||||
}
|
||||
|
||||
dump("gamma0", chain->GammaOutput(0));
|
||||
dump("gamma6", chain->GammaOutput(LSFG_MIP_LEVELS - 1));
|
||||
dump("delta2_out1", chain->DeltaOutput1(LSFG_DELTA_INSTANCES - 1));
|
||||
dump("delta2_out2", chain->DeltaOutput2(LSFG_DELTA_INSTANCES - 1));
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/renderer_vulkan/present/frame_gen_pacer.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_chain.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
|
||||
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class Device;
|
||||
class Scheduler;
|
||||
struct Frame;
|
||||
|
||||
class FrameGen {
|
||||
public:
|
||||
explicit FrameGen(MemoryAllocator& memory_allocator, Scheduler& scheduler);
|
||||
~FrameGen();
|
||||
|
||||
void Process(const Device& device, Frame* frame, VkFormat format, VkExtent2D guest_extent);
|
||||
|
||||
[[nodiscard]] size_t WantedGenerations(size_t capacity);
|
||||
|
||||
[[nodiscard]] size_t GeneratedFrameCount() const;
|
||||
|
||||
void GenerateInto(const Device& device, Frame* destination, size_t generation);
|
||||
|
||||
private:
|
||||
void Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale);
|
||||
void DumpDebugImages(u64 count);
|
||||
|
||||
MemoryAllocator& memory_allocator;
|
||||
Scheduler& scheduler;
|
||||
|
||||
std::optional<LsfgShaders> shaders;
|
||||
std::optional<LsfgChain> chain;
|
||||
FrameGenPacer pacer;
|
||||
FrameGenPlan plan{};
|
||||
VkExtent2D peak_guest_extent{};
|
||||
VkExtent2D built_extent{};
|
||||
VkFormat built_format{VK_FORMAT_UNDEFINED};
|
||||
f32 built_flow_scale{};
|
||||
u64 frame_count{};
|
||||
u64 last_count{};
|
||||
size_t last_generations{};
|
||||
u32 warm_streak{};
|
||||
bool generated{};
|
||||
bool unavailable{};
|
||||
bool dumped{};
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,240 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "video_core/renderer_vulkan/present/frame_gen_pacer.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
constexpr f32 INTERVAL_SMOOTHING = 0.25f;
|
||||
constexpr f32 MINIMUM_BASE_RATE = 10.0f;
|
||||
constexpr f32 BURST_CADENCE_RATIO = 3.0f;
|
||||
constexpr f32 BURST_TARGET_RATIO = 2.0f;
|
||||
constexpr f32 PROBE_THROUGHPUT_TOLERANCE = 0.95f;
|
||||
constexpr f32 PROBE_BASE_COLLAPSE_RATIO = 0.70f;
|
||||
constexpr f32 PROBE_MARGINAL_GAIN = 1.15f;
|
||||
constexpr f32 TARGET_SATISFIED_RATIO = 0.95f;
|
||||
constexpr f32 UNLOADED_BASE_RETENTION = 0.75f;
|
||||
constexpr f32 CREDIT_EPSILON = 1.0e-4f;
|
||||
constexpr u32 MAX_PROBE_FAILURES = 4;
|
||||
|
||||
constexpr auto STABILIZATION_DURATION = std::chrono::seconds(1);
|
||||
constexpr auto PROBE_DURATION = std::chrono::seconds(1);
|
||||
constexpr auto DEFICIT_DURATION = std::chrono::seconds(1);
|
||||
constexpr auto PROBE_STEP_DELAY = std::chrono::milliseconds(250);
|
||||
|
||||
[[nodiscard]] Clock::duration ProbeBackoff(u32 failures) {
|
||||
switch (failures) {
|
||||
case 1:
|
||||
return std::chrono::seconds(5);
|
||||
case 2:
|
||||
return std::chrono::seconds(15);
|
||||
case 3:
|
||||
return std::chrono::seconds(30);
|
||||
default:
|
||||
return std::chrono::seconds(60);
|
||||
}
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
FrameGenPlan FrameGenPacer::Plan(size_t capacity) {
|
||||
const size_t ceiling = std::min(capacity, Settings::FrameGenMaxGenerations());
|
||||
if (ceiling == 0) {
|
||||
Reset();
|
||||
return {};
|
||||
}
|
||||
|
||||
const Clock::time_point now = Clock::now();
|
||||
const size_t previous_generations = std::exchange(issued_generations, 0);
|
||||
if (!last_frame) {
|
||||
last_frame = now;
|
||||
return {};
|
||||
}
|
||||
|
||||
const Clock::duration interval = now - *last_frame;
|
||||
const f32 interval_seconds = std::chrono::duration<f32>(interval).count();
|
||||
last_frame = now;
|
||||
|
||||
if (interval_seconds <= 0.0f) {
|
||||
Stabilize(now);
|
||||
return {};
|
||||
}
|
||||
|
||||
const f32 target_rate = static_cast<f32>(Settings::values.frame_gen_target_rate.GetValue());
|
||||
|
||||
if (smoothed_interval > 0.0f) {
|
||||
f32 burst_threshold = BURST_CADENCE_RATIO / smoothed_interval;
|
||||
if (target_rate > 0.0f) {
|
||||
burst_threshold = std::max(burst_threshold, target_rate * BURST_TARGET_RATIO);
|
||||
}
|
||||
if (1.0f / interval_seconds > burst_threshold) {
|
||||
DeferEvaluations(interval);
|
||||
output_credit = 0.0f;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (interval_seconds > 1.0f / MINIMUM_BASE_RATE) {
|
||||
Stabilize(now);
|
||||
return {};
|
||||
}
|
||||
|
||||
smoothed_interval = smoothed_interval > 0.0f
|
||||
? smoothed_interval +
|
||||
(interval_seconds - smoothed_interval) * INTERVAL_SMOOTHING
|
||||
: interval_seconds;
|
||||
|
||||
if (previous_generations == 0) {
|
||||
const f32 measured = 1.0f / smoothed_interval;
|
||||
unloaded_base_rate =
|
||||
unloaded_base_rate > 0.0f
|
||||
? unloaded_base_rate + (measured - unloaded_base_rate) * INTERVAL_SMOOTHING
|
||||
: measured;
|
||||
}
|
||||
|
||||
if (stable_until) {
|
||||
if (now < *stable_until) {
|
||||
return {};
|
||||
}
|
||||
stable_until.reset();
|
||||
}
|
||||
|
||||
if (target_rate == 0.0f) {
|
||||
limit = std::min(Settings::FrameGenGenerations(), ceiling);
|
||||
output_credit = 0.0f;
|
||||
issued_generations = limit;
|
||||
return {.generations = limit, .warm = limit > 0};
|
||||
}
|
||||
|
||||
UpdateLimit(now, 1.0f / smoothed_interval, target_rate, ceiling);
|
||||
|
||||
const size_t allowed = std::min(limit, ceiling);
|
||||
const f32 desired_outputs = smoothed_interval * target_rate;
|
||||
if (allowed == 0 || desired_outputs <= 1.0f) {
|
||||
output_credit = 0.0f;
|
||||
return {};
|
||||
}
|
||||
|
||||
output_credit += desired_outputs;
|
||||
const size_t outputs =
|
||||
std::max<size_t>(1, static_cast<size_t>(std::floor(output_credit + CREDIT_EPSILON)));
|
||||
const size_t generations = std::min(outputs - 1, allowed);
|
||||
|
||||
output_credit -= static_cast<f32>(generations + 1);
|
||||
if (output_credit < 0.0f) {
|
||||
output_credit = 0.0f;
|
||||
} else if (generations == allowed && output_credit >= 1.0f) {
|
||||
output_credit = std::fmod(output_credit, 1.0f);
|
||||
}
|
||||
|
||||
issued_generations = generations;
|
||||
return {.generations = generations, .warm = true};
|
||||
}
|
||||
|
||||
void FrameGenPacer::UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate,
|
||||
size_t ceiling) {
|
||||
limit = std::min(limit, ceiling);
|
||||
|
||||
if (probe_until) {
|
||||
if (now < *probe_until) {
|
||||
return;
|
||||
}
|
||||
probe_until.reset();
|
||||
output_credit = 0.0f;
|
||||
|
||||
const f32 previous_output =
|
||||
std::min(target_rate, probe_base_rate * static_cast<f32>(probe_previous_limit + 1));
|
||||
const f32 current_output =
|
||||
std::min(target_rate, base_rate * static_cast<f32>(limit + 1));
|
||||
|
||||
const bool throughput_regressed =
|
||||
current_output < previous_output * PROBE_THROUGHPUT_TOLERANCE;
|
||||
const bool collapsed_for_marginal_gain =
|
||||
base_rate < probe_base_rate * PROBE_BASE_COLLAPSE_RATIO &&
|
||||
current_output < previous_output * PROBE_MARGINAL_GAIN;
|
||||
const bool emulation_slowed = unloaded_base_rate > 0.0f &&
|
||||
base_rate < unloaded_base_rate * UNLOADED_BASE_RETENTION;
|
||||
|
||||
if (throughput_regressed || collapsed_for_marginal_gain || emulation_slowed) {
|
||||
limit = probe_previous_limit;
|
||||
probe_failures = std::min(probe_failures + 1, MAX_PROBE_FAILURES);
|
||||
next_probe = now + ProbeBackoff(probe_failures);
|
||||
deficit_since.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
probe_failures = 0;
|
||||
next_probe = now + PROBE_STEP_DELAY;
|
||||
}
|
||||
|
||||
if (base_rate * static_cast<f32>(limit + 1) >= target_rate * TARGET_SATISFIED_RATIO ||
|
||||
limit >= ceiling) {
|
||||
deficit_since.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!deficit_since) {
|
||||
deficit_since = now;
|
||||
return;
|
||||
}
|
||||
if (now - *deficit_since < DEFICIT_DURATION) {
|
||||
return;
|
||||
}
|
||||
if (next_probe && now < *next_probe) {
|
||||
return;
|
||||
}
|
||||
|
||||
probe_previous_limit = limit;
|
||||
probe_base_rate = base_rate;
|
||||
++limit;
|
||||
probe_until = now + PROBE_DURATION;
|
||||
deficit_since.reset();
|
||||
output_credit = 0.0f;
|
||||
}
|
||||
|
||||
void FrameGenPacer::DeferEvaluations(Clock::duration amount) {
|
||||
const auto defer = [amount](std::optional<Clock::time_point>& deadline) {
|
||||
if (deadline) {
|
||||
*deadline += amount;
|
||||
}
|
||||
};
|
||||
defer(stable_until);
|
||||
defer(probe_until);
|
||||
defer(next_probe);
|
||||
deficit_since.reset();
|
||||
}
|
||||
|
||||
void FrameGenPacer::Stabilize(Clock::time_point now) {
|
||||
stable_until = now + STABILIZATION_DURATION;
|
||||
probe_until.reset();
|
||||
deficit_since.reset();
|
||||
smoothed_interval = 0.0f;
|
||||
output_credit = 0.0f;
|
||||
}
|
||||
|
||||
void FrameGenPacer::Reset() {
|
||||
last_frame.reset();
|
||||
stable_until.reset();
|
||||
probe_until.reset();
|
||||
next_probe.reset();
|
||||
deficit_since.reset();
|
||||
smoothed_interval = 0.0f;
|
||||
output_credit = 0.0f;
|
||||
probe_base_rate = 0.0f;
|
||||
unloaded_base_rate = 0.0f;
|
||||
issued_generations = 0;
|
||||
probe_previous_limit = 0;
|
||||
limit = 0;
|
||||
probe_failures = 0;
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
struct FrameGenPlan {
|
||||
size_t generations{};
|
||||
bool warm{};
|
||||
};
|
||||
|
||||
class FrameGenPacer {
|
||||
public:
|
||||
[[nodiscard]] FrameGenPlan Plan(size_t capacity);
|
||||
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
void Stabilize(Clock::time_point now);
|
||||
void DeferEvaluations(Clock::duration amount);
|
||||
void UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, size_t ceiling);
|
||||
|
||||
std::optional<Clock::time_point> last_frame;
|
||||
std::optional<Clock::time_point> stable_until;
|
||||
std::optional<Clock::time_point> probe_until;
|
||||
std::optional<Clock::time_point> next_probe;
|
||||
std::optional<Clock::time_point> deficit_since;
|
||||
f32 smoothed_interval{};
|
||||
f32 output_credit{};
|
||||
f32 probe_base_rate{};
|
||||
f32 unloaded_base_rate{};
|
||||
size_t issued_generations{};
|
||||
size_t probe_previous_limit{};
|
||||
size_t limit{};
|
||||
u32 probe_failures{};
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "video_core/frame_gen/lossless_dll.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_alpha.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
#include "video_core/vulkan_common/vulkan_device.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 DISPATCH_TILE_SHIFT = 3;
|
||||
|
||||
[[nodiscard]] u32 GroupCount(u32 size) {
|
||||
return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT;
|
||||
}
|
||||
|
||||
[[nodiscard]] VkExtent2D HalveExtent(VkExtent2D extent) {
|
||||
return VkExtent2D{
|
||||
.width = (extent.width + 1) >> 1,
|
||||
.height = (extent.height + 1) >> 1,
|
||||
};
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
LsfgAlphaPasses::LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders) {
|
||||
using namespace VideoCore::FrameGen::PerformanceShader;
|
||||
|
||||
passes[0] = LsfgPass(device, shaders, ALPHA[0],
|
||||
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
passes[1] = LsfgPass(device, shaders, ALPHA[1],
|
||||
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
passes[2] = LsfgPass(device, shaders, ALPHA[2],
|
||||
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
passes[3] = LsfgPass(device, shaders, ALPHA[3],
|
||||
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
}
|
||||
|
||||
LsfgAlpha::LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator,
|
||||
const LsfgAlphaPasses& passes_, LsfgResources& resources,
|
||||
vk::DescriptorPool& descriptor_pool, LsfgImage& input_)
|
||||
: passes{&passes_}, input{&input_} {
|
||||
const VkExtent2D half_extent = HalveExtent(input->Extent());
|
||||
const VkExtent2D quarter_extent = HalveExtent(half_extent);
|
||||
|
||||
temp1 = LsfgImage(device, memory_allocator, half_extent);
|
||||
temp2 = LsfgImage(device, memory_allocator, half_extent);
|
||||
for (size_t i = 0; i < temp3.size(); ++i) {
|
||||
temp3[i] = LsfgImage(device, memory_allocator, quarter_extent);
|
||||
for (size_t j = 0; j < LSFG_HISTORY_SLOTS; ++j) {
|
||||
out_images[j][i] = LsfgImage(device, memory_allocator, quarter_extent);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<VkDescriptorSetLayout> layouts;
|
||||
for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) {
|
||||
layouts.push_back(passes->Get(i).SetLayout());
|
||||
}
|
||||
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
|
||||
layouts.push_back(passes->Get(3).SetLayout());
|
||||
}
|
||||
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
|
||||
|
||||
for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) {
|
||||
descriptor_sets[i] = owned_sets[i];
|
||||
}
|
||||
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
|
||||
last_descriptor_sets[i] = owned_sets[LSFG_ALPHA_STAGES - 1 + i];
|
||||
}
|
||||
|
||||
const VkSampler sampler = resources.GetSampler();
|
||||
|
||||
LsfgDescriptorWriter(descriptor_sets[0])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImage(*input)
|
||||
.AddStorageImage(temp1)
|
||||
.Build(device);
|
||||
LsfgDescriptorWriter(descriptor_sets[1])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImage(temp1)
|
||||
.AddStorageImage(temp2)
|
||||
.Build(device);
|
||||
LsfgDescriptorWriter(descriptor_sets[2])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImage(temp2)
|
||||
.AddStorageImages(temp3)
|
||||
.Build(device);
|
||||
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
|
||||
LsfgDescriptorWriter(last_descriptor_sets[i])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImages(temp3)
|
||||
.AddStorageImages(out_images[i])
|
||||
.Build(device);
|
||||
}
|
||||
}
|
||||
|
||||
void LsfgAlpha::PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage) {
|
||||
switch (stage) {
|
||||
case 0:
|
||||
barriers.WriteToRead(*input).ReadToWrite(temp1);
|
||||
break;
|
||||
case 1:
|
||||
barriers.WriteToRead(temp1).ReadToWrite(temp2);
|
||||
break;
|
||||
case 2:
|
||||
barriers.WriteToRead(temp2).ReadToWriteAll(temp3);
|
||||
break;
|
||||
default:
|
||||
barriers.WriteToReadAll(temp3).ReadToWriteAll(out_images[frame_count % LSFG_HISTORY_SLOTS]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LsfgAlpha::DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage) {
|
||||
const VkExtent2D extent = stage < 2 ? temp1.Extent() : temp3[0].Extent();
|
||||
const VkDescriptorSet set = stage < LSFG_ALPHA_STAGES - 1
|
||||
? descriptor_sets[stage]
|
||||
: last_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS];
|
||||
|
||||
passes->Get(stage).BindSet(cmdbuf, set);
|
||||
cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1);
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_common.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class Device;
|
||||
class LsfgShaders;
|
||||
|
||||
constexpr size_t LSFG_ALPHA_STAGES = 4;
|
||||
|
||||
class LsfgAlphaPasses {
|
||||
public:
|
||||
LsfgAlphaPasses() = default;
|
||||
LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders);
|
||||
|
||||
[[nodiscard]] const LsfgPass& Get(size_t stage) const {
|
||||
return passes[stage];
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<LsfgPass, LSFG_ALPHA_STAGES> passes;
|
||||
};
|
||||
|
||||
class LsfgAlpha {
|
||||
public:
|
||||
LsfgAlpha() = default;
|
||||
LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator,
|
||||
const LsfgAlphaPasses& passes_, LsfgResources& resources,
|
||||
vk::DescriptorPool& descriptor_pool, LsfgImage& input);
|
||||
|
||||
void PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage);
|
||||
void DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage);
|
||||
|
||||
[[nodiscard]] LsfgImageHistory& Outputs() {
|
||||
return out_images;
|
||||
}
|
||||
|
||||
private:
|
||||
const LsfgAlphaPasses* passes{};
|
||||
LsfgImage* input{};
|
||||
|
||||
std::array<VkDescriptorSet, LSFG_ALPHA_STAGES - 1> descriptor_sets{};
|
||||
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> last_descriptor_sets{};
|
||||
vk::DescriptorSets owned_sets;
|
||||
|
||||
LsfgImage temp1;
|
||||
LsfgImage temp2;
|
||||
LsfgImagePair temp3;
|
||||
LsfgImageHistory out_images;
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,147 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "video_core/frame_gen/lossless_dll.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_beta.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_shaders.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
#include "video_core/vulkan_common/vulkan_device.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 DISPATCH_TILE_SHIFT = 3;
|
||||
constexpr u32 OUTPUT_TILE_SHIFT = 5;
|
||||
|
||||
[[nodiscard]] u32 GroupCount(u32 size, u32 shift) {
|
||||
return (size + (1u << shift) - 1) >> shift;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
LsfgBeta::LsfgBeta(const Device& device, MemoryAllocator& memory_allocator,
|
||||
const LsfgShaders& shaders, LsfgResources& resources,
|
||||
vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_)
|
||||
: inputs{&inputs_} {
|
||||
using namespace VideoCore::FrameGen::PerformanceShader;
|
||||
|
||||
passes[0] = LsfgPass(device, shaders, BETA[0],
|
||||
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{6, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
for (size_t i = 1; i < LSFG_BETA_STAGES - 1; ++i) {
|
||||
passes[i] = LsfgPass(device, shaders, BETA[i],
|
||||
{{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
}
|
||||
passes[4] = LsfgPass(device, shaders, BETA[4],
|
||||
{{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER},
|
||||
{1, VK_DESCRIPTOR_TYPE_SAMPLER},
|
||||
{2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE},
|
||||
{6, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}});
|
||||
|
||||
const VkExtent2D extent = (*inputs)[0][0].Extent();
|
||||
for (size_t i = 0; i < temp1.size(); ++i) {
|
||||
temp1[i] = LsfgImage(device, memory_allocator, extent);
|
||||
temp2[i] = LsfgImage(device, memory_allocator, extent);
|
||||
}
|
||||
for (size_t i = 0; i < LSFG_BETA_OUTPUTS; ++i) {
|
||||
const VkExtent2D level_extent{
|
||||
.width = extent.width >> i,
|
||||
.height = extent.height >> i,
|
||||
};
|
||||
out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT);
|
||||
}
|
||||
|
||||
std::vector<VkDescriptorSetLayout> layouts;
|
||||
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
|
||||
layouts.push_back(passes[0].SetLayout());
|
||||
}
|
||||
for (size_t i = 1; i < LSFG_BETA_STAGES; ++i) {
|
||||
layouts.push_back(passes[i].SetLayout());
|
||||
}
|
||||
owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts);
|
||||
|
||||
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
|
||||
first_descriptor_sets[i] = owned_sets[i];
|
||||
}
|
||||
for (size_t i = 0; i < LSFG_BETA_STAGES - 1; ++i) {
|
||||
descriptor_sets[i] = owned_sets[LSFG_HISTORY_SLOTS + i];
|
||||
}
|
||||
|
||||
const VkSampler sampler = resources.GetSampler();
|
||||
const VkSampler border_sampler = resources.GetSampler(
|
||||
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true);
|
||||
|
||||
for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) {
|
||||
LsfgDescriptorWriter(first_descriptor_sets[i])
|
||||
.AddSampler(border_sampler)
|
||||
.AddSampledImages((*inputs)[(i + 1) % LSFG_HISTORY_SLOTS])
|
||||
.AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS])
|
||||
.AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS])
|
||||
.AddStorageImages(temp1)
|
||||
.Build(device);
|
||||
}
|
||||
LsfgDescriptorWriter(descriptor_sets[0])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImages(temp1)
|
||||
.AddStorageImages(temp2)
|
||||
.Build(device);
|
||||
LsfgDescriptorWriter(descriptor_sets[1])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImages(temp2)
|
||||
.AddStorageImages(temp1)
|
||||
.Build(device);
|
||||
LsfgDescriptorWriter(descriptor_sets[2])
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImages(temp1)
|
||||
.AddStorageImages(temp2)
|
||||
.Build(device);
|
||||
LsfgDescriptorWriter(descriptor_sets[3])
|
||||
.AddUniformBuffer(resources.GetBuffer(0.5f), LsfgResources::BufferSize())
|
||||
.AddSampler(sampler)
|
||||
.AddSampledImages(temp2)
|
||||
.AddStorageImages(out_images)
|
||||
.Build(device);
|
||||
}
|
||||
|
||||
void LsfgBeta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) {
|
||||
const VkExtent2D extent = temp1[0].Extent();
|
||||
const u32 groups_x = GroupCount(extent.width, DISPATCH_TILE_SHIFT);
|
||||
const u32 groups_y = GroupCount(extent.height, DISPATCH_TILE_SHIFT);
|
||||
|
||||
LsfgBarriers barriers(cmdbuf);
|
||||
for (auto& slot : *inputs) {
|
||||
barriers.WriteToReadAll(slot);
|
||||
}
|
||||
barriers.ReadToWriteAll(temp1).Build();
|
||||
|
||||
passes[0].Bind(cmdbuf, first_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS]);
|
||||
cmdbuf.Dispatch(groups_x, groups_y, 1);
|
||||
|
||||
LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build();
|
||||
passes[1].Bind(cmdbuf, descriptor_sets[0]);
|
||||
cmdbuf.Dispatch(groups_x, groups_y, 1);
|
||||
|
||||
LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build();
|
||||
passes[2].Bind(cmdbuf, descriptor_sets[1]);
|
||||
cmdbuf.Dispatch(groups_x, groups_y, 1);
|
||||
|
||||
LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build();
|
||||
passes[3].Bind(cmdbuf, descriptor_sets[2]);
|
||||
cmdbuf.Dispatch(groups_x, groups_y, 1);
|
||||
|
||||
LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(out_images).Build();
|
||||
passes[4].Bind(cmdbuf, descriptor_sets[3]);
|
||||
cmdbuf.Dispatch(GroupCount(extent.width, OUTPUT_TILE_SHIFT),
|
||||
GroupCount(extent.height, OUTPUT_TILE_SHIFT), 1);
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/renderer_vulkan/present/lsfg_common.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class Device;
|
||||
class LsfgShaders;
|
||||
|
||||
constexpr size_t LSFG_BETA_STAGES = 5;
|
||||
constexpr size_t LSFG_BETA_OUTPUTS = 6;
|
||||
|
||||
class LsfgBeta {
|
||||
public:
|
||||
LsfgBeta() = default;
|
||||
LsfgBeta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders,
|
||||
LsfgResources& resources, vk::DescriptorPool& descriptor_pool,
|
||||
LsfgImageHistory& inputs);
|
||||
|
||||
void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count);
|
||||
|
||||
[[nodiscard]] LsfgImage& Output(size_t level) {
|
||||
return out_images[level];
|
||||
}
|
||||
|
||||
private:
|
||||
LsfgImageHistory* inputs{};
|
||||
|
||||
std::array<LsfgPass, LSFG_BETA_STAGES> passes;
|
||||
std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{};
|
||||
std::array<VkDescriptorSet, LSFG_BETA_STAGES - 1> descriptor_sets{};
|
||||
vk::DescriptorSets owned_sets;
|
||||
|
||||
LsfgImagePair temp1;
|
||||
LsfgImagePair temp2;
|
||||
std::array<LsfgImage, LSFG_BETA_OUTPUTS> out_images;
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user