mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-08-16 21:33:01 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbf96ad587 | |||
| 866881d0e3 | |||
| a56b8d3de8 | |||
| e544cb3cf6 | |||
| ca9f2d43be | |||
| e10f55d9db | |||
| 71e035f83b | |||
| 16f2564478 | |||
| 75fda70db2 | |||
| b9e052b3a7 | |||
| 69aff83ef4 | |||
| e07e269bd7 | |||
| 08232ce642 | |||
| d536a66010 | |||
| 8907523ee4 | |||
| 9b1f6801a5 | |||
| ff03ef657a | |||
| 5acddfde16 | |||
| 913a74dbd3 | |||
| cbb92e75d3 | |||
| bd7d0e0466 | |||
| cce77b8d11 | |||
| c2f4449b1c | |||
| 48ba1f3f24 | |||
| d59fcf01bf | |||
| b8456394f1 | |||
| a467dd1ba6 | |||
| c156f4760f | |||
| 13f11ebf49 | |||
| 6065e9aa09 | |||
| 8ed0ed5828 | |||
| 33067af283 | |||
| d0a054270e | |||
| 2a3507c2b9 | |||
| f71f43561d | |||
| ecbfad4193 | |||
| d76b2b5d26 | |||
| cd9527072d | |||
| ffdaf7369a |
@@ -1,11 +0,0 @@
|
|||||||
--- a/libs/context/CMakeLists.txt 2025-09-08 00:42:31.303651800 -0400
|
|
||||||
+++ b/libs/context/CMakeLists.txt 2025-09-08 00:42:40.592184300 -0400
|
|
||||||
@@ -146,7 +146,7 @@
|
|
||||||
set(ASM_LANGUAGE ASM)
|
|
||||||
endif()
|
|
||||||
elseif(BOOST_CONTEXT_ASSEMBLER STREQUAL armasm)
|
|
||||||
- set(ASM_LANGUAGE ASM_ARMASM)
|
|
||||||
+ set(ASM_LANGUAGE ASM_MARMASM)
|
|
||||||
else()
|
|
||||||
set(ASM_LANGUAGE ASM_MASM)
|
|
||||||
endif()
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
diff --git a/libs/context/CMakeLists.txt b/libs/context/CMakeLists.txt
|
|
||||||
index 8210f65..0e59dd7 100644
|
|
||||||
--- a/libs/context/CMakeLists.txt
|
|
||||||
+++ b/libs/context/CMakeLists.txt
|
|
||||||
@@ -186,7 +186,8 @@ if(BOOST_CONTEXT_IMPLEMENTATION STREQUAL "fcontext")
|
|
||||||
set_property(SOURCE ${ASM_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "/safeseh")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
- else() # masm
|
|
||||||
+ # armasm doesn't support most of these options
|
|
||||||
+ elseif(NOT BOOST_CONTEXT_ASSEMBLER STREQUAL armasm) # masm
|
|
||||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
|
||||||
set_property(SOURCE ${ASM_SOURCES} APPEND PROPERTY COMPILE_OPTIONS "-x" "assembler-with-cpp")
|
|
||||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
|
||||||
@@ -1,52 +1,62 @@
|
|||||||
From e1a946ffb79022d38351a0623f819a5419965c3e Mon Sep 17 00:00:00 2001
|
From 436fc1978c78edd085d99b33275b24be0ac96aa0 Mon Sep 17 00:00:00 2001
|
||||||
From: crueter <crueter@eden-emu.dev>
|
From: crueter <crueter@eden-emu.dev>
|
||||||
Date: Fri, 24 Oct 2025 23:41:09 -0700
|
Date: Sun, 1 Feb 2026 16:21:10 -0500
|
||||||
Subject: [PATCH] [build] Fix MinGW missing GetAddrInfoExCancel definition
|
Subject: [PATCH] Fix build on MinGW
|
||||||
|
|
||||||
MinGW does not define GetAddrInfoExCancel in its wstcpi whatever header,
|
MinGW doesn't define GetAddrInfoExCancel.
|
||||||
so to get around this we can just load it with GetProcAddress et al.
|
|
||||||
|
|
||||||
Signed-off-by: crueter <crueter@eden-emu.dev>
|
Signed-off-by: crueter <crueter@eden-emu.dev>
|
||||||
---
|
---
|
||||||
httplib.h | 14 ++++++++++++--
|
httplib.h | 18 ++++++++++++++++--
|
||||||
1 file changed, 12 insertions(+), 2 deletions(-)
|
1 file changed, 16 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
diff --git a/httplib.h b/httplib.h
|
diff --git a/httplib.h b/httplib.h
|
||||||
index e15ba44..90a76dc 100644
|
index ec8d2a2..5f9a510 100644
|
||||||
--- a/httplib.h
|
--- a/httplib.h
|
||||||
+++ b/httplib.h
|
+++ b/httplib.h
|
||||||
@@ -203,11 +203,13 @@
|
@@ -203,14 +203,17 @@
|
||||||
#error Sorry, Visual Studio versions prior to 2015 are not supported
|
#error Sorry, Visual Studio versions prior to 2015 are not supported
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
-#pragma comment(lib, "ws2_32.lib")
|
-#pragma comment(lib, "ws2_32.lib")
|
||||||
-
|
-
|
||||||
|
#ifndef _SSIZE_T_DEFINED
|
||||||
using ssize_t = __int64;
|
using ssize_t = __int64;
|
||||||
|
#define _SSIZE_T_DEFINED
|
||||||
|
#endif
|
||||||
#endif // _MSC_VER
|
#endif // _MSC_VER
|
||||||
|
|
||||||
+#if defined(_MSC_VER) || defined(__MINGW32__)
|
+#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||||
+#pragma comment(lib, "ws2_32.lib")
|
+#pragma comment(lib, "ws2_32.lib")
|
||||||
+#endif
|
+#endif
|
||||||
|
+
|
||||||
+
|
+
|
||||||
#ifndef S_ISREG
|
#ifndef S_ISREG
|
||||||
#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG)
|
#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG)
|
||||||
#endif // S_ISREG
|
#endif // S_ISREG
|
||||||
@@ -3557,7 +3559,15 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
@@ -4528,7 +4531,17 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||||
auto wait_result =
|
auto wait_result =
|
||||||
::WaitForSingleObject(event, static_cast<DWORD>(timeout_sec * 1000));
|
::WaitForSingleObject(event, static_cast<DWORD>(timeout_sec * 1000));
|
||||||
if (wait_result == WAIT_TIMEOUT) {
|
if (wait_result == WAIT_TIMEOUT) {
|
||||||
+#ifdef __MINGW32__
|
+#ifdef __MINGW32__
|
||||||
+ typedef INT (WSAAPI *PFN_GETADDRINFOEXCANCEL)(HANDLE *CancelHandle);
|
+ typedef INT(WSAAPI * PFN_GETADDRINFOEXCANCEL)(HANDLE * CancelHandle);
|
||||||
+ auto wsdll = LoadLibraryW((wchar_t*) "ws2_32.lib");
|
+ auto wsdll = LoadLibraryW((wchar_t *)"ws2_32.lib");
|
||||||
+ PFN_GETADDRINFOEXCANCEL GetAddrInfoExCancel = (PFN_GETADDRINFOEXCANCEL) GetProcAddress(wsdll, "GetAddrInfoExCancel");
|
+ PFN_GETADDRINFOEXCANCEL GetAddrInfoExCancel =
|
||||||
|
+ (PFN_GETADDRINFOEXCANCEL)GetProcAddress(wsdll, "GetAddrInfoExCancel");
|
||||||
+
|
+
|
||||||
+ if (cancel_handle) { GetAddrInfoExCancel(&cancel_handle); }
|
+ if (cancel_handle) { GetAddrInfoExCancel(&cancel_handle); }
|
||||||
+#else
|
+#else
|
||||||
if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); }
|
if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); }
|
||||||
+#endif
|
+#endif
|
||||||
|
+
|
||||||
::CloseHandle(event);
|
::CloseHandle(event);
|
||||||
return EAI_AGAIN;
|
return EAI_AGAIN;
|
||||||
}
|
}
|
||||||
|
@@ -13952,3 +13965,4 @@ inline SSL_CTX *Client::ssl_context() const {
|
||||||
|
} // namespace httplib
|
||||||
|
|
||||||
|
#endif // CPPHTTPLIB_HTTPLIB_H
|
||||||
|
+
|
||||||
--
|
--
|
||||||
2.51.0
|
2.51.2
|
||||||
|
|
||||||
|
|||||||
+14
-5
@@ -32,8 +32,8 @@ if (PLATFORM_OPENBSD)
|
|||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R6/include -D_LIBCPP_PSTL_BACKEND_SERIAL=1")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R6/include -D_LIBCPP_PSTL_BACKEND_SERIAL=1")
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R6/lib")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R6/lib")
|
||||||
elseif (PLATFORM_NETBSD)
|
elseif (PLATFORM_NETBSD)
|
||||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
|
||||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -178,7 +178,9 @@ endif()
|
|||||||
|
|
||||||
# Disable Warnings as Errors for MSVC
|
# Disable Warnings as Errors for MSVC
|
||||||
if (MSVC AND NOT CXX_CLANG)
|
if (MSVC AND NOT CXX_CLANG)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
|
# This was dripping into spirv, being overriden, and causing cl flag override warning
|
||||||
|
# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
|
||||||
|
set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} /W3 /WX-")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Set bundled sdl2/qt as dependent options.
|
# Set bundled sdl2/qt as dependent options.
|
||||||
@@ -244,11 +246,12 @@ cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib"
|
|||||||
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
|
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
|
||||||
|
|
||||||
set(DEFAULT_ENABLE_OPENSSL ON)
|
set(DEFAULT_ENABLE_OPENSSL ON)
|
||||||
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN)
|
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
|
||||||
# - Windows defaults to the Schannel backend.
|
# - Windows defaults to the Schannel backend.
|
||||||
# - macOS defaults to the SecureTransport backend.
|
# - macOS defaults to the SecureTransport backend.
|
||||||
# - Android currently has no SSL backend as the NDK doesn't include any SSL
|
# - Android currently has no SSL backend as the NDK doesn't include any SSL
|
||||||
# library; a proper 'native' backend would have to go through Java.
|
# library; a proper 'native' backend would have to go through Java.
|
||||||
|
# - Solaris and OpenBSD have too old backends
|
||||||
# But you can force builds for those platforms to use OpenSSL if you have
|
# But you can force builds for those platforms to use OpenSSL if you have
|
||||||
# your own copy of it.
|
# your own copy of it.
|
||||||
set(DEFAULT_ENABLE_OPENSSL OFF)
|
set(DEFAULT_ENABLE_OPENSSL OFF)
|
||||||
@@ -261,7 +264,7 @@ endif()
|
|||||||
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
|
||||||
|
|
||||||
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
|
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
|
||||||
if (EXT_DEFAULT OR PLATFORM_SUN)
|
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
|
||||||
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
|
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -587,6 +590,7 @@ if (NOT YUZU_STATIC_ROOM)
|
|||||||
find_package(sirit)
|
find_package(sirit)
|
||||||
find_package(gamemode)
|
find_package(gamemode)
|
||||||
find_package(mcl)
|
find_package(mcl)
|
||||||
|
find_package(frozen)
|
||||||
|
|
||||||
if (ARCHITECTURE_riscv64)
|
if (ARCHITECTURE_riscv64)
|
||||||
find_package(biscuit)
|
find_package(biscuit)
|
||||||
@@ -693,6 +697,11 @@ if (ENABLE_QT)
|
|||||||
set(QT_MAJOR_VERSION 6)
|
set(QT_MAJOR_VERSION 6)
|
||||||
# Qt6 sets cxx_std_17 and we need to undo that
|
# Qt6 sets cxx_std_17 and we need to undo that
|
||||||
set_target_properties(Qt6::Platform PROPERTIES INTERFACE_COMPILE_FEATURES "")
|
set_target_properties(Qt6::Platform PROPERTIES INTERFACE_COMPILE_FEATURES "")
|
||||||
|
|
||||||
|
## Qt Externals ##
|
||||||
|
|
||||||
|
# QuaZip
|
||||||
|
AddJsonPackage(quazip)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT YUZU_STATIC_ROOM AND NOT (YUZU_USE_BUNDLED_FFMPEG OR YUZU_USE_EXTERNAL_FFMPEG))
|
if (NOT YUZU_STATIC_ROOM AND NOT (YUZU_USE_BUNDLED_FFMPEG OR YUZU_USE_EXTERNAL_FFMPEG))
|
||||||
|
|||||||
+192
-125
@@ -41,6 +41,11 @@ function(cpm_utils_message level name message)
|
|||||||
message(${level} "[CPMUtil] ${name}: ${message}")
|
message(${level} "[CPMUtil] ${name}: ${message}")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# propagate a variable to parent scope
|
||||||
|
macro(Propagate var)
|
||||||
|
set(${var} ${${var}} PARENT_SCOPE)
|
||||||
|
endmacro()
|
||||||
|
|
||||||
function(array_to_list array length out)
|
function(array_to_list array length out)
|
||||||
math(EXPR range "${length} - 1")
|
math(EXPR range "${length} - 1")
|
||||||
|
|
||||||
@@ -72,6 +77,159 @@ function(get_json_element object out member default)
|
|||||||
set("${out}" "${outvar}" PARENT_SCOPE)
|
set("${out}" "${outvar}" PARENT_SCOPE)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# Determine whether or not a package has a viable system candidate.
|
||||||
|
function(SystemPackageViable JSON_NAME)
|
||||||
|
string(JSON object GET "${CPMFILE_CONTENT}" "${JSON_NAME}")
|
||||||
|
|
||||||
|
parse_object(${object})
|
||||||
|
|
||||||
|
string(REPLACE " " ";" find_args "${find_args}")
|
||||||
|
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
|
||||||
|
|
||||||
|
set(${pkg}_VIABLE ${${package}_FOUND} PARENT_SCOPE)
|
||||||
|
set(${pkg}_PACKAGE ${package} PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# Add several packages such that if one is bundled,
|
||||||
|
# all the rest must also be bundled.
|
||||||
|
function(AddDependentPackages)
|
||||||
|
set(_some_system OFF)
|
||||||
|
set(_some_bundled OFF)
|
||||||
|
|
||||||
|
foreach(pkg ${ARGN})
|
||||||
|
SystemPackageViable(${pkg})
|
||||||
|
|
||||||
|
if (${pkg}_VIABLE)
|
||||||
|
set(_some_system ON)
|
||||||
|
list(APPEND _system_pkgs ${${pkg}_PACKAGE})
|
||||||
|
else()
|
||||||
|
set(_some_bundled ON)
|
||||||
|
list(APPEND _bundled_pkgs ${${pkg}_PACKAGE})
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if (_some_system AND _some_bundled)
|
||||||
|
foreach(pkg ${ARGN})
|
||||||
|
list(APPEND package_names ${${pkg}_PACKAGE})
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
string(REPLACE ";" ", " package_names "${package_names}")
|
||||||
|
string(REPLACE ";" ", " bundled_names "${_bundled_pkgs}")
|
||||||
|
foreach(sys ${_system_pkgs})
|
||||||
|
list(APPEND system_names ${sys}_FORCE_BUNDLED)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
string(REPLACE ";" ", " system_names "${system_names}")
|
||||||
|
|
||||||
|
message(FATAL_ERROR "Partial dependency installation detected "
|
||||||
|
"for the following packages:\n${package_names}\n"
|
||||||
|
"You can solve this in one of two ways:\n"
|
||||||
|
"1. Install the following packages to your system if available:"
|
||||||
|
"\n\t${bundled_names}\n"
|
||||||
|
"2. Set the following variables to ON:"
|
||||||
|
"\n\t${system_names}\n"
|
||||||
|
"This may also be caused by a version mismatch, "
|
||||||
|
"such as one package being newer than the other.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach(pkg ${ARGN})
|
||||||
|
AddJsonPackage(${pkg})
|
||||||
|
endforeach()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
# json util
|
||||||
|
macro(parse_object object)
|
||||||
|
get_json_element("${object}" package package ${JSON_NAME})
|
||||||
|
get_json_element("${object}" repo repo "")
|
||||||
|
get_json_element("${object}" ci ci OFF)
|
||||||
|
get_json_element("${object}" version version "")
|
||||||
|
|
||||||
|
if(ci)
|
||||||
|
get_json_element("${object}" name name "${JSON_NAME}")
|
||||||
|
get_json_element("${object}" extension extension "tar.zst")
|
||||||
|
get_json_element("${object}" min_version min_version "")
|
||||||
|
get_json_element("${object}" raw_disabled disabled_platforms "")
|
||||||
|
|
||||||
|
if(raw_disabled)
|
||||||
|
array_to_list("${raw_disabled}"
|
||||||
|
${raw_disabled_LENGTH} disabled_platforms)
|
||||||
|
else()
|
||||||
|
set(disabled_platforms "")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
get_json_element("${object}" hash hash "")
|
||||||
|
get_json_element("${object}" hash_suffix hash_suffix "")
|
||||||
|
get_json_element("${object}" sha sha "")
|
||||||
|
get_json_element("${object}" url url "")
|
||||||
|
get_json_element("${object}" key key "")
|
||||||
|
get_json_element("${object}" tag tag "")
|
||||||
|
get_json_element("${object}" artifact artifact "")
|
||||||
|
get_json_element("${object}" git_version git_version "")
|
||||||
|
get_json_element("${object}" git_host git_host "")
|
||||||
|
get_json_element("${object}" source_subdir source_subdir "")
|
||||||
|
get_json_element("${object}" bundled bundled "unset")
|
||||||
|
get_json_element("${object}" find_args find_args "")
|
||||||
|
get_json_element("${object}" raw_patches patches "")
|
||||||
|
|
||||||
|
# okay here comes the fun part: REPLACEMENTS!
|
||||||
|
# first: tag gets %VERSION% replaced if applicable,
|
||||||
|
# with either git_version (preferred) or version
|
||||||
|
# second: artifact gets %VERSION% and %TAG% replaced
|
||||||
|
# accordingly (same rules for VERSION)
|
||||||
|
|
||||||
|
if(git_version)
|
||||||
|
set(version_replace ${git_version})
|
||||||
|
else()
|
||||||
|
set(version_replace ${version})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# TODO(crueter): fmt module for cmake
|
||||||
|
if(tag)
|
||||||
|
string(REPLACE "%VERSION%" "${version_replace}" tag ${tag})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(artifact)
|
||||||
|
string(REPLACE "%VERSION%" "${version_replace}"
|
||||||
|
artifact ${artifact})
|
||||||
|
string(REPLACE "%TAG%" "${tag}" artifact ${artifact})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# format patchdir
|
||||||
|
if(raw_patches)
|
||||||
|
math(EXPR range "${raw_patches_LENGTH} - 1")
|
||||||
|
|
||||||
|
foreach(IDX RANGE ${range})
|
||||||
|
string(JSON _patch GET "${raw_patches}" "${IDX}")
|
||||||
|
|
||||||
|
set(full_patch
|
||||||
|
"${PROJECT_SOURCE_DIR}/.patch/${JSON_NAME}/${_patch}")
|
||||||
|
if(NOT EXISTS ${full_patch})
|
||||||
|
cpm_utils_message(FATAL_ERROR ${JSON_NAME}
|
||||||
|
"specifies patch ${full_patch} which does not exist")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(APPEND patches "${full_patch}")
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
# end format patchdir
|
||||||
|
|
||||||
|
# options
|
||||||
|
get_json_element("${object}" raw_options options "")
|
||||||
|
|
||||||
|
if(raw_options)
|
||||||
|
array_to_list("${raw_options}" ${raw_options_LENGTH} options)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(options ${options} ${JSON_OPTIONS})
|
||||||
|
# end options
|
||||||
|
|
||||||
|
# system/bundled
|
||||||
|
if(bundled STREQUAL "unset" AND DEFINED JSON_BUNDLED_PACKAGE)
|
||||||
|
set(bundled ${JSON_BUNDLED_PACKAGE})
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endmacro()
|
||||||
|
|
||||||
# The preferred usage
|
# The preferred usage
|
||||||
function(AddJsonPackage)
|
function(AddJsonPackage)
|
||||||
set(oneValueArgs
|
set(oneValueArgs
|
||||||
@@ -80,7 +238,8 @@ function(AddJsonPackage)
|
|||||||
# these are overrides that can be generated at runtime,
|
# these are overrides that can be generated at runtime,
|
||||||
# so can be defined separately from the json
|
# so can be defined separately from the json
|
||||||
DOWNLOAD_ONLY
|
DOWNLOAD_ONLY
|
||||||
BUNDLED_PACKAGE)
|
BUNDLED_PACKAGE
|
||||||
|
FORCE_BUNDLED_PACKAGE)
|
||||||
|
|
||||||
set(multiValueArgs OPTIONS)
|
set(multiValueArgs OPTIONS)
|
||||||
|
|
||||||
@@ -111,24 +270,9 @@ function(AddJsonPackage)
|
|||||||
cpm_utils_message(FATAL_ERROR ${JSON_NAME} "Not found in cpmfile")
|
cpm_utils_message(FATAL_ERROR ${JSON_NAME} "Not found in cpmfile")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
get_json_element("${object}" package package ${JSON_NAME})
|
parse_object(${object})
|
||||||
get_json_element("${object}" repo repo "")
|
|
||||||
get_json_element("${object}" ci ci OFF)
|
|
||||||
get_json_element("${object}" version version "")
|
|
||||||
|
|
||||||
if(ci)
|
if(ci)
|
||||||
get_json_element("${object}" name name "${JSON_NAME}")
|
|
||||||
get_json_element("${object}" extension extension "tar.zst")
|
|
||||||
get_json_element("${object}" min_version min_version "")
|
|
||||||
get_json_element("${object}" raw_disabled disabled_platforms "")
|
|
||||||
|
|
||||||
if(raw_disabled)
|
|
||||||
array_to_list("${raw_disabled}"
|
|
||||||
${raw_disabled_LENGTH} disabled_platforms)
|
|
||||||
else()
|
|
||||||
set(disabled_platforms "")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
AddCIPackage(
|
AddCIPackage(
|
||||||
VERSION ${version}
|
VERSION ${version}
|
||||||
NAME ${name}
|
NAME ${name}
|
||||||
@@ -138,116 +282,38 @@ function(AddJsonPackage)
|
|||||||
MIN_VERSION ${min_version}
|
MIN_VERSION ${min_version}
|
||||||
DISABLED_PLATFORMS ${disabled_platforms})
|
DISABLED_PLATFORMS ${disabled_platforms})
|
||||||
|
|
||||||
# pass stuff to parent scope
|
|
||||||
set(${package}_ADDED "${${package}_ADDED}"
|
|
||||||
PARENT_SCOPE)
|
|
||||||
set(${package}_SOURCE_DIR "${${package}_SOURCE_DIR}"
|
|
||||||
PARENT_SCOPE)
|
|
||||||
set(${package}_BINARY_DIR "${${package}_BINARY_DIR}"
|
|
||||||
PARENT_SCOPE)
|
|
||||||
|
|
||||||
return()
|
|
||||||
endif()
|
|
||||||
|
|
||||||
get_json_element("${object}" hash hash "")
|
|
||||||
get_json_element("${object}" hash_suffix hash_suffix "")
|
|
||||||
get_json_element("${object}" sha sha "")
|
|
||||||
get_json_element("${object}" url url "")
|
|
||||||
get_json_element("${object}" key key "")
|
|
||||||
get_json_element("${object}" tag tag "")
|
|
||||||
get_json_element("${object}" artifact artifact "")
|
|
||||||
get_json_element("${object}" git_version git_version "")
|
|
||||||
get_json_element("${object}" git_host git_host "")
|
|
||||||
get_json_element("${object}" source_subdir source_subdir "")
|
|
||||||
get_json_element("${object}" bundled bundled "unset")
|
|
||||||
get_json_element("${object}" find_args find_args "")
|
|
||||||
get_json_element("${object}" raw_patches patches "")
|
|
||||||
|
|
||||||
# okay here comes the fun part: REPLACEMENTS!
|
|
||||||
# first: tag gets %VERSION% replaced if applicable,
|
|
||||||
# with either git_version (preferred) or version
|
|
||||||
# second: artifact gets %VERSION% and %TAG% replaced
|
|
||||||
# accordingly (same rules for VERSION)
|
|
||||||
|
|
||||||
if(git_version)
|
|
||||||
set(version_replace ${git_version})
|
|
||||||
else()
|
else()
|
||||||
set(version_replace ${version})
|
if (NOT DEFINED JSON_FORCE_BUNDLED_PACKAGE)
|
||||||
|
set(JSON_FORCE_BUNDLED_PACKAGE OFF)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
AddPackage(
|
||||||
|
NAME "${package}"
|
||||||
|
VERSION "${version}"
|
||||||
|
URL "${url}"
|
||||||
|
HASH "${hash}"
|
||||||
|
HASH_SUFFIX "${hash_suffix}"
|
||||||
|
SHA "${sha}"
|
||||||
|
REPO "${repo}"
|
||||||
|
KEY "${key}"
|
||||||
|
PATCHES "${patches}"
|
||||||
|
OPTIONS "${options}"
|
||||||
|
FIND_PACKAGE_ARGUMENTS "${find_args}"
|
||||||
|
BUNDLED_PACKAGE "${bundled}"
|
||||||
|
FORCE_BUNDLED_PACKAGE "${JSON_FORCE_BUNDLED_PACKAGE}"
|
||||||
|
SOURCE_SUBDIR "${source_subdir}"
|
||||||
|
|
||||||
|
GIT_VERSION ${git_version}
|
||||||
|
GIT_HOST ${git_host}
|
||||||
|
|
||||||
|
ARTIFACT ${artifact}
|
||||||
|
TAG ${tag})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# TODO(crueter): fmt module for cmake
|
|
||||||
if(tag)
|
|
||||||
string(REPLACE "%VERSION%" "${version_replace}" tag ${tag})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if(artifact)
|
|
||||||
string(REPLACE "%VERSION%" "${version_replace}" artifact ${artifact})
|
|
||||||
string(REPLACE "%TAG%" "${tag}" artifact ${artifact})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# format patchdir
|
|
||||||
if(raw_patches)
|
|
||||||
math(EXPR range "${raw_patches_LENGTH} - 1")
|
|
||||||
|
|
||||||
foreach(IDX RANGE ${range})
|
|
||||||
string(JSON _patch GET "${raw_patches}" "${IDX}")
|
|
||||||
|
|
||||||
set(full_patch
|
|
||||||
"${PROJECT_SOURCE_DIR}/.patch/${JSON_NAME}/${_patch}")
|
|
||||||
if(NOT EXISTS ${full_patch})
|
|
||||||
cpm_utils_message(FATAL_ERROR ${JSON_NAME}
|
|
||||||
"specifies patch ${full_patch} which does not exist")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
list(APPEND patches "${full_patch}")
|
|
||||||
endforeach()
|
|
||||||
endif()
|
|
||||||
# end format patchdir
|
|
||||||
|
|
||||||
# options
|
|
||||||
get_json_element("${object}" raw_options options "")
|
|
||||||
|
|
||||||
if(raw_options)
|
|
||||||
array_to_list("${raw_options}" ${raw_options_LENGTH} options)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
set(options ${options} ${JSON_OPTIONS})
|
|
||||||
# end options
|
|
||||||
|
|
||||||
# system/bundled
|
|
||||||
if(bundled STREQUAL "unset" AND DEFINED JSON_BUNDLED_PACKAGE)
|
|
||||||
set(bundled ${JSON_BUNDLED_PACKAGE})
|
|
||||||
endif()
|
|
||||||
|
|
||||||
AddPackage(
|
|
||||||
NAME "${package}"
|
|
||||||
VERSION "${version}"
|
|
||||||
URL "${url}"
|
|
||||||
HASH "${hash}"
|
|
||||||
HASH_SUFFIX "${hash_suffix}"
|
|
||||||
SHA "${sha}"
|
|
||||||
REPO "${repo}"
|
|
||||||
KEY "${key}"
|
|
||||||
PATCHES "${patches}"
|
|
||||||
OPTIONS "${options}"
|
|
||||||
FIND_PACKAGE_ARGUMENTS "${find_args}"
|
|
||||||
BUNDLED_PACKAGE "${bundled}"
|
|
||||||
SOURCE_SUBDIR "${source_subdir}"
|
|
||||||
|
|
||||||
GIT_VERSION ${git_version}
|
|
||||||
GIT_HOST ${git_host}
|
|
||||||
|
|
||||||
ARTIFACT ${artifact}
|
|
||||||
TAG ${tag})
|
|
||||||
|
|
||||||
# pass stuff to parent scope
|
# pass stuff to parent scope
|
||||||
set(${package}_ADDED "${${package}_ADDED}"
|
Propagate(${package}_ADDED)
|
||||||
PARENT_SCOPE)
|
Propagate(${package}_SOURCE_DIR)
|
||||||
set(${package}_SOURCE_DIR "${${package}_SOURCE_DIR}"
|
Propagate(${package}_BINARY_DIR)
|
||||||
PARENT_SCOPE)
|
|
||||||
set(${package}_BINARY_DIR "${${package}_BINARY_DIR}"
|
|
||||||
PARENT_SCOPE)
|
|
||||||
|
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
function(AddPackage)
|
function(AddPackage)
|
||||||
@@ -343,7 +409,7 @@ function(AddPackage)
|
|||||||
|
|
||||||
if(DEFINED PKG_ARGS_ARTIFACT)
|
if(DEFINED PKG_ARGS_ARTIFACT)
|
||||||
set(pkg_url
|
set(pkg_url
|
||||||
${pkg_git_url}/releases/download/${PKG_ARGS_TAG}/${PKG_ARGS_ARTIFACT})
|
"${pkg_git_url}/releases/download/${PKG_ARGS_TAG}/${PKG_ARGS_ARTIFACT}")
|
||||||
else()
|
else()
|
||||||
set(pkg_url
|
set(pkg_url
|
||||||
${pkg_git_url}/archive/refs/tags/${PKG_ARGS_TAG}.tar.gz)
|
${pkg_git_url}/archive/refs/tags/${PKG_ARGS_TAG}.tar.gz)
|
||||||
@@ -625,7 +691,8 @@ function(AddCIPackage)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (DEFINED pkgname AND NOT "${pkgname}" IN_LIST DISABLED_PLATFORMS)
|
if (DEFINED pkgname AND NOT "${pkgname}" IN_LIST DISABLED_PLATFORMS)
|
||||||
set(ARTIFACT "${ARTIFACT_NAME}-${pkgname}-${ARTIFACT_VERSION}.${ARTIFACT_EXT}")
|
set(ARTIFACT
|
||||||
|
"${ARTIFACT_NAME}-${pkgname}-${ARTIFACT_VERSION}.${ARTIFACT_EXT}")
|
||||||
|
|
||||||
AddPackage(
|
AddPackage(
|
||||||
NAME ${ARTIFACT_PACKAGE}
|
NAME ${ARTIFACT_PACKAGE}
|
||||||
|
|||||||
+18
-7
@@ -12,14 +12,12 @@
|
|||||||
"repo": "boostorg/boost",
|
"repo": "boostorg/boost",
|
||||||
"tag": "boost-%VERSION%",
|
"tag": "boost-%VERSION%",
|
||||||
"artifact": "%TAG%-cmake.tar.xz",
|
"artifact": "%TAG%-cmake.tar.xz",
|
||||||
"hash": "4fb7f6fde92762305aad8754d7643cd918dd1f3f67e104e9ab385b18c73178d72a17321354eb203b790b6702f2cf6d725a5d6e2dfbc63b1e35f9eb59fb42ece9",
|
"hash": "6ae6e94664fe7f2fb01976b59b276ac5df8085c7503fa829d810fbfe495960cfec44fa2c36e2cb23480bc19c956ed199d4952b02639a00a6c07625d4e7130c2d",
|
||||||
"git_version": "1.89.0",
|
"git_version": "1.90.0",
|
||||||
"version": "1.57",
|
"version": "1.57",
|
||||||
"find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem",
|
"find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem",
|
||||||
"patches": [
|
"patches": [
|
||||||
"0001-clang-cl.patch",
|
"0001-clang-cl.patch"
|
||||||
"0002-use-marmasm.patch",
|
|
||||||
"0003-armasm-options.patch"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"fmt": {
|
"fmt": {
|
||||||
@@ -48,9 +46,9 @@
|
|||||||
"package": "ZLIB",
|
"package": "ZLIB",
|
||||||
"repo": "madler/zlib",
|
"repo": "madler/zlib",
|
||||||
"tag": "v%VERSION%",
|
"tag": "v%VERSION%",
|
||||||
"hash": "8c9642495bafd6fad4ab9fb67f09b268c69ff9af0f4f20cf15dfc18852ff1f312bd8ca41de761b3f8d8e90e77d79f2ccacd3d4c5b19e475ecf09d021fdfe9088",
|
"hash": "06eaa3a1eaaeb31f461a2283b03a91ed8eb2406e62cd97ea1c69836324909edeecd93edd03ff0bf593d9dde223e3376149134c5b1fe2e8688c258cadf8cd60ff",
|
||||||
"version": "1.2",
|
"version": "1.2",
|
||||||
"git_version": "1.3.1",
|
"git_version": "1.3.1.2",
|
||||||
"options": [
|
"options": [
|
||||||
"ZLIB_BUILD_SHARED OFF",
|
"ZLIB_BUILD_SHARED OFF",
|
||||||
"ZLIB_INSTALL OFF"
|
"ZLIB_INSTALL OFF"
|
||||||
@@ -103,5 +101,18 @@
|
|||||||
"git_version": "1.4.335.0",
|
"git_version": "1.4.335.0",
|
||||||
"artifact": "android-binaries-%VERSION%.zip",
|
"artifact": "android-binaries-%VERSION%.zip",
|
||||||
"hash": "48167c4a17736301bd08f9290f41830443e1f18cce8ad867fc6f289b49e18b40e93c9850b377951af82f51b5b6d7313aa6a884fc5df79f5ce3df82696c1c1244"
|
"hash": "48167c4a17736301bd08f9290f41830443e1f18cce8ad867fc6f289b49e18b40e93c9850b377951af82f51b5b6d7313aa6a884fc5df79f5ce3df82696c1c1244"
|
||||||
|
},
|
||||||
|
"quazip": {
|
||||||
|
"package": "QuaZip-Qt6",
|
||||||
|
"repo": "stachenov/quazip",
|
||||||
|
"sha": "2e95c9001b",
|
||||||
|
"hash": "609c240c7f029ac26a37d8fbab51bc16284e05e128b78b9b9c0e95d083538c36047a67d682759ac990e4adb0eeb90f04f1ea7fe2253bbda7e7e3bcce32e53dd8",
|
||||||
|
"version": "1.3",
|
||||||
|
"git_version": "1.5",
|
||||||
|
"options": [
|
||||||
|
"QUAZIP_QT_MAJOR_VERSION 6",
|
||||||
|
"QUAZIP_INSTALL OFF",
|
||||||
|
"QUAZIP_ENABLE_QTEXTCODEC OFF"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@
|
|||||||
src/web_service @AleksandrPopovich
|
src/web_service @AleksandrPopovich
|
||||||
src/dynarmic @Lizzie
|
src/dynarmic @Lizzie
|
||||||
src/core @Lizzie @Maufeat @PavelBARABANOV @MrPurple666 @JPikachu
|
src/core @Lizzie @Maufeat @PavelBARABANOV @MrPurple666 @JPikachu
|
||||||
src/core/hle @Maufeat @PavelBARABANOV @SDK-Chan
|
src/core/hle @Maufeat @PavelBARABANOV
|
||||||
src/core/arm @Lizzie @MrPurple666
|
src/core/arm @Lizzie @MrPurple666
|
||||||
src/*_room @AleksandrPopovich
|
src/*_room @AleksandrPopovich
|
||||||
src/video_core @CamilleLaVey @MaranBr @Wildcard @weakboson
|
src/video_core @CamilleLaVey @MaranBr @Wildcard @weakboson
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# AddDependentPackage
|
||||||
|
|
||||||
|
Use `AddDependentPackage` when you have multiple packages that are required to all be from the system, OR bundled. This is useful in cases where e.g. versions must absolutely match.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Versioning must be handled by the package itself.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Vulkan
|
||||||
|
|
||||||
|
`cpmfile.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"vulkan-headers": {
|
||||||
|
"repo": "KhronosGroup/Vulkan-Headers",
|
||||||
|
"package": "VulkanHeaders",
|
||||||
|
"version": "1.4.317",
|
||||||
|
"hash": "26e0ad8fa34ab65a91ca62ddc54cc4410d209a94f64f2817dcdb8061dc621539a4262eab6387e9b9aa421db3dbf2cf8e2a4b041b696d0d03746bae1f25191272",
|
||||||
|
"git_version": "1.4.342",
|
||||||
|
"tag": "v%VERSION%"
|
||||||
|
},
|
||||||
|
"vulkan-utility-libraries": {
|
||||||
|
"repo": "KhronosGroup/Vulkan-Utility-Libraries",
|
||||||
|
"package": "VulkanUtilityLibraries",
|
||||||
|
"hash": "8147370f964fd82c315d6bb89adeda30186098427bf3efaa641d36282d42a263f31e96e4586bfd7ae0410ff015379c19aa4512ba160630444d3d8553afd1ec14",
|
||||||
|
"git_version": "1.4.342",
|
||||||
|
"tag": "v%VERSION%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`CMakeLists.txt`:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
AddDependentPackages(vulkan-headers vulkan-utility-libraries)
|
||||||
|
```
|
||||||
|
|
||||||
|
If Vulkan Headers are installed, but NOT Vulkan Utility Libraries, then CPMUtil will throw an error.
|
||||||
@@ -31,6 +31,10 @@ The core of CPMUtil is the [`AddPackage`](./AddPackage.md) function. [`AddPackag
|
|||||||
|
|
||||||
[`AddJsonPackage`](./AddJsonPackage.md) is the recommended method of usage for CPMUtil.
|
[`AddJsonPackage`](./AddJsonPackage.md) is the recommended method of usage for CPMUtil.
|
||||||
|
|
||||||
|
## AddDependentPackage
|
||||||
|
|
||||||
|
[`AddDependentPackage`](./AddDependentPackage.md) allows you to add multiple packages such that all of them must be from the system OR bundled.
|
||||||
|
|
||||||
## AddQt
|
## AddQt
|
||||||
|
|
||||||
[`AddQt`](./AddQt.md) adds a specific version of Qt to your project.
|
[`AddQt`](./AddQt.md) adds a specific version of Qt to your project.
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ If you have `quazip1_qt6_devel`, uninstall it. It may call `Core5Compat` on CMak
|
|||||||
|
|
||||||
## OpenBSD
|
## OpenBSD
|
||||||
|
|
||||||
|
System boost doesn't have `context` (as of 7.8); so you may need to specify `-DYUZU_USE_CPM=ON -DBoost_FORCE_BUNDLED=ON`.
|
||||||
|
|
||||||
After configuration, you may need to modify `externals/ffmpeg/CMakeFiles/ffmpeg-build/build.make` to use `-j$(nproc)` instead of just `-j`.
|
After configuration, you may need to modify `externals/ffmpeg/CMakeFiles/ffmpeg-build/build.make` to use `-j$(nproc)` instead of just `-j`.
|
||||||
|
|
||||||
`-lc++-experimental` doesn't exist in OpenBSD but the LLVM driver still tries to link against it, to solve just symlink `ln -s /usr/lib/libc++.a /usr/lib/libc++experimental.a`. Builds are currently not working due to lack of `std::jthread` and such, either compile libc++ manually or wait for ports to catch up.
|
`-lc++-experimental` doesn't exist in OpenBSD but the LLVM driver still tries to link against it, to solve just symlink `ln -s /usr/lib/libc++.a /usr/lib/libc++experimental.a`. Builds are currently not working due to lack of `std::jthread` and such, either compile libc++ manually or wait for ports to catch up.
|
||||||
|
|||||||
+1
-1
@@ -277,7 +277,7 @@ For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
pkg_add -u
|
pkg_add -u
|
||||||
pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake llvm-19.1.7p3 qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl2 libusb1-1.0.27
|
pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl2 libusb1-1.0.29
|
||||||
```
|
```
|
||||||
|
|
||||||
[Caveats](./Caveats.md#openbsd).
|
[Caveats](./Caveats.md#openbsd).
|
||||||
|
|||||||
+5
-5
@@ -82,15 +82,15 @@ You may additionally need the `Qt Extension Pack` extension if building Qt.
|
|||||||
|
|
||||||
# Build speedup
|
# Build speedup
|
||||||
|
|
||||||
If you have an HDD, use ramdisk (build in RAM):
|
If you have an HDD, use ramdisk (build in RAM), approximatedly you need 4GB for a full build with debug symbols:
|
||||||
```sh
|
```sh
|
||||||
sudo mkdir /tmp/ramdisk
|
mkdir /tmp/ramdisk
|
||||||
sudo chmod 777 /tmp/ramdisk
|
chmod 777 /tmp/ramdisk
|
||||||
# about 8GB needed
|
# about 8GB needed
|
||||||
sudo mount -t tmpfs -o size=8G myramdisk /tmp/ramdisk
|
mount -t tmpfs -o size=4G myramdisk /tmp/ramdisk
|
||||||
cmake -B /tmp/ramdisk
|
cmake -B /tmp/ramdisk
|
||||||
cmake --build /tmp/ramdisk -- -j32
|
cmake --build /tmp/ramdisk -- -j32
|
||||||
sudo umount /tmp/ramdisk
|
umount /tmp/ramdisk
|
||||||
```
|
```
|
||||||
|
|
||||||
# Assets and large files
|
# Assets and large files
|
||||||
|
|||||||
Vendored
+4
-6
@@ -83,13 +83,11 @@ endif()
|
|||||||
# mcl
|
# mcl
|
||||||
AddJsonPackage(mcl)
|
AddJsonPackage(mcl)
|
||||||
|
|
||||||
# VulkanUtilityHeaders - pulls in headers and utility libs
|
# Vulkan stuff
|
||||||
AddJsonPackage(vulkan-utility-headers)
|
AddDependentPackages(vulkan-headers vulkan-utility-libraries)
|
||||||
|
|
||||||
# small hack
|
# frozen
|
||||||
if (NOT VulkanUtilityLibraries_ADDED)
|
AddJsonPackage(frozen)
|
||||||
find_package(VulkanHeaders 1.3.274 REQUIRED)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# DiscordRPC
|
# DiscordRPC
|
||||||
if (USE_DISCORD_PRESENCE)
|
if (USE_DISCORD_PRESENCE)
|
||||||
|
|||||||
Vendored
+25
-13
@@ -28,8 +28,8 @@
|
|||||||
"httplib": {
|
"httplib": {
|
||||||
"repo": "yhirose/cpp-httplib",
|
"repo": "yhirose/cpp-httplib",
|
||||||
"tag": "v%VERSION%",
|
"tag": "v%VERSION%",
|
||||||
"hash": "e7a8877d489c97669a8ee536e1498575be921e558ed947253013fe6b67a49d4569eedd01f543caa70183b92d8ac0e8687d662a70d880954412e387317008a239",
|
"hash": "a229e24cca4afe78e5c0aa2e482f15108ac34101fd8dbd927365f15e8c37dec4de38c5277d635017d692a5b320e1b929f8bfcc076f52b8e4dcdab8fe53bfdf2e",
|
||||||
"git_version": "0.28.0",
|
"git_version": "0.30.1",
|
||||||
"find_args": "MODULE GLOBAL",
|
"find_args": "MODULE GLOBAL",
|
||||||
"patches": [
|
"patches": [
|
||||||
"0001-mingw.patch"
|
"0001-mingw.patch"
|
||||||
@@ -118,15 +118,6 @@
|
|||||||
"git_version": "1.3.18",
|
"git_version": "1.3.18",
|
||||||
"find_args": "MODULE"
|
"find_args": "MODULE"
|
||||||
},
|
},
|
||||||
"vulkan-utility-headers": {
|
|
||||||
"package": "VulkanUtilityLibraries",
|
|
||||||
"repo": "scripts/VulkanUtilityHeaders",
|
|
||||||
"tag": "%VERSION%",
|
|
||||||
"git_version": "1.4.335",
|
|
||||||
"artifact": "VulkanUtilityHeaders.tar.zst",
|
|
||||||
"git_host": "git.crueter.xyz",
|
|
||||||
"hash": "16dac0e6586702580c4279e4cd37ffe3cf909c93eb31b5069da7af36436d47b270a9cbaac953bb66c22ed12ed67ffa096688599267f307dfb62be1bc09f79833"
|
|
||||||
},
|
|
||||||
"spirv-tools": {
|
"spirv-tools": {
|
||||||
"package": "SPIRV-Tools",
|
"package": "SPIRV-Tools",
|
||||||
"repo": "KhronosGroup/SPIRV-Tools",
|
"repo": "KhronosGroup/SPIRV-Tools",
|
||||||
@@ -174,9 +165,9 @@
|
|||||||
"package": "Catch2",
|
"package": "Catch2",
|
||||||
"repo": "catchorg/Catch2",
|
"repo": "catchorg/Catch2",
|
||||||
"tag": "v%VERSION%",
|
"tag": "v%VERSION%",
|
||||||
"hash": "a95495142f915d6e9c2a23e80fe360343e9097680066a2f9d3037a070ba5f81ee5559a0407cc9e972dc2afae325873f1fc7ea07a64012c0f01aac6e549f03e3f",
|
"hash": "acb3f463a7404d6a3bce52e474075cdadf9bb241d93feaf147c182d756e5a2f8bd412f4658ca186d15ab8fed36fc587d79ec311f55642d8e4ded16df9e213656",
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"git_version": "3.11.0",
|
"git_version": "3.12.0",
|
||||||
"patches": [
|
"patches": [
|
||||||
"0001-solaris-isnan-fix.patch"
|
"0001-solaris-isnan-fix.patch"
|
||||||
]
|
]
|
||||||
@@ -281,5 +272,26 @@
|
|||||||
"tag": "%VERSION%",
|
"tag": "%VERSION%",
|
||||||
"hash": "dc37a189a44ce8b5c988ca550582431a6c7eadfd3c6e709bee6277116ee803e714333e85c9e6cbb5c69346a14d6f2cc7ed96e8aa09cc5fb8a89f945059651db6",
|
"hash": "dc37a189a44ce8b5c988ca550582431a6c7eadfd3c6e709bee6277116ee803e714333e85c9e6cbb5c69346a14d6f2cc7ed96e8aa09cc5fb8a89f945059651db6",
|
||||||
"version": "121125"
|
"version": "121125"
|
||||||
|
},
|
||||||
|
"vulkan-headers": {
|
||||||
|
"repo": "KhronosGroup/Vulkan-Headers",
|
||||||
|
"package": "VulkanHeaders",
|
||||||
|
"version": "1.4.317",
|
||||||
|
"hash": "26e0ad8fa34ab65a91ca62ddc54cc4410d209a94f64f2817dcdb8061dc621539a4262eab6387e9b9aa421db3dbf2cf8e2a4b041b696d0d03746bae1f25191272",
|
||||||
|
"git_version": "1.4.342",
|
||||||
|
"tag": "v%VERSION%"
|
||||||
|
},
|
||||||
|
"vulkan-utility-libraries": {
|
||||||
|
"repo": "KhronosGroup/Vulkan-Utility-Libraries",
|
||||||
|
"package": "VulkanUtilityLibraries",
|
||||||
|
"hash": "8147370f964fd82c315d6bb89adeda30186098427bf3efaa641d36282d42a263f31e96e4586bfd7ae0410ff015379c19aa4512ba160630444d3d8553afd1ec14",
|
||||||
|
"git_version": "1.4.342",
|
||||||
|
"tag": "v%VERSION%"
|
||||||
|
},
|
||||||
|
"frozen": {
|
||||||
|
"package": "frozen",
|
||||||
|
"repo": "serge-sans-paille/frozen",
|
||||||
|
"sha": "61dce5ae18",
|
||||||
|
"hash": "b8dfe741c82bc178dfc9749d4ab5a130cee718d9ee7b71d9b547cf5f7f23027ed0152ad250012a8546399fcc1e12187efc68d89d6731256c4d2df7d04eef8d5c"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ pkgs.mkShellNoCC {
|
|||||||
# optional components
|
# optional components
|
||||||
discord-rpc gamemode
|
discord-rpc gamemode
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,4 +614,23 @@ object NativeLibrary {
|
|||||||
* Updates the device power state to global variables
|
* Updates the device power state to global variables
|
||||||
*/
|
*/
|
||||||
external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean)
|
external fun updatePowerState(percentage: Int, isCharging: Boolean, hasBattery: Boolean)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profile manager native calls
|
||||||
|
*/
|
||||||
|
external fun getAllUsers(): Array<String>?
|
||||||
|
external fun getUserUsername(uuid: String): String?
|
||||||
|
external fun getUserCount(): Long
|
||||||
|
external fun canCreateUser(): Boolean
|
||||||
|
external fun createUser(uuid: String, username: String): Boolean
|
||||||
|
external fun updateUserUsername(uuid: String, username: String): Boolean
|
||||||
|
external fun removeUser(uuid: String): Boolean
|
||||||
|
external fun getCurrentUser(): String?
|
||||||
|
external fun setCurrentUser(uuid: String): Boolean
|
||||||
|
external fun getUserImagePath(uuid: String): String?
|
||||||
|
external fun saveUserImage(uuid: String, imagePath: String): Boolean
|
||||||
|
external fun reloadProfiles()
|
||||||
|
external fun getFirmwareAvatarCount(): Int
|
||||||
|
external fun getFirmwareAvatarImage(index: Int): ByteArray?
|
||||||
|
external fun getDefaultAccountBackupJpeg(): ByteArray
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -10,6 +10,7 @@ import android.view.LayoutInflater
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
|
||||||
import org.yuzu.yuzu_emu.model.Patch
|
import org.yuzu.yuzu_emu.model.Patch
|
||||||
|
import org.yuzu.yuzu_emu.model.PatchType
|
||||||
import org.yuzu.yuzu_emu.model.AddonViewModel
|
import org.yuzu.yuzu_emu.model.AddonViewModel
|
||||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
|
||||||
|
|
||||||
@@ -31,7 +32,12 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
|
|||||||
binding.addonSwitch.isChecked = model.enabled
|
binding.addonSwitch.isChecked = model.enabled
|
||||||
|
|
||||||
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
|
||||||
model.enabled = checked
|
if (PatchType.from(model.type) == PatchType.Update && checked) {
|
||||||
|
addonViewModel.enableOnlyThisUpdate(model)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
} else {
|
||||||
|
model.enabled = checked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val deleteAction = {
|
val deleteAction = {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ItemFirmwareAvatarBinding
|
||||||
|
|
||||||
|
class FirmwareAvatarAdapter(
|
||||||
|
private val avatars: List<Bitmap>,
|
||||||
|
private val onAvatarSelected: (Bitmap) -> Unit
|
||||||
|
) : RecyclerView.Adapter<FirmwareAvatarAdapter.AvatarViewHolder>() {
|
||||||
|
|
||||||
|
private var selectedPosition = -1
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvatarViewHolder {
|
||||||
|
val binding = ItemFirmwareAvatarBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return AvatarViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: AvatarViewHolder, position: Int) {
|
||||||
|
holder.bind(avatars[position], position == selectedPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = avatars.size
|
||||||
|
|
||||||
|
inner class AvatarViewHolder(
|
||||||
|
private val binding: ItemFirmwareAvatarBinding
|
||||||
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(avatar: Bitmap, isSelected: Boolean) {
|
||||||
|
binding.imageAvatar.setImageBitmap(avatar)
|
||||||
|
binding.root.isChecked = isSelected
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
val previousSelected = selectedPosition
|
||||||
|
selectedPosition = bindingAdapterPosition
|
||||||
|
|
||||||
|
if (previousSelected != -1) {
|
||||||
|
notifyItemChanged(previousSelected)
|
||||||
|
}
|
||||||
|
notifyItemChanged(selectedPosition)
|
||||||
|
|
||||||
|
onAvatarSelected(avatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -7,8 +10,10 @@ import android.net.Uri
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
|
||||||
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
|
||||||
|
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
|
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
|
||||||
@@ -31,6 +36,12 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
|
|||||||
path.text = Uri.parse(model.uriString).path
|
path.text = Uri.parse(model.uriString).path
|
||||||
path.marquee()
|
path.marquee()
|
||||||
|
|
||||||
|
// Set type indicator, shows below folder name, to see if DLC or Games
|
||||||
|
typeIndicator.text = when (model.type) {
|
||||||
|
DirectoryType.GAME -> activity.getString(R.string.games)
|
||||||
|
DirectoryType.EXTERNAL_CONTENT -> activity.getString(R.string.external_content)
|
||||||
|
}
|
||||||
|
|
||||||
buttonEdit.setOnClickListener {
|
buttonEdit.setOnClickListener {
|
||||||
GameFolderPropertiesDialogFragment.newInstance(model)
|
GameFolderPropertiesDialogFragment.newInstance(model)
|
||||||
.show(
|
.show(
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.adapters
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.databinding.ListItemProfileBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.UserProfile
|
||||||
|
import java.io.File
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
|
||||||
|
class ProfileAdapter(
|
||||||
|
private val onProfileClick: (UserProfile) -> Unit,
|
||||||
|
private val onEditClick: (UserProfile) -> Unit,
|
||||||
|
private val onDeleteClick: (UserProfile) -> Unit
|
||||||
|
) : RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
|
||||||
|
|
||||||
|
private var currentUserUUID: String = ""
|
||||||
|
|
||||||
|
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<UserProfile>() {
|
||||||
|
override fun areItemsTheSame(oldItem: UserProfile, newItem: UserProfile): Boolean {
|
||||||
|
return oldItem.uuid == newItem.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: UserProfile, newItem: UserProfile): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fun submitList(list: List<UserProfile>) {
|
||||||
|
differ.submitList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentUser(uuid: String) {
|
||||||
|
currentUserUUID = uuid
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
|
||||||
|
val binding = ListItemProfileBinding.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return ProfileViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
|
||||||
|
holder.bind(differ.currentList[position])
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = differ.currentList.size
|
||||||
|
|
||||||
|
inner class ProfileViewHolder(private val binding: ListItemProfileBinding) :
|
||||||
|
RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
fun bind(profile: UserProfile) {
|
||||||
|
binding.textUsername.text = profile.username
|
||||||
|
binding.textUuid.text = formatUUID(profile.uuid)
|
||||||
|
|
||||||
|
val imageFile = File(profile.imagePath)
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(profile.imagePath)
|
||||||
|
binding.imageAvatar.setImageBitmap(bitmap)
|
||||||
|
} else {
|
||||||
|
val jpegData = NativeLibrary.getDefaultAccountBackupJpeg()
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
|
||||||
|
binding.imageAvatar.setImageBitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.uuid == currentUserUUID) {
|
||||||
|
binding.checkContainer.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.checkContainer.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.root.setOnClickListener {
|
||||||
|
onProfileClick(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonEdit.setOnClickListener {
|
||||||
|
onEditClick(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonDelete.setOnClickListener {
|
||||||
|
onDeleteClick(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatUUID(uuid: String): String {
|
||||||
|
if (uuid.length != 32) return uuid
|
||||||
|
return buildString {
|
||||||
|
append(uuid.substring(0, 8))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(8, 12))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(12, 16))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(16, 20))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(20, 32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,14 +22,6 @@ import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting
|
|||||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
|
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
|
||||||
|
|
||||||
class QuickSettings(val emulationFragment: EmulationFragment) {
|
class QuickSettings(val emulationFragment: EmulationFragment) {
|
||||||
// Kinda a crappy workaround to get a title from setting keys
|
|
||||||
// Idk how to do this witthout hardcoding every single one
|
|
||||||
private fun getSettingTitle(settingKey: String): String {
|
|
||||||
return settingKey.replace("_", " ").split(" ")
|
|
||||||
.joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSettings() {
|
private fun saveSettings() {
|
||||||
if (emulationFragment.shouldUseCustom) {
|
if (emulationFragment.shouldUseCustom) {
|
||||||
NativeConfig.savePerGameConfig()
|
NativeConfig.savePerGameConfig()
|
||||||
@@ -60,6 +52,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
// settings
|
// settings
|
||||||
|
|
||||||
fun addIntSetting(
|
fun addIntSetting(
|
||||||
|
name: Int,
|
||||||
container: ViewGroup,
|
container: ViewGroup,
|
||||||
setting: IntSetting,
|
setting: IntSetting,
|
||||||
namesArrayId: Int,
|
namesArrayId: Int,
|
||||||
@@ -73,7 +66,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.expand_icon)
|
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.expand_icon)
|
||||||
val radioGroup = itemView.findViewById<RadioGroup>(R.id.radio_group)
|
val radioGroup = itemView.findViewById<RadioGroup>(R.id.radio_group)
|
||||||
|
|
||||||
titleView.text = getSettingTitle(setting.key)
|
titleView.text = YuzuApplication.appContext.getString(name)
|
||||||
|
|
||||||
val names = emulationFragment.resources.getStringArray(namesArrayId)
|
val names = emulationFragment.resources.getStringArray(namesArrayId)
|
||||||
val values = emulationFragment.resources.getIntArray(valuesArrayId)
|
val values = emulationFragment.resources.getIntArray(valuesArrayId)
|
||||||
@@ -115,6 +108,8 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addBooleanSetting(
|
fun addBooleanSetting(
|
||||||
|
name: Int,
|
||||||
|
|
||||||
container: ViewGroup,
|
container: ViewGroup,
|
||||||
setting: BooleanSetting
|
setting: BooleanSetting
|
||||||
) {
|
) {
|
||||||
@@ -125,7 +120,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
val titleView = itemView.findViewById<TextView>(R.id.switch_title)
|
val titleView = itemView.findViewById<TextView>(R.id.switch_title)
|
||||||
val switchView = itemView.findViewById<com.google.android.material.materialswitch.MaterialSwitch>(R.id.setting_switch)
|
val switchView = itemView.findViewById<com.google.android.material.materialswitch.MaterialSwitch>(R.id.setting_switch)
|
||||||
|
|
||||||
titleView.text = getSettingTitle(setting.key)
|
titleView.text = YuzuApplication.appContext.getString(name)
|
||||||
switchContainer.visibility = View.VISIBLE
|
switchContainer.visibility = View.VISIBLE
|
||||||
switchView.isChecked = setting.getBoolean()
|
switchView.isChecked = setting.getBoolean()
|
||||||
|
|
||||||
@@ -141,6 +136,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addSliderSetting(
|
fun addSliderSetting(
|
||||||
|
name: Int,
|
||||||
container: ViewGroup,
|
container: ViewGroup,
|
||||||
setting: AbstractSetting,
|
setting: AbstractSetting,
|
||||||
minValue: Int = 0,
|
minValue: Int = 0,
|
||||||
@@ -156,7 +152,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
|||||||
val slider = itemView.findViewById<com.google.android.material.slider.Slider>(R.id.setting_slider)
|
val slider = itemView.findViewById<com.google.android.material.slider.Slider>(R.id.setting_slider)
|
||||||
|
|
||||||
|
|
||||||
titleView.text = getSettingTitle(setting.key)
|
titleView.text = YuzuApplication.appContext.getString(name)
|
||||||
sliderContainer.visibility = View.VISIBLE
|
sliderContainer.visibility = View.VISIBLE
|
||||||
|
|
||||||
slider.valueFrom = minValue.toFloat()
|
slider.valueFrom = minValue.toFloat()
|
||||||
|
|||||||
+6
-1
@@ -24,6 +24,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
|||||||
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
|
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
|
||||||
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
|
||||||
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
|
||||||
|
ENABLE_BUFFER_HISTORY("enable_buffer_history"),
|
||||||
SYNC_MEMORY_OPERATIONS("sync_memory_operations"),
|
SYNC_MEMORY_OPERATIONS("sync_memory_operations"),
|
||||||
BUFFER_REORDER_DISABLE("disable_buffer_reorder"),
|
BUFFER_REORDER_DISABLE("disable_buffer_reorder"),
|
||||||
RENDERER_DEBUG("debug"),
|
RENDERER_DEBUG("debug"),
|
||||||
@@ -36,6 +37,9 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
|||||||
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
USE_CUSTOM_RTC("custom_rtc_enabled"),
|
||||||
BLACK_BACKGROUNDS("black_backgrounds"),
|
BLACK_BACKGROUNDS("black_backgrounds"),
|
||||||
|
|
||||||
|
ENABLE_FOLDER_BUTTON("enable_folder_button"),
|
||||||
|
ENABLE_QLAUNCH_BUTTON("enable_qlaunch_button"),
|
||||||
|
|
||||||
ENABLE_UPDATE_CHECKS("enable_update_checks"),
|
ENABLE_UPDATE_CHECKS("enable_update_checks"),
|
||||||
JOYSTICK_REL_CENTER("joystick_rel_center"),
|
JOYSTICK_REL_CENTER("joystick_rel_center"),
|
||||||
DPAD_SLIDE("dpad_slide"),
|
DPAD_SLIDE("dpad_slide"),
|
||||||
@@ -79,8 +83,9 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
|
|||||||
GPU_LOG_VULKAN_CALLS("gpu_log_vulkan_calls"),
|
GPU_LOG_VULKAN_CALLS("gpu_log_vulkan_calls"),
|
||||||
GPU_LOG_SHADER_DUMPS("gpu_log_shader_dumps"),
|
GPU_LOG_SHADER_DUMPS("gpu_log_shader_dumps"),
|
||||||
GPU_LOG_MEMORY_TRACKING("gpu_log_memory_tracking"),
|
GPU_LOG_MEMORY_TRACKING("gpu_log_memory_tracking"),
|
||||||
GPU_LOG_DRIVER_DEBUG("gpu_log_driver_debug");
|
GPU_LOG_DRIVER_DEBUG("gpu_log_driver_debug"),
|
||||||
|
|
||||||
|
ENABLE_QUICK_SETTINGS("enable_quick_settings");
|
||||||
|
|
||||||
// external fun isFrameSkippingEnabled(): Boolean
|
// external fun isFrameSkippingEnabled(): Boolean
|
||||||
external fun isFrameInterpolationEnabled(): Boolean
|
external fun isFrameInterpolationEnabled(): Boolean
|
||||||
|
|||||||
+33
@@ -60,6 +60,11 @@ abstract class SettingsItem(
|
|||||||
return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
|
return NativeInput.getStyleIndex(0) != NpadStyleIndex.Handheld
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Can't edit enable_qlaunch_button if firmware is not available
|
||||||
|
if (setting.key == BooleanSetting.ENABLE_QLAUNCH_BUTTON.key) {
|
||||||
|
return NativeLibrary.isFirmwareAvailable()
|
||||||
|
}
|
||||||
|
|
||||||
// Can't edit settings that aren't saveable in per-game config even if they are switchable
|
// Can't edit settings that aren't saveable in per-game config even if they are switchable
|
||||||
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
|
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
|
||||||
return false
|
return false
|
||||||
@@ -740,6 +745,13 @@ abstract class SettingsItem(
|
|||||||
descriptionId = R.string.renderer_reactive_flushing_description
|
descriptionId = R.string.renderer_reactive_flushing_description
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
put(
|
||||||
|
SwitchSetting(
|
||||||
|
BooleanSetting.ENABLE_BUFFER_HISTORY,
|
||||||
|
titleId = R.string.enable_buffer_history,
|
||||||
|
descriptionId = R.string.enable_buffer_history_description
|
||||||
|
)
|
||||||
|
)
|
||||||
put(
|
put(
|
||||||
SwitchSetting(
|
SwitchSetting(
|
||||||
BooleanSetting.SYNC_MEMORY_OPERATIONS,
|
BooleanSetting.SYNC_MEMORY_OPERATIONS,
|
||||||
@@ -794,6 +806,27 @@ abstract class SettingsItem(
|
|||||||
descriptionId = R.string.enable_update_checks_description,
|
descriptionId = R.string.enable_update_checks_description,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
put(
|
||||||
|
SwitchSetting(
|
||||||
|
BooleanSetting.ENABLE_QUICK_SETTINGS,
|
||||||
|
titleId = R.string.enable_quick_settings,
|
||||||
|
descriptionId = R.string.enable_quick_settings_description,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
SwitchSetting(
|
||||||
|
BooleanSetting.ENABLE_FOLDER_BUTTON,
|
||||||
|
titleId = R.string.enable_folder_button,
|
||||||
|
descriptionId = R.string.enable_folder_button_description,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
put(
|
||||||
|
SwitchSetting(
|
||||||
|
BooleanSetting.ENABLE_QLAUNCH_BUTTON,
|
||||||
|
titleId = R.string.enable_qlaunch_button,
|
||||||
|
descriptionId = R.string.enable_qlaunch_button_description,
|
||||||
|
)
|
||||||
|
)
|
||||||
put(
|
put(
|
||||||
SingleChoiceSetting(
|
SingleChoiceSetting(
|
||||||
IntSetting.APP_LANGUAGE,
|
IntSetting.APP_LANGUAGE,
|
||||||
|
|||||||
+10
@@ -275,6 +275,7 @@ class SettingsFragmentPresenter(
|
|||||||
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
|
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
|
||||||
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
|
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
|
||||||
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
|
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
|
||||||
|
add(BooleanSetting.ENABLE_BUFFER_HISTORY.key)
|
||||||
|
|
||||||
add(HeaderSetting(R.string.hacks))
|
add(HeaderSetting(R.string.hacks))
|
||||||
|
|
||||||
@@ -1074,6 +1075,8 @@ class SettingsFragmentPresenter(
|
|||||||
add(BooleanSetting.ENABLE_UPDATE_CHECKS.key)
|
add(BooleanSetting.ENABLE_UPDATE_CHECKS.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add(BooleanSetting.ENABLE_QUICK_SETTINGS.key)
|
||||||
|
|
||||||
add(HeaderSetting(R.string.theme_and_color))
|
add(HeaderSetting(R.string.theme_and_color))
|
||||||
|
|
||||||
|
|
||||||
@@ -1191,6 +1194,13 @@ class SettingsFragmentPresenter(
|
|||||||
descriptionId = R.string.use_black_backgrounds_description
|
descriptionId = R.string.use_black_backgrounds_description
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add(HeaderSetting(R.string.buttons))
|
||||||
|
add(BooleanSetting.ENABLE_FOLDER_BUTTON.key)
|
||||||
|
add(BooleanSetting.ENABLE_QLAUNCH_BUTTON.key)
|
||||||
|
if (!NativeLibrary.isFirmwareAvailable()) {
|
||||||
|
BooleanSetting.ENABLE_QLAUNCH_BUTTON.setBoolean(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,459 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
|
import org.yuzu.yuzu_emu.R
|
||||||
|
import org.yuzu.yuzu_emu.adapters.FirmwareAvatarAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentEditUserDialogBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.ProfileUtils
|
||||||
|
import org.yuzu.yuzu_emu.model.UserProfile
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import androidx.core.graphics.scale
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
class EditUserDialogFragment : Fragment() {
|
||||||
|
private var _binding: FragmentEditUserDialogBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
|
private var currentUUID: String = ""
|
||||||
|
private var isEditMode = false
|
||||||
|
private var selectedImageUri: Uri? = null
|
||||||
|
private var selectedFirmwareAvatar: Bitmap? = null
|
||||||
|
private var hasCustomImage = false
|
||||||
|
private var revertedToDefault = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_UUID = "uuid"
|
||||||
|
private const val ARG_USERNAME = "username"
|
||||||
|
|
||||||
|
fun newInstance(profile: UserProfile?): EditUserDialogFragment {
|
||||||
|
val fragment = EditUserDialogFragment()
|
||||||
|
profile?.let {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(ARG_UUID, it.uuid)
|
||||||
|
args.putString(ARG_USERNAME, it.username)
|
||||||
|
fragment.arguments = args
|
||||||
|
}
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val imagePickerLauncher = registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
result.data?.data?.let { uri ->
|
||||||
|
selectedImageUri = uri
|
||||||
|
loadImage(uri)
|
||||||
|
hasCustomImage = true
|
||||||
|
binding.buttonRevertImage.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentEditUserDialogBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
val existingUUID = arguments?.getString(ARG_UUID)
|
||||||
|
val existingUsername = arguments?.getString(ARG_USERNAME)
|
||||||
|
|
||||||
|
if (existingUUID != null && existingUsername != null) {
|
||||||
|
isEditMode = true
|
||||||
|
currentUUID = existingUUID
|
||||||
|
binding.toolbarNewUser.title = getString(R.string.profile_edit_user)
|
||||||
|
binding.editUsername.setText(existingUsername)
|
||||||
|
binding.textUuid.text = formatUUID(existingUUID)
|
||||||
|
binding.buttonGenerateUuid.visibility = View.GONE
|
||||||
|
|
||||||
|
val imagePath = NativeLibrary.getUserImagePath(existingUUID)
|
||||||
|
val imageFile = File(imagePath)
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
val bitmap = BitmapFactory.decodeFile(imagePath)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(bitmap)
|
||||||
|
hasCustomImage = true
|
||||||
|
binding.buttonRevertImage.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
loadDefaultAvatar()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isEditMode = false
|
||||||
|
currentUUID = ProfileUtils.generateRandomUUID()
|
||||||
|
binding.toolbarNewUser.title = getString(R.string.profile_new_user)
|
||||||
|
binding.textUuid.text = formatUUID(currentUUID)
|
||||||
|
loadDefaultAvatar()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.toolbarNewUser.setNavigationOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.editUsername.doAfterTextChanged {
|
||||||
|
validateInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonGenerateUuid.setOnClickListener {
|
||||||
|
currentUUID = ProfileUtils.generateRandomUUID()
|
||||||
|
binding.textUuid.text = formatUUID(currentUUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonSelectImage.setOnClickListener {
|
||||||
|
selectImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonRevertImage.setOnClickListener {
|
||||||
|
revertToDefaultImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeLibrary.isFirmwareAvailable()) {
|
||||||
|
binding.buttonFirmwareAvatars.visibility = View.VISIBLE
|
||||||
|
binding.buttonFirmwareAvatars.setOnClickListener {
|
||||||
|
showFirmwareAvatarPicker()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonSave.setOnClickListener {
|
||||||
|
saveUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.buttonCancel.setOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
validateInput()
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFirmwareAvatarPicker() {
|
||||||
|
val dialogView = LayoutInflater.from(requireContext())
|
||||||
|
.inflate(R.layout.dialog_firmware_avatar_picker, null)
|
||||||
|
|
||||||
|
val gridAvatars = dialogView.findViewById<RecyclerView>(R.id.grid_avatars)
|
||||||
|
val progressLoading = dialogView.findViewById<View>(R.id.progress_loading)
|
||||||
|
val textEmpty = dialogView.findViewById<View>(R.id.text_empty)
|
||||||
|
|
||||||
|
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.profile_firmware_avatars)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
val avatars = withContext(Dispatchers.IO) {
|
||||||
|
loadFirmwareAvatars()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatars.isEmpty()) {
|
||||||
|
progressLoading.visibility = View.GONE
|
||||||
|
textEmpty.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
progressLoading.visibility = View.GONE
|
||||||
|
gridAvatars.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val adapter = FirmwareAvatarAdapter(avatars) { selectedAvatar ->
|
||||||
|
val scaledBitmap = selectedAvatar.scale(256, 256)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(scaledBitmap)
|
||||||
|
selectedFirmwareAvatar = scaledBitmap
|
||||||
|
hasCustomImage = true
|
||||||
|
binding.buttonRevertImage.visibility = View.VISIBLE
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
gridAvatars.apply {
|
||||||
|
layoutManager = GridLayoutManager(requireContext(), 4)
|
||||||
|
this.adapter = adapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadFirmwareAvatars(): List<Bitmap> {
|
||||||
|
val avatars = mutableListOf<Bitmap>()
|
||||||
|
val count = NativeLibrary.getFirmwareAvatarCount()
|
||||||
|
|
||||||
|
for (i in 0 until count) {
|
||||||
|
try {
|
||||||
|
val imageData = NativeLibrary.getFirmwareAvatarImage(i) ?: continue
|
||||||
|
|
||||||
|
val argbData = IntArray(256 * 256)
|
||||||
|
for (pixel in 0 until 256 * 256) {
|
||||||
|
val offset = pixel * 4
|
||||||
|
val r = imageData[offset].toInt() and 0xFF
|
||||||
|
val g = imageData[offset + 1].toInt() and 0xFF
|
||||||
|
val b = imageData[offset + 2].toInt() and 0xFF
|
||||||
|
val a = imageData[offset + 3].toInt() and 0xFF
|
||||||
|
argbData[pixel] = (a shl 24) or (r shl 16) or (g shl 8) or b
|
||||||
|
}
|
||||||
|
|
||||||
|
val bitmap = Bitmap.createBitmap(argbData, 256, 256, Bitmap.Config.ARGB_8888)
|
||||||
|
avatars.add(bitmap)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return avatars
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatUUID(uuid: String): String {
|
||||||
|
if (uuid.length != 32) return uuid
|
||||||
|
return buildString {
|
||||||
|
append(uuid.substring(0, 8))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(8, 12))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(12, 16))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(16, 20))
|
||||||
|
append("-")
|
||||||
|
append(uuid.substring(20, 32))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateInput() {
|
||||||
|
val username = binding.editUsername.text.toString()
|
||||||
|
val isValid = username.isNotEmpty() && username.length <= 32
|
||||||
|
binding.buttonSave.isEnabled = isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectImage() {
|
||||||
|
val intent = Intent(Intent.ACTION_PICK).apply {
|
||||||
|
type = "image/*"
|
||||||
|
}
|
||||||
|
imagePickerLauncher.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadImage(uri: Uri) {
|
||||||
|
try {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val source = ImageDecoder.createSource(requireContext().contentResolver, uri)
|
||||||
|
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
|
||||||
|
decoder.setTargetSampleSize(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaStore.Images.Media.getBitmap(requireContext().contentResolver, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
val croppedBitmap = centerCropBitmap(bitmap, 256, 256)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(croppedBitmap)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(getString(R.string.profile_image_load_error, e.message))
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadDefaultAvatar() {
|
||||||
|
val jpegData = NativeLibrary.getDefaultAccountBackupJpeg()
|
||||||
|
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
|
||||||
|
binding.imageUserAvatar.setImageBitmap(bitmap)
|
||||||
|
|
||||||
|
hasCustomImage = false
|
||||||
|
binding.buttonRevertImage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun revertToDefaultImage() {
|
||||||
|
selectedImageUri = null
|
||||||
|
selectedFirmwareAvatar = null
|
||||||
|
revertedToDefault = true
|
||||||
|
loadDefaultAvatar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveUser() {
|
||||||
|
val username = binding.editUsername.text.toString()
|
||||||
|
|
||||||
|
if (isEditMode) {
|
||||||
|
if (NativeLibrary.updateUserUsername(currentUUID, username)) {
|
||||||
|
saveImageIfNeeded()
|
||||||
|
findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
showError(getString(R.string.profile_update_failed))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (NativeLibrary.createUser(currentUUID, username)) {
|
||||||
|
saveImageIfNeeded()
|
||||||
|
findNavController().popBackStack()
|
||||||
|
} else {
|
||||||
|
showError(getString(R.string.profile_create_failed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveImageIfNeeded() {
|
||||||
|
if (revertedToDefault && isEditMode) {
|
||||||
|
val imagePath = NativeLibrary.getUserImagePath(currentUUID)
|
||||||
|
if (imagePath != null) {
|
||||||
|
val imageFile = File(imagePath)
|
||||||
|
if (imageFile.exists()) {
|
||||||
|
imageFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCustomImage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val bitmapToSave: Bitmap? = when {
|
||||||
|
selectedFirmwareAvatar != null -> selectedFirmwareAvatar
|
||||||
|
selectedImageUri != null -> {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
val source = ImageDecoder.createSource(
|
||||||
|
requireContext().contentResolver,
|
||||||
|
selectedImageUri!!
|
||||||
|
)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaStore.Images.Media.getBitmap(
|
||||||
|
requireContext().contentResolver,
|
||||||
|
selectedImageUri
|
||||||
|
)
|
||||||
|
}
|
||||||
|
centerCropBitmap(bitmap, 256, 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmapToSave == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val tempFile = File(requireContext().cacheDir, "temp_avatar_${currentUUID}.jpg")
|
||||||
|
FileOutputStream(tempFile).use { out ->
|
||||||
|
bitmapToSave.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.saveUserImage(currentUUID, tempFile.absolutePath)
|
||||||
|
|
||||||
|
tempFile.delete()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError(getString(R.string.profile_image_save_error, e.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun centerCropBitmap(source: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
|
||||||
|
val sourceWidth = source.width
|
||||||
|
val sourceHeight = source.height
|
||||||
|
|
||||||
|
val scale = maxOf(
|
||||||
|
targetWidth.toFloat() / sourceWidth,
|
||||||
|
targetHeight.toFloat() / sourceHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
val scaledWidth = (sourceWidth * scale).toInt()
|
||||||
|
val scaledHeight = (sourceHeight * scale).toInt()
|
||||||
|
|
||||||
|
val scaledBitmap = source.scale(scaledWidth, scaledHeight)
|
||||||
|
|
||||||
|
val x = (scaledWidth - targetWidth) / 2
|
||||||
|
val y = (scaledHeight - targetHeight) / 2
|
||||||
|
|
||||||
|
return Bitmap.createBitmap(scaledBitmap, x, y, targetWidth, targetHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showError(message: String) {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.error)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() =
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInset = barInsets.left + cutoutInsets.left
|
||||||
|
val topInset = cutoutInsets.top
|
||||||
|
val rightInset = barInsets.right + cutoutInsets.right
|
||||||
|
val bottomInset = barInsets.bottom + cutoutInsets.bottom
|
||||||
|
|
||||||
|
binding.appbar.updatePadding(
|
||||||
|
left = leftInset,
|
||||||
|
top = topInset,
|
||||||
|
right = rightInset
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.scrollContent.updatePadding(
|
||||||
|
left = leftInset,
|
||||||
|
right = rightInset
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.buttonContainer.updatePadding(
|
||||||
|
left = leftInset,
|
||||||
|
right = rightInset,
|
||||||
|
bottom = bottomInset
|
||||||
|
)
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -690,8 +690,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
})
|
})
|
||||||
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
|
||||||
|
|
||||||
|
if (!BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()) {
|
||||||
|
binding.drawerLayout.setDrawerLockMode(
|
||||||
|
DrawerLayout.LOCK_MODE_LOCKED_CLOSED,
|
||||||
|
binding.quickSettingsSheet
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
updateGameTitle()
|
updateGameTitle()
|
||||||
|
|
||||||
|
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
|
||||||
|
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
|
||||||
|
|
||||||
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
|
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
|
||||||
val lockMode = IntSetting.LOCK_DRAWER.getInt()
|
val lockMode = IntSetting.LOCK_DRAWER.getInt()
|
||||||
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
|
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
|
||||||
@@ -749,10 +759,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.menu_quick_settings -> {
|
if (BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean())
|
||||||
openQuickSettingsMenu()
|
R.id.menu_quick_settings else 0 -> {
|
||||||
true
|
openQuickSettingsMenu()
|
||||||
}
|
true
|
||||||
|
}
|
||||||
|
|
||||||
R.id.menu_settings_per_game -> {
|
R.id.menu_settings_per_game -> {
|
||||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||||
@@ -1045,11 +1056,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
quickSettings.addBooleanSetting(
|
quickSettings.addBooleanSetting(
|
||||||
|
R.string.frame_limit_enable,
|
||||||
container,
|
container,
|
||||||
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
|
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
|
||||||
)
|
)
|
||||||
|
|
||||||
quickSettings.addSliderSetting(
|
quickSettings.addSliderSetting(
|
||||||
|
R.string.frame_limit_slider,
|
||||||
container,
|
container,
|
||||||
ShortSetting.RENDERER_SPEED_LIMIT,
|
ShortSetting.RENDERER_SPEED_LIMIT,
|
||||||
minValue = 0,
|
minValue = 0,
|
||||||
@@ -1058,6 +1071,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
)
|
)
|
||||||
|
|
||||||
quickSettings.addBooleanSetting(
|
quickSettings.addBooleanSetting(
|
||||||
|
R.string.use_docked_mode,
|
||||||
container,
|
container,
|
||||||
BooleanSetting.USE_DOCKED_MODE,
|
BooleanSetting.USE_DOCKED_MODE,
|
||||||
)
|
)
|
||||||
@@ -1065,6 +1079,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
quickSettings.addDivider(container)
|
quickSettings.addDivider(container)
|
||||||
|
|
||||||
quickSettings.addIntSetting(
|
quickSettings.addIntSetting(
|
||||||
|
R.string.renderer_accuracy,
|
||||||
container,
|
container,
|
||||||
IntSetting.RENDERER_ACCURACY,
|
IntSetting.RENDERER_ACCURACY,
|
||||||
R.array.rendererAccuracyNames,
|
R.array.rendererAccuracyNames,
|
||||||
@@ -1073,6 +1088,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
|
|
||||||
|
|
||||||
quickSettings.addIntSetting(
|
quickSettings.addIntSetting(
|
||||||
|
R.string.renderer_scaling_filter,
|
||||||
container,
|
container,
|
||||||
IntSetting.RENDERER_SCALING_FILTER,
|
IntSetting.RENDERER_SCALING_FILTER,
|
||||||
R.array.rendererScalingFilterNames,
|
R.array.rendererScalingFilterNames,
|
||||||
@@ -1080,6 +1096,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
)
|
)
|
||||||
|
|
||||||
quickSettings.addSliderSetting(
|
quickSettings.addSliderSetting(
|
||||||
|
R.string.fsr_sharpness,
|
||||||
container,
|
container,
|
||||||
IntSetting.FSR_SHARPENING_SLIDER,
|
IntSetting.FSR_SHARPENING_SLIDER,
|
||||||
minValue = 0,
|
minValue = 0,
|
||||||
@@ -1088,6 +1105,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
|||||||
)
|
)
|
||||||
|
|
||||||
quickSettings.addIntSetting(
|
quickSettings.addIntSetting(
|
||||||
|
R.string.renderer_anti_aliasing,
|
||||||
container,
|
container,
|
||||||
IntSetting.RENDERER_ANTI_ALIASING,
|
IntSetting.RENDERER_ANTI_ALIASING,
|
||||||
R.array.rendererAntiAliasingNames,
|
R.array.rendererAntiAliasingNames,
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
@@ -110,7 +110,7 @@ class FreedrenoSettingsFragment : Fragment() {
|
|||||||
val commonVars = listOf(
|
val commonVars = listOf(
|
||||||
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
|
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
|
||||||
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
|
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
|
||||||
"TU_BREADCRUMBS"
|
"TU_BREADCRUMBS", "FD_DEV_FEATURES"
|
||||||
)
|
)
|
||||||
|
|
||||||
for (varName in commonVars) {
|
for (varName in commonVars) {
|
||||||
|
|||||||
+19
-8
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -6,11 +9,13 @@ package org.yuzu.yuzu_emu.fragments
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||||
import org.yuzu.yuzu_emu.model.GameDir
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
@@ -25,14 +30,18 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||||||
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
|
||||||
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
|
||||||
|
|
||||||
// Restore checkbox state
|
// Hide deepScan for external content, do automatically
|
||||||
binding.deepScanSwitch.isChecked =
|
if (gameDir.type == DirectoryType.EXTERNAL_CONTENT) {
|
||||||
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
binding.deepScanSwitch.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
// Restore checkbox state for game dirs
|
||||||
|
binding.deepScanSwitch.isChecked =
|
||||||
|
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
|
||||||
|
|
||||||
// Ensure that we can get the checkbox state even if the view is destroyed
|
|
||||||
deepScan = binding.deepScanSwitch.isChecked
|
|
||||||
binding.deepScanSwitch.setOnClickListener {
|
|
||||||
deepScan = binding.deepScanSwitch.isChecked
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
binding.deepScanSwitch.setOnClickListener {
|
||||||
|
deepScan = binding.deepScanSwitch.isChecked
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MaterialAlertDialogBuilder(requireContext())
|
return MaterialAlertDialogBuilder(requireContext())
|
||||||
@@ -41,8 +50,10 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
|
|||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||||
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
|
||||||
if (folderIndex != -1) {
|
if (folderIndex != -1) {
|
||||||
gamesViewModel.folders.value[folderIndex].deepScan =
|
if (gameDir.type == DirectoryType.GAME) {
|
||||||
binding.deepScanSwitch.isChecked
|
gamesViewModel.folders.value[folderIndex].deepScan =
|
||||||
|
binding.deepScanSwitch.isChecked
|
||||||
|
}
|
||||||
gamesViewModel.updateGameDirs()
|
gamesViewModel.updateGameDirs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
@@ -15,11 +15,14 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
import org.yuzu.yuzu_emu.adapters.FolderAdapter
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.DirectoryType
|
||||||
|
import org.yuzu.yuzu_emu.model.GameDir
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
import org.yuzu.yuzu_emu.ui.main.MainActivity
|
||||||
@@ -73,7 +76,25 @@ class GameFoldersFragment : Fragment() {
|
|||||||
|
|
||||||
val mainActivity = requireActivity() as MainActivity
|
val mainActivity = requireActivity() as MainActivity
|
||||||
binding.buttonAdd.setOnClickListener {
|
binding.buttonAdd.setOnClickListener {
|
||||||
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
// Show a model to choose between Game and External Content
|
||||||
|
val options = arrayOf(
|
||||||
|
getString(R.string.games),
|
||||||
|
getString(R.string.external_content)
|
||||||
|
)
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.add_folders)
|
||||||
|
.setItems(options) { _, which ->
|
||||||
|
when (which) {
|
||||||
|
0 -> { // Game Folder
|
||||||
|
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
|
}
|
||||||
|
1 -> { // External Content Folder
|
||||||
|
mainActivity.getExternalContentDirectory.launch(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
setInsets()
|
setInsets()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
@@ -114,6 +117,17 @@ class HomeSettingsFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
add(
|
||||||
|
HomeSetting(
|
||||||
|
R.string.profile_manager,
|
||||||
|
R.string.profile_manager_description,
|
||||||
|
R.drawable.ic_account_circle,
|
||||||
|
{
|
||||||
|
binding.root.findNavController()
|
||||||
|
.navigate(R.id.action_homeSettingsFragment_to_profileManagerFragment)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
add(
|
add(
|
||||||
HomeSetting(
|
HomeSetting(
|
||||||
R.string.gpu_driver_manager,
|
R.string.gpu_driver_manager,
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// 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.core.view.ViewCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
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.ProfileAdapter
|
||||||
|
import org.yuzu.yuzu_emu.databinding.FragmentProfileManagerBinding
|
||||||
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
|
import org.yuzu.yuzu_emu.model.UserProfile
|
||||||
|
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||||
|
|
||||||
|
class ProfileManagerFragment : Fragment() {
|
||||||
|
private var _binding: FragmentProfileManagerBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
private lateinit var profileAdapter: ProfileAdapter
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
_binding = FragmentProfileManagerBinding.inflate(layoutInflater)
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
||||||
|
|
||||||
|
binding.toolbarProfiles.setNavigationOnClickListener {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupRecyclerView()
|
||||||
|
loadProfiles()
|
||||||
|
|
||||||
|
binding.buttonAddUser.setOnClickListener {
|
||||||
|
if (NativeLibrary.canCreateUser()) {
|
||||||
|
findNavController().navigate(R.id.action_profileManagerFragment_to_newUserDialog)
|
||||||
|
} else {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.profile_max_users_title)
|
||||||
|
.setMessage(R.string.profile_max_users_message)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupRecyclerView() {
|
||||||
|
profileAdapter = ProfileAdapter(
|
||||||
|
onProfileClick = { profile -> selectProfile(profile) },
|
||||||
|
onEditClick = { profile -> editProfile(profile) },
|
||||||
|
onDeleteClick = { profile -> confirmDeleteProfile(profile) }
|
||||||
|
)
|
||||||
|
binding.listProfiles.apply {
|
||||||
|
layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
adapter = profileAdapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadProfiles() {
|
||||||
|
val profiles = mutableListOf<UserProfile>()
|
||||||
|
val userUUIDs = NativeLibrary.getAllUsers() ?: emptyArray()
|
||||||
|
val currentUserUUID = NativeLibrary.getCurrentUser()
|
||||||
|
|
||||||
|
for (uuid in userUUIDs) {
|
||||||
|
if (uuid.isNotEmpty()) {
|
||||||
|
val username = NativeLibrary.getUserUsername(uuid)
|
||||||
|
if (!username.isNullOrEmpty()) {
|
||||||
|
val imagePath = NativeLibrary.getUserImagePath(uuid) ?: ""
|
||||||
|
profiles.add(UserProfile(uuid, username, imagePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
profileAdapter.submitList(profiles)
|
||||||
|
profileAdapter.setCurrentUser(currentUserUUID ?: "")
|
||||||
|
|
||||||
|
binding.buttonAddUser.isEnabled = NativeLibrary.canCreateUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectProfile(profile: UserProfile) {
|
||||||
|
if (NativeLibrary.setCurrentUser(profile.uuid)) {
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun editProfile(profile: UserProfile) {
|
||||||
|
val bundle = Bundle().apply {
|
||||||
|
putString("uuid", profile.uuid)
|
||||||
|
putString("username", profile.username)
|
||||||
|
}
|
||||||
|
findNavController().navigate(R.id.action_profileManagerFragment_to_newUserDialog, bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun confirmDeleteProfile(profile: UserProfile) {
|
||||||
|
val currentUser = NativeLibrary.getCurrentUser()
|
||||||
|
val isCurrentUser = profile.uuid == currentUser
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.profile_delete_confirm_title)
|
||||||
|
.setMessage(
|
||||||
|
if (isCurrentUser) {
|
||||||
|
getString(R.string.profile_delete_current_user_message, profile.username)
|
||||||
|
} else {
|
||||||
|
getString(R.string.profile_delete_confirm_message, profile.username)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.setPositiveButton(R.string.profile_delete) { _, _ ->
|
||||||
|
deleteProfile(profile)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteProfile(profile: UserProfile) {
|
||||||
|
val currentUser = NativeLibrary.getCurrentUser()
|
||||||
|
if (!currentUser.isNullOrEmpty() && profile.uuid == currentUser) {
|
||||||
|
val users = NativeLibrary.getAllUsers() ?: emptyArray()
|
||||||
|
for (uuid in users) {
|
||||||
|
if (uuid.isNotEmpty() && uuid != profile.uuid) {
|
||||||
|
NativeLibrary.setCurrentUser(uuid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NativeLibrary.removeUser(profile.uuid)) {
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
|
binding.root
|
||||||
|
) { _: View, windowInsets: WindowInsetsCompat ->
|
||||||
|
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||||
|
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
||||||
|
|
||||||
|
val leftInsets = barInsets.left + cutoutInsets.left
|
||||||
|
val rightInsets = barInsets.right + cutoutInsets.right
|
||||||
|
|
||||||
|
val fabLayoutParams = binding.buttonAddUser.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
fabLayoutParams.leftMargin = leftInsets + 24
|
||||||
|
fabLayoutParams.rightMargin = rightInsets + 24
|
||||||
|
fabLayoutParams.bottomMargin = barInsets.bottom + 24
|
||||||
|
binding.buttonAddUser.layoutParams = fabLayoutParams
|
||||||
|
|
||||||
|
windowInsets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
super.onDestroyView()
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
_binding = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -48,16 +51,68 @@ class AddonViewModel : ViewModel() {
|
|||||||
?: emptyArray()
|
?: emptyArray()
|
||||||
).toMutableList()
|
).toMutableList()
|
||||||
patchList.sortBy { it.name }
|
patchList.sortBy { it.name }
|
||||||
|
|
||||||
|
// Ensure only one update is enabled
|
||||||
|
ensureSingleUpdateEnabled(patchList)
|
||||||
|
|
||||||
|
removeDuplicates(patchList)
|
||||||
|
|
||||||
_patchList.value = patchList
|
_patchList.value = patchList
|
||||||
isRefreshing.set(false)
|
isRefreshing.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ensureSingleUpdateEnabled(patchList: MutableList<Patch>) {
|
||||||
|
val updates = patchList.filter { PatchType.from(it.type) == PatchType.Update }
|
||||||
|
if (updates.size <= 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val enabledUpdates = updates.filter { it.enabled }
|
||||||
|
|
||||||
|
if (enabledUpdates.size > 1) {
|
||||||
|
val nandOrSdmcEnabled = enabledUpdates.find {
|
||||||
|
it.name.contains("(NAND)") || it.name.contains("(SDMC)")
|
||||||
|
}
|
||||||
|
|
||||||
|
val updateToKeep = nandOrSdmcEnabled ?: enabledUpdates.first()
|
||||||
|
|
||||||
|
for (patch in patchList) {
|
||||||
|
if (PatchType.from(patch.type) == PatchType.Update) {
|
||||||
|
patch.enabled = (patch === updateToKeep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeDuplicates(patchList: MutableList<Patch>) {
|
||||||
|
val seen = mutableSetOf<String>()
|
||||||
|
val iterator = patchList.iterator()
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
val patch = iterator.next()
|
||||||
|
val key = "${patch.name}|${patch.version}|${patch.type}"
|
||||||
|
if (seen.contains(key)) {
|
||||||
|
iterator.remove()
|
||||||
|
} else {
|
||||||
|
seen.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setAddonToDelete(patch: Patch?) {
|
fun setAddonToDelete(patch: Patch?) {
|
||||||
_addonToDelete.value = patch
|
_addonToDelete.value = patch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun enableOnlyThisUpdate(selectedPatch: Patch) {
|
||||||
|
val currentList = _patchList.value
|
||||||
|
for (patch in currentList) {
|
||||||
|
if (PatchType.from(patch.type) == PatchType.Update) {
|
||||||
|
patch.enabled = (patch === selectedPatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onDeleteAddon(patch: Patch) {
|
fun onDeleteAddon(patch: Patch) {
|
||||||
when (PatchType.from(patch.type)) {
|
when (PatchType.from(patch.type)) {
|
||||||
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
|
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
|
||||||
@@ -72,13 +127,27 @@ class AddonViewModel : ViewModel() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if there are multiple update versions
|
||||||
|
val updates = _patchList.value.filter { PatchType.from(it.type) == PatchType.Update }
|
||||||
|
val hasMultipleUpdates = updates.size > 1
|
||||||
|
|
||||||
NativeConfig.setDisabledAddons(
|
NativeConfig.setDisabledAddons(
|
||||||
game!!.programId,
|
game!!.programId,
|
||||||
_patchList.value.mapNotNull {
|
_patchList.value.mapNotNull {
|
||||||
if (it.enabled) {
|
if (it.enabled) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
it.name
|
if (PatchType.from(it.type) == PatchType.Update) {
|
||||||
|
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
|
||||||
|
it.name
|
||||||
|
} else if (hasMultipleUpdates) {
|
||||||
|
"Update@${it.numericVersion}"
|
||||||
|
} else {
|
||||||
|
it.name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
it.name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -9,5 +12,14 @@ import kotlinx.parcelize.Parcelize
|
|||||||
@Parcelize
|
@Parcelize
|
||||||
data class GameDir(
|
data class GameDir(
|
||||||
val uriString: String,
|
val uriString: String,
|
||||||
var deepScan: Boolean
|
var deepScan: Boolean,
|
||||||
) : Parcelable
|
val type: DirectoryType = DirectoryType.GAME
|
||||||
|
) : Parcelable {
|
||||||
|
// Needed for JNI backward compatability
|
||||||
|
constructor(uriString: String, deepScan: Boolean) : this(uriString, deepScan, DirectoryType.GAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DirectoryType {
|
||||||
|
GAME,
|
||||||
|
EXTERNAL_CONTENT
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.model
|
package org.yuzu.yuzu_emu.model
|
||||||
@@ -56,7 +56,7 @@ class GamesViewModel : ViewModel() {
|
|||||||
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
// Ensure keys are loaded so that ROM metadata can be decrypted.
|
||||||
NativeLibrary.reloadKeys()
|
NativeLibrary.reloadKeys()
|
||||||
|
|
||||||
getGameDirs()
|
getGameDirsAndExternalContent()
|
||||||
reloadGames(directoriesChanged = false, firstStartup = true)
|
reloadGames(directoriesChanged = false, firstStartup = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,11 +144,19 @@ class GamesViewModel : ViewModel() {
|
|||||||
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
|
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
NativeConfig.addGameDir(gameDir)
|
when (gameDir.type) {
|
||||||
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
DirectoryType.GAME -> {
|
||||||
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
|
NativeConfig.addGameDir(gameDir)
|
||||||
|
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
|
||||||
getGameDirs(!isFirstTimeSetup)
|
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
|
||||||
|
getGameDirsAndExternalContent(!isFirstTimeSetup)
|
||||||
|
}
|
||||||
|
DirectoryType.EXTERNAL_CONTENT -> {
|
||||||
|
addExternalContentDir(gameDir.uriString)
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
getGameDirsAndExternalContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedFromGameFragment) {
|
if (savedFromGameFragment) {
|
||||||
@@ -168,8 +176,15 @@ class GamesViewModel : ViewModel() {
|
|||||||
val removedDirIndex = gameDirs.indexOf(gameDir)
|
val removedDirIndex = gameDirs.indexOf(gameDir)
|
||||||
if (removedDirIndex != -1) {
|
if (removedDirIndex != -1) {
|
||||||
gameDirs.removeAt(removedDirIndex)
|
gameDirs.removeAt(removedDirIndex)
|
||||||
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
when (gameDir.type) {
|
||||||
getGameDirs()
|
DirectoryType.GAME -> {
|
||||||
|
NativeConfig.setGameDirs(gameDirs.filter { it.type == DirectoryType.GAME }.toTypedArray())
|
||||||
|
}
|
||||||
|
DirectoryType.EXTERNAL_CONTENT -> {
|
||||||
|
removeExternalContentDir(gameDir.uriString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getGameDirsAndExternalContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -177,15 +192,16 @@ class GamesViewModel : ViewModel() {
|
|||||||
fun updateGameDirs() =
|
fun updateGameDirs() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
NativeConfig.setGameDirs(_folders.value.toTypedArray())
|
val gameDirs = _folders.value.filter { it.type == DirectoryType.GAME }
|
||||||
getGameDirs()
|
NativeConfig.setGameDirs(gameDirs.toTypedArray())
|
||||||
|
getGameDirsAndExternalContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onOpenGameFoldersFragment() =
|
fun onOpenGameFoldersFragment() =
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
getGameDirs()
|
getGameDirsAndExternalContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,16 +209,36 @@ class GamesViewModel : ViewModel() {
|
|||||||
NativeConfig.saveGlobalConfig()
|
NativeConfig.saveGlobalConfig()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
getGameDirs(true)
|
getGameDirsAndExternalContent(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGameDirs(reloadList: Boolean = false) {
|
private fun getGameDirsAndExternalContent(reloadList: Boolean = false) {
|
||||||
val gameDirs = NativeConfig.getGameDirs()
|
val gameDirs = NativeConfig.getGameDirs().toMutableList()
|
||||||
_folders.value = gameDirs.toMutableList()
|
val externalContentDirs = NativeConfig.getExternalContentDirs().map {
|
||||||
|
GameDir(it, false, DirectoryType.EXTERNAL_CONTENT)
|
||||||
|
}
|
||||||
|
gameDirs.addAll(externalContentDirs)
|
||||||
|
_folders.value = gameDirs
|
||||||
if (reloadList) {
|
if (reloadList) {
|
||||||
reloadGames(true)
|
reloadGames(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addExternalContentDir(path: String) {
|
||||||
|
val currentDirs = NativeConfig.getExternalContentDirs().toMutableList()
|
||||||
|
if (!currentDirs.contains(path)) {
|
||||||
|
currentDirs.add(path)
|
||||||
|
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeExternalContentDir(path: String) {
|
||||||
|
val currentDirs = NativeConfig.getExternalContentDirs().toMutableList()
|
||||||
|
currentDirs.remove(path)
|
||||||
|
NativeConfig.setExternalContentDirs(currentDirs.toTypedArray())
|
||||||
|
NativeConfig.saveGlobalConfig()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -12,5 +15,6 @@ data class Patch(
|
|||||||
val version: String,
|
val version: String,
|
||||||
val type: Int,
|
val type: Int,
|
||||||
val programId: String,
|
val programId: String,
|
||||||
val titleId: String
|
val titleId: String,
|
||||||
|
val numericVersion: Long = 0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
package org.yuzu.yuzu_emu.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class UserProfile(
|
||||||
|
val uuid: String,
|
||||||
|
val username: String,
|
||||||
|
val imagePath: String = ""
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
object ProfileUtils {
|
||||||
|
fun generateRandomUUID(): String {
|
||||||
|
val uuid = java.util.UUID.randomUUID()
|
||||||
|
return uuid.toString().replace("-", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.ui
|
package org.yuzu.yuzu_emu.ui
|
||||||
@@ -13,6 +13,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
@@ -27,10 +28,14 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
import org.yuzu.yuzu_emu.adapters.GameAdapter
|
||||||
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
|
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
|
||||||
|
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||||
|
import org.yuzu.yuzu_emu.model.AppletInfo
|
||||||
import org.yuzu.yuzu_emu.model.Game
|
import org.yuzu.yuzu_emu.model.Game
|
||||||
import org.yuzu.yuzu_emu.model.GamesViewModel
|
import org.yuzu.yuzu_emu.model.GamesViewModel
|
||||||
import org.yuzu.yuzu_emu.model.HomeViewModel
|
import org.yuzu.yuzu_emu.model.HomeViewModel
|
||||||
@@ -173,10 +178,16 @@ class GamesFragment : Fragment() {
|
|||||||
|
|
||||||
setupTopView()
|
setupTopView()
|
||||||
|
|
||||||
|
updateButtonsVisibility()
|
||||||
|
|
||||||
binding.addDirectory.setOnClickListener {
|
binding.addDirectory.setOnClickListener {
|
||||||
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.launchQlaunch?.setOnClickListener {
|
||||||
|
launchQLaunch()
|
||||||
|
}
|
||||||
|
|
||||||
setInsets()
|
setInsets()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,6 +456,47 @@ class GamesFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchQLaunch() {
|
||||||
|
try {
|
||||||
|
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.QLaunch.entryId)
|
||||||
|
if (appletPath.isEmpty()) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.applets_error_applet,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeLibrary.setCurrentAppletId(AppletInfo.QLaunch.appletId)
|
||||||
|
|
||||||
|
val qlaunchGame = Game(
|
||||||
|
title = getString(R.string.qlaunch_applet),
|
||||||
|
path = appletPath
|
||||||
|
)
|
||||||
|
|
||||||
|
val action = HomeNavigationDirections.actionGlobalEmulationActivity(qlaunchGame)
|
||||||
|
findNavController().navigate(action)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
"Failed to launch QLaunch: ${e.message}",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateButtonsVisibility() {
|
||||||
|
val showQLaunch = BooleanSetting.ENABLE_QLAUNCH_BUTTON.getBoolean()
|
||||||
|
val showFolder = BooleanSetting.ENABLE_FOLDER_BUTTON.getBoolean()
|
||||||
|
val isFirmwareAvailable = NativeLibrary.isFirmwareAvailable()
|
||||||
|
|
||||||
|
val shouldShowQLaunch = showQLaunch && isFirmwareAvailable
|
||||||
|
binding.launchQlaunch.visibility = if (shouldShowQLaunch) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
binding.addDirectory.visibility = if (showFolder) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
private fun setInsets() =
|
private fun setInsets() =
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
binding.root
|
binding.root
|
||||||
@@ -498,6 +550,13 @@ class GamesFragment : Fragment() {
|
|||||||
mlpFab.rightMargin = rightInset + fabPadding
|
mlpFab.rightMargin = rightInset + fabPadding
|
||||||
binding.addDirectory.layoutParams = mlpFab
|
binding.addDirectory.layoutParams = mlpFab
|
||||||
|
|
||||||
|
binding.launchQlaunch?.let { qlaunchButton ->
|
||||||
|
val mlpQLaunch = qlaunchButton.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
|
mlpQLaunch.leftMargin = leftInset + fabPadding
|
||||||
|
mlpQLaunch.bottomMargin = barInsets.bottom + fabPadding
|
||||||
|
qlaunchButton.layoutParams = mlpQLaunch
|
||||||
|
}
|
||||||
|
|
||||||
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||||
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
|
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
|
||||||
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
|
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ThemeProvider {
|
class MainActivity : AppCompatActivity(), ThemeProvider {
|
||||||
private lateinit var binding: ActivityMainBinding
|
private lateinit var binding: ActivityMainBinding
|
||||||
@@ -199,8 +200,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
var artifact = tag
|
var artifact = tag
|
||||||
// Nightly builds have a slightly different format
|
// Nightly builds have a slightly different format
|
||||||
if (NativeLibrary.isNightlyBuild()) {
|
if (NativeLibrary.isNightlyBuild()) {
|
||||||
val splitTag = tag.split('.')
|
artifact = tag.substringAfter('.', tag)
|
||||||
artifact = splitTag.subList(1, splitTag.size - 1).joinToString(".")
|
|
||||||
}
|
}
|
||||||
downloadAndInstallUpdate(tag, artifact)
|
downloadAndInstallUpdate(tag, artifact)
|
||||||
}
|
}
|
||||||
@@ -389,6 +389,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val getExternalContentDirectory =
|
||||||
|
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
|
||||||
|
if (result != null) {
|
||||||
|
processExternalContentDir(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
|
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
|
||||||
contentResolver.takePersistableUriPermission(
|
contentResolver.takePersistableUriPermission(
|
||||||
result,
|
result,
|
||||||
@@ -410,6 +417,27 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
|
|||||||
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
.show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun processExternalContentDir(result: Uri) {
|
||||||
|
contentResolver.takePersistableUriPermission(
|
||||||
|
result,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
|
val uriString = result.toString()
|
||||||
|
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
|
||||||
|
if (folder != null) {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.folder_already_added,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val externalContentDir = org.yuzu.yuzu_emu.model.GameDir(uriString, false, org.yuzu.yuzu_emu.model.DirectoryType.EXTERNAL_CONTENT)
|
||||||
|
gamesViewModel.addFolder(externalContentDir, savedFromGameFragment = false)
|
||||||
|
}
|
||||||
|
|
||||||
val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
val getProdKey = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
processKey(result, "keys")
|
processKey(result, "keys")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
@@ -25,6 +25,7 @@ object DirectoryInitialization {
|
|||||||
initializeInternalStorage()
|
initializeInternalStorage()
|
||||||
NativeLibrary.initializeSystem(false)
|
NativeLibrary.initializeSystem(false)
|
||||||
NativeConfig.initializeGlobalConfig()
|
NativeConfig.initializeGlobalConfig()
|
||||||
|
NativeLibrary.reloadProfiles()
|
||||||
migrateSettings()
|
migrateSettings()
|
||||||
areDirectoriesReady = true
|
areDirectoriesReady = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -49,6 +49,17 @@ object GameHelper {
|
|||||||
// Remove previous filesystem provider information so we can get up to date version info
|
// Remove previous filesystem provider information so we can get up to date version info
|
||||||
NativeLibrary.clearFilesystemProvider()
|
NativeLibrary.clearFilesystemProvider()
|
||||||
|
|
||||||
|
// Scan External Content directories and register all NSP/XCI files
|
||||||
|
val externalContentDirs = NativeConfig.getExternalContentDirs()
|
||||||
|
for (externalDir in externalContentDirs) {
|
||||||
|
if (externalDir.isNotEmpty()) {
|
||||||
|
val externalDirUri = externalDir.toUri()
|
||||||
|
if (FileUtil.isTreeUriValid(externalDirUri)) {
|
||||||
|
scanExternalContentRecursive(FileUtil.listFiles(externalDirUri), 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val badDirs = mutableListOf<Int>()
|
val badDirs = mutableListOf<Int>()
|
||||||
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
|
||||||
val gameDirUri = gameDir.uriString.toUri()
|
val gameDirUri = gameDir.uriString.toUri()
|
||||||
@@ -88,6 +99,33 @@ object GameHelper {
|
|||||||
return games.toList()
|
return games.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File extensions considered as external content, buuut should
|
||||||
|
// be done better imo.
|
||||||
|
private val externalContentExtensions = setOf("nsp", "xci")
|
||||||
|
|
||||||
|
private fun scanExternalContentRecursive(
|
||||||
|
files: Array<MinimalDocumentFile>,
|
||||||
|
depth: Int
|
||||||
|
) {
|
||||||
|
if (depth <= 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach {
|
||||||
|
if (it.isDirectory) {
|
||||||
|
scanExternalContentRecursive(
|
||||||
|
FileUtil.listFiles(it.uri),
|
||||||
|
depth - 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val extension = FileUtil.getExtension(it.uri).lowercase()
|
||||||
|
if (externalContentExtensions.contains(extension)) {
|
||||||
|
NativeLibrary.addFileToFilesystemProvider(it.uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun addGamesRecursive(
|
private fun addGamesRecursive(
|
||||||
games: MutableList<Game>,
|
games: MutableList<Game>,
|
||||||
files: Array<MinimalDocumentFile>,
|
files: Array<MinimalDocumentFile>,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -204,4 +204,12 @@ object NativeConfig {
|
|||||||
external fun getSdmcDir(): String
|
external fun getSdmcDir(): String
|
||||||
@Synchronized
|
@Synchronized
|
||||||
external fun setSdmcDir(path: String)
|
external fun setSdmcDir(path: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External Content Provider
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
external fun getExternalContentDirs(): Array<String>
|
||||||
|
@Synchronized
|
||||||
|
external fun setExternalContentDirs(dirs: Array<String>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.utils
|
package org.yuzu.yuzu_emu.utils
|
||||||
@@ -144,6 +144,15 @@ object FreedrenoPresets {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val DEV_FEATURES_UBWC_HINT = FreedrenoPreset(
|
||||||
|
name = "Dev - UBWC Flag Hint",
|
||||||
|
description = "Enable TP UBWC flag hint for development",
|
||||||
|
icon = "ic_dev_features",
|
||||||
|
variables = mapOf(
|
||||||
|
"FD_DEV_FEATURES" to "enable_tp_ubwc_flag_hint=1"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val PERFORMANCE_DEFAULT = FreedrenoPreset(
|
val PERFORMANCE_DEFAULT = FreedrenoPreset(
|
||||||
name = "Performance - Default",
|
name = "Performance - Default",
|
||||||
description = "Clear all debug options for performance",
|
description = "Clear all debug options for performance",
|
||||||
@@ -159,6 +168,7 @@ object FreedrenoPresets {
|
|||||||
CAPTURE_FRAMES,
|
CAPTURE_FRAMES,
|
||||||
SHADER_DEBUG,
|
SHADER_DEBUG,
|
||||||
GPU_HANG_TRACE,
|
GPU_HANG_TRACE,
|
||||||
|
DEV_FEATURES_UBWC_HINT,
|
||||||
PERFORMANCE_DEFAULT
|
PERFORMANCE_DEFAULT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <common/fs/path_util.h>
|
#include <common/fs/path_util.h>
|
||||||
#include <common/logging/log.h>
|
#include <common/logging/log.h>
|
||||||
|
#include <common/settings.h>
|
||||||
#include <input_common/main.h>
|
#include <input_common/main.h>
|
||||||
#include "android_config.h"
|
#include "android_config.h"
|
||||||
#include "android_settings.h"
|
#include "android_settings.h"
|
||||||
@@ -69,6 +70,18 @@ void AndroidConfig::ReadPathValues() {
|
|||||||
}
|
}
|
||||||
EndArray();
|
EndArray();
|
||||||
|
|
||||||
|
// Read external content directories
|
||||||
|
Settings::values.external_content_dirs.clear();
|
||||||
|
const int external_dirs_size = BeginArray(std::string("external_content_dirs"));
|
||||||
|
for (int i = 0; i < external_dirs_size; ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
std::string dir_path = ReadStringSetting(std::string("path"));
|
||||||
|
if (!dir_path.empty()) {
|
||||||
|
Settings::values.external_content_dirs.push_back(dir_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
const auto nand_dir_setting = ReadStringSetting(std::string("nand_directory"));
|
const auto nand_dir_setting = ReadStringSetting(std::string("nand_directory"));
|
||||||
if (!nand_dir_setting.empty()) {
|
if (!nand_dir_setting.empty()) {
|
||||||
Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting);
|
Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting);
|
||||||
@@ -241,6 +254,14 @@ void AndroidConfig::SavePathValues() {
|
|||||||
}
|
}
|
||||||
EndArray();
|
EndArray();
|
||||||
|
|
||||||
|
// Save external content directories
|
||||||
|
BeginArray(std::string("external_content_dirs"));
|
||||||
|
for (size_t i = 0; i < Settings::values.external_content_dirs.size(); ++i) {
|
||||||
|
SetArrayIndex(i);
|
||||||
|
WriteStringSetting(std::string("path"), Settings::values.external_content_dirs[i]);
|
||||||
|
}
|
||||||
|
EndArray();
|
||||||
|
|
||||||
// Save custom NAND directory
|
// Save custom NAND directory
|
||||||
const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir);
|
const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir);
|
||||||
WriteStringSetting(std::string("nand_directory"), nand_path,
|
WriteStringSetting(std::string("nand_directory"), nand_path,
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ namespace AndroidSettings {
|
|||||||
Settings::Setting<s32> app_language{linkage, 0, "app_language", Settings::Category::Android};
|
Settings::Setting<s32> app_language{linkage, 0, "app_language", Settings::Category::Android};
|
||||||
Settings::Setting<bool> enable_update_checks{linkage, true, "enable_update_checks",
|
Settings::Setting<bool> enable_update_checks{linkage, true, "enable_update_checks",
|
||||||
Settings::Category::Android};
|
Settings::Category::Android};
|
||||||
|
Settings::Setting<bool> enable_folder_button{linkage, true, "enable_folder_button",
|
||||||
|
Settings::Category::Android};
|
||||||
|
Settings::Setting<bool> enable_qlaunch_button{linkage, false, "enable_qlaunch_button",
|
||||||
|
Settings::Category::Android};
|
||||||
|
|
||||||
// Input/performance overlay settings
|
// Input/performance overlay settings
|
||||||
std::vector<OverlayControlData> overlay_control_data;
|
std::vector<OverlayControlData> overlay_control_data;
|
||||||
@@ -198,9 +202,14 @@ namespace AndroidSettings {
|
|||||||
Settings::Specialization::Default, true, true,
|
Settings::Specialization::Default, true, true,
|
||||||
&show_soc_overlay};
|
&show_soc_overlay};
|
||||||
|
|
||||||
|
// MISC
|
||||||
Settings::Setting<bool> dont_show_driver_shader_warning{linkage, false,
|
Settings::Setting<bool> dont_show_driver_shader_warning{linkage, false,
|
||||||
"dont_show_driver_shader_warning",
|
"dont_show_driver_shader_warning",
|
||||||
Settings::Category::Android, Settings::Specialization::Default, true, true};
|
Settings::Category::Android, Settings::Specialization::Default, true, true};
|
||||||
|
Settings::Setting<bool> enable_quick_settings{linkage, true,
|
||||||
|
"enable_quick_settings",
|
||||||
|
Settings::Category::Android, Settings::Specialization::Default, true,
|
||||||
|
false};
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Values values;
|
extern Values values;
|
||||||
|
|||||||
@@ -49,12 +49,17 @@
|
|||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "frontend_common/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
|
#include "core/constants.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/cpu_manager.h"
|
#include "core/cpu_manager.h"
|
||||||
#include "core/crypto/key_manager.h"
|
#include "core/crypto/key_manager.h"
|
||||||
#include "core/file_sys/card_image.h"
|
#include "core/file_sys/card_image.h"
|
||||||
#include "core/file_sys/content_archive.h"
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/control_metadata.h"
|
||||||
#include "core/file_sys/fs_filesystem.h"
|
#include "core/file_sys/fs_filesystem.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
|
#include "core/file_sys/nca_metadata.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
#include "core/file_sys/submission_package.h"
|
#include "core/file_sys/submission_package.h"
|
||||||
#include "core/file_sys/vfs/vfs.h"
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
#include "core/file_sys/vfs/vfs_real.h"
|
#include "core/file_sys/vfs/vfs_real.h"
|
||||||
@@ -210,6 +215,109 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1));
|
||||||
|
|
||||||
|
if (extension == "nsp") {
|
||||||
|
auto nsp = std::make_shared<FileSys::NSP>(file);
|
||||||
|
if (nsp->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
std::map<u64, u32> nsp_versions;
|
||||||
|
std::map<u64, std::string> nsp_version_strings;
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : nsp->GetNCAs()) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (content_type == FileSys::ContentRecordType::Meta) {
|
||||||
|
const auto meta_nca = std::make_shared<FileSys::NCA>(nca->GetBaseFile());
|
||||||
|
if (meta_nca->GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
const auto section0 = meta_nca->GetSubdirectories();
|
||||||
|
if (!section0.empty()) {
|
||||||
|
for (const auto& meta_file : section0[0]->GetFiles()) {
|
||||||
|
if (meta_file->GetExtension() == "cnmt") {
|
||||||
|
FileSys::CNMT cnmt(meta_file);
|
||||||
|
nsp_versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content_type == FileSys::ContentRecordType::Control &&
|
||||||
|
title_type == FileSys::TitleType::Update) {
|
||||||
|
auto romfs = nca->GetRomFS();
|
||||||
|
if (romfs) {
|
||||||
|
auto extracted = FileSys::ExtractRomFS(romfs);
|
||||||
|
if (extracted) {
|
||||||
|
auto nacp_file = extracted->GetFile("control.nacp");
|
||||||
|
if (!nacp_file) {
|
||||||
|
nacp_file = extracted->GetFile("Control.nacp");
|
||||||
|
}
|
||||||
|
if (nacp_file) {
|
||||||
|
FileSys::NACP nacp(nacp_file);
|
||||||
|
auto ver_str = nacp.GetVersionString();
|
||||||
|
if (!ver_str.empty()) {
|
||||||
|
nsp_version_strings[title_id] = ver_str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : nsp->GetNCAs()) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (title_type == FileSys::TitleType::Update) {
|
||||||
|
u32 version = 0;
|
||||||
|
auto ver_it = nsp_versions.find(title_id);
|
||||||
|
if (ver_it != nsp_versions.end()) {
|
||||||
|
version = ver_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string version_string;
|
||||||
|
auto str_it = nsp_version_strings.find(title_id);
|
||||||
|
if (str_it != nsp_version_strings.end()) {
|
||||||
|
version_string = str_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_manual_provider->AddEntryWithVersion(
|
||||||
|
title_type, content_type, title_id, version, version_string,
|
||||||
|
nca->GetBaseFile());
|
||||||
|
|
||||||
|
LOG_DEBUG(Frontend, "Added NSP update entry - TitleID: {:016X}, Version: {}, VersionStr: {}",
|
||||||
|
title_id, version, version_string);
|
||||||
|
} else {
|
||||||
|
// Use regular AddEntry for non-updates
|
||||||
|
m_manual_provider->AddEntry(title_type, content_type, title_id,
|
||||||
|
nca->GetBaseFile());
|
||||||
|
LOG_DEBUG(Frontend, "Added NSP entry - TitleID: {:016X}, TitleType: {}, ContentType: {}",
|
||||||
|
title_id, static_cast<int>(title_type), static_cast<int>(content_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle XCI files
|
||||||
|
if (extension == "xci") {
|
||||||
|
FileSys::XCI xci{file};
|
||||||
|
if (xci.GetStatus() == Loader::ResultStatus::Success) {
|
||||||
|
const auto nsp = xci.GetSecurePartitionNSP();
|
||||||
|
if (nsp) {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto loader = Loader::GetLoader(m_system, file);
|
auto loader = Loader::GetLoader(m_system, file);
|
||||||
if (!loader) {
|
if (!loader) {
|
||||||
return;
|
return;
|
||||||
@@ -226,17 +334,6 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
|
|||||||
m_manual_provider->AddEntry(FileSys::TitleType::Application,
|
m_manual_provider->AddEntry(FileSys::TitleType::Application,
|
||||||
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
|
||||||
program_id, file);
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1331,7 +1428,8 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
|
|||||||
Common::Android::ToJString(env, patch.name),
|
Common::Android::ToJString(env, patch.name),
|
||||||
Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
|
Common::Android::ToJString(env, patch.version), static_cast<jint>(patch.type),
|
||||||
Common::Android::ToJString(env, std::to_string(patch.program_id)),
|
Common::Android::ToJString(env, std::to_string(patch.program_id)),
|
||||||
Common::Android::ToJString(env, std::to_string(patch.title_id)));
|
Common::Android::ToJString(env, std::to_string(patch.title_id)),
|
||||||
|
static_cast<jlong>(patch.numeric_version));
|
||||||
env->SetObjectArrayElement(jpatchArray, i, jpatch);
|
env->SetObjectArrayElement(jpatchArray, i, jpatch);
|
||||||
++i;
|
++i;
|
||||||
}
|
}
|
||||||
@@ -1678,7 +1776,7 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", version_str, variant);
|
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
|
||||||
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
|
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
|
||||||
std::string{Common::g_build_auto_update_website},
|
std::string{Common::g_build_auto_update_website},
|
||||||
std::string{Common::g_build_auto_update_repo},
|
std::string{Common::g_build_auto_update_repo},
|
||||||
@@ -1686,6 +1784,7 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
|
|||||||
apk_filename);
|
apk_filename);
|
||||||
|
|
||||||
env->ReleaseStringUTFChars(tag, version_str);
|
env->ReleaseStringUTFChars(tag, version_str);
|
||||||
|
env->ReleaseStringUTFChars(artifact, artifact_str);
|
||||||
env->ReleaseStringUTFChars(packageId, package_id_str);
|
env->ReleaseStringUTFChars(packageId, package_id_str);
|
||||||
return env->NewStringUTF(url.c_str());
|
return env->NewStringUTF(url.c_str());
|
||||||
}
|
}
|
||||||
@@ -1697,4 +1796,388 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
|
|||||||
return env->NewStringUTF(Common::g_build_version);
|
return env->NewStringUTF(Common::g_build_version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jobjectArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getAllUsers(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
|
||||||
|
manager.ResetUserSaveFile();
|
||||||
|
|
||||||
|
if (manager.GetUserCount() == 0) {
|
||||||
|
manager.CreateNewUser(Common::UUID::MakeRandom(), "Eden");
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& users = manager.GetAllUsers();
|
||||||
|
|
||||||
|
jclass string_class = env->FindClass("java/lang/String");
|
||||||
|
if (!string_class) {
|
||||||
|
return env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
jsize valid_count = 0;
|
||||||
|
for (const auto& user : users) {
|
||||||
|
if (user.IsValid()) {
|
||||||
|
valid_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jobjectArray result = env->NewObjectArray(valid_count, string_class, nullptr);
|
||||||
|
if (!result) {
|
||||||
|
return env->NewObjectArray(0, string_class, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill array sequentially with only valid users
|
||||||
|
jsize array_index = 0;
|
||||||
|
for (const auto& user : users) {
|
||||||
|
if (user.IsValid()) {
|
||||||
|
jstring uuid_str = env->NewStringUTF(user.FormattedString().c_str());
|
||||||
|
if (uuid_str) {
|
||||||
|
env->SetObjectArrayElement(result, array_index++, uuid_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserUsername(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
Service::Account::ProfileBase profile{};
|
||||||
|
if (!manager.GetProfileBase(uuid, profile)) {
|
||||||
|
jstring result = env->NewStringUTF("");
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto text = Common::StringFromFixedZeroTerminatedBuffer(
|
||||||
|
reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
|
||||||
|
jstring result = env->NewStringUTF(text.c_str());
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jlong JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserCount(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
return static_cast<jlong>(manager.GetUserCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_canCreateUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
return manager.CanSystemRegisterUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_createUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid,
|
||||||
|
jstring jusername) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto username = Common::Android::GetJString(env, jusername);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto result = manager.CreateNewUser(uuid, username);
|
||||||
|
if (result.IsSuccess()) {
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_updateUserUsername(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid,
|
||||||
|
jstring jusername) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto username = Common::Android::GetJString(env, jusername);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
Service::Account::ProfileBase profile{};
|
||||||
|
if (!manager.GetProfileBase(uuid, profile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fill(profile.username.begin(), profile.username.end(), '\0');
|
||||||
|
std::copy(username.begin(), username.end(), profile.username.begin());
|
||||||
|
|
||||||
|
if (manager.SetProfileBase(uuid, profile)) {
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto user_index = manager.GetUserIndex(uuid);
|
||||||
|
if (!user_index) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings::values.current_user.GetValue() == static_cast<s32>(*user_index)) {
|
||||||
|
Settings::values.current_user = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager.RemoveUser(uuid)) {
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
manager.ResetUserSaveFile();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getCurrentUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto user_id = manager.GetUser(Settings::values.current_user.GetValue());
|
||||||
|
if (!user_id) {
|
||||||
|
jstring result = env->NewStringUTF("");
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
jstring result = env->NewStringUTF(user_id->FormattedString().c_str());
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_setCurrentUser(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto index = manager.GetUserIndex(uuid);
|
||||||
|
if (index) {
|
||||||
|
Settings::values.current_user = static_cast<s32>(*index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUserImagePath(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid) {
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
|
||||||
|
const auto path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
|
||||||
|
fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());
|
||||||
|
|
||||||
|
jstring result = Common::Android::ToJString(env, Common::FS::PathToUTF8String(path));
|
||||||
|
return result ? result : env->NewStringUTF("");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_saveUserImage(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jstring juuid,
|
||||||
|
jstring jimagePath) {
|
||||||
|
const auto uuid_string = Common::Android::GetJString(env, juuid);
|
||||||
|
const auto uuid = Common::UUID{uuid_string};
|
||||||
|
const auto image_source = Common::Android::GetJString(env, jimagePath);
|
||||||
|
|
||||||
|
const auto dest_path = Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
|
||||||
|
fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());
|
||||||
|
|
||||||
|
const auto dest_dir = dest_path.parent_path();
|
||||||
|
if (!Common::FS::CreateDirs(dest_dir)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::filesystem::copy_file(image_source, dest_path,
|
||||||
|
std::filesystem::copy_options::overwrite_existing);
|
||||||
|
return true;
|
||||||
|
} catch (const std::filesystem::filesystem_error& e) {
|
||||||
|
LOG_ERROR(Common_Filesystem, "Failed to copy image file: {}", e.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_reloadProfiles(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
auto& manager = EmulationSession::GetInstance().System().GetProfileManager();
|
||||||
|
manager.ResetUserSaveFile();
|
||||||
|
|
||||||
|
// create a default user if non exist
|
||||||
|
if (manager.GetUserCount() == 0) {
|
||||||
|
manager.CreateNewUser(Common::UUID::MakeRandom(), "Eden");
|
||||||
|
manager.WriteUserSaveFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Service_ACC, "Profile manager reloaded, user count: {}", manager.GetUserCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
// for firmware avatar images
|
||||||
|
static std::vector<uint8_t> DecompressYaz0(const FileSys::VirtualFile& file) {
|
||||||
|
if (!file) {
|
||||||
|
return std::vector<uint8_t>();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t magic{};
|
||||||
|
file->ReadObject(&magic, 0);
|
||||||
|
if (magic != Common::MakeMagic('Y', 'a', 'z', '0')) {
|
||||||
|
return std::vector<uint8_t>();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t decoded_length{};
|
||||||
|
file->ReadObject(&decoded_length, 4);
|
||||||
|
decoded_length = Common::swap32(decoded_length);
|
||||||
|
|
||||||
|
std::size_t input_size = file->GetSize() - 16;
|
||||||
|
std::vector<uint8_t> input(input_size);
|
||||||
|
file->ReadBytes(input.data(), input_size, 16);
|
||||||
|
|
||||||
|
uint32_t input_offset{};
|
||||||
|
uint32_t output_offset{};
|
||||||
|
std::vector<uint8_t> output(decoded_length);
|
||||||
|
|
||||||
|
uint16_t mask{};
|
||||||
|
uint8_t header{};
|
||||||
|
|
||||||
|
while (output_offset < decoded_length) {
|
||||||
|
if ((mask >>= 1) == 0) {
|
||||||
|
if (input_offset >= input.size()) break;
|
||||||
|
header = input[input_offset++];
|
||||||
|
mask = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((header & mask) != 0) {
|
||||||
|
if (output_offset >= output.size() || input_offset >= input.size()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
output[output_offset++] = input[input_offset++];
|
||||||
|
} else {
|
||||||
|
if (input_offset + 1 >= input.size()) break;
|
||||||
|
uint8_t byte1 = input[input_offset++];
|
||||||
|
uint8_t byte2 = input[input_offset++];
|
||||||
|
|
||||||
|
uint32_t dist = ((byte1 & 0xF) << 8) | byte2;
|
||||||
|
uint32_t position = output_offset - (dist + 1);
|
||||||
|
|
||||||
|
uint32_t length = byte1 >> 4;
|
||||||
|
if (length == 0) {
|
||||||
|
if (input_offset >= input.size()) break;
|
||||||
|
length = static_cast<uint32_t>(input[input_offset++]) + 0x12;
|
||||||
|
} else {
|
||||||
|
length += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < length && output_offset < decoded_length; ++i) {
|
||||||
|
output[output_offset++] = output[position++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FileSys::VirtualDir GetFirmwareAvatarDirectory() {
|
||||||
|
constexpr u64 AvatarImageDataId = 0x010000000000080AULL;
|
||||||
|
|
||||||
|
auto* bis_system = EmulationSession::GetInstance().System().GetFileSystemController().GetSystemNANDContents();
|
||||||
|
if (!bis_system) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto nca = bis_system->GetEntry(AvatarImageDataId, FileSys::ContentRecordType::Data);
|
||||||
|
if (!nca) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto romfs = nca->GetRomFS();
|
||||||
|
if (!romfs) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto extracted = FileSys::ExtractRomFS(romfs);
|
||||||
|
if (!extracted) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extracted->GetSubdirectory("chara");
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getFirmwareAvatarCount(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
const auto chara_dir = GetFirmwareAvatarDirectory();
|
||||||
|
if (!chara_dir) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& item : chara_dir->GetFiles()) {
|
||||||
|
if (item->GetExtension() == "szs") {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getFirmwareAvatarImage(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj,
|
||||||
|
jint index) {
|
||||||
|
const auto chara_dir = GetFirmwareAvatarDirectory();
|
||||||
|
if (!chara_dir) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current_index = 0;
|
||||||
|
for (const auto& item : chara_dir->GetFiles()) {
|
||||||
|
if (item->GetExtension() != "szs") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_index == index) {
|
||||||
|
auto image_data = DecompressYaz0(item);
|
||||||
|
if (image_data.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
jbyteArray result = env->NewByteArray(image_data.size());
|
||||||
|
if (result) {
|
||||||
|
env->SetByteArrayRegion(result, 0, image_data.size(),
|
||||||
|
reinterpret_cast<const jbyte*>(image_data.data()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
current_index++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jbyteArray JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getDefaultAccountBackupJpeg(
|
||||||
|
JNIEnv* env,
|
||||||
|
[[maybe_unused]] jobject obj) {
|
||||||
|
jbyteArray result = env->NewByteArray(Core::Constants::ACCOUNT_BACKUP_JPEG.size());
|
||||||
|
if (result) {
|
||||||
|
env->SetByteArrayRegion(result, 0, Core::Constants::ACCOUNT_BACKUP_JPEG.size(),
|
||||||
|
reinterpret_cast<const jbyte*>(Core::Constants::ACCOUNT_BACKUP_JPEG.data()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <common/fs/path_util.h>
|
|
||||||
|
|
||||||
#include "android_config.h"
|
#include "android_config.h"
|
||||||
#include "android_settings.h"
|
#include "android_settings.h"
|
||||||
#include "common/android/android_common.h"
|
#include "common/android/android_common.h"
|
||||||
#include "common/android/id_cache.h"
|
#include "common/android/id_cache.h"
|
||||||
|
#include "common/fs/path_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "frontend_common/config.h"
|
#include "frontend_common/config.h"
|
||||||
|
#include "frontend_common/settings_generator.h"
|
||||||
#include "native.h"
|
#include "native.h"
|
||||||
|
|
||||||
std::unique_ptr<AndroidConfig> global_config;
|
std::unique_ptr<AndroidConfig> global_config;
|
||||||
@@ -37,6 +38,7 @@ extern "C" {
|
|||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
|
||||||
global_config = std::make_unique<AndroidConfig>();
|
global_config = std::make_unique<AndroidConfig>();
|
||||||
|
FrontendCommon::GenerateSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
|
||||||
@@ -581,4 +583,26 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setSdmcDir(JNIEnv* env, jobject
|
|||||||
Common::FS::SetEdenPath(Common::FS::EdenPath::SDMCDir, path);
|
Common::FS::SetEdenPath(Common::FS::EdenPath::SDMCDir, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getExternalContentDirs(JNIEnv* env,
|
||||||
|
jobject obj) {
|
||||||
|
const auto& dirs = Settings::values.external_content_dirs;
|
||||||
|
jobjectArray jdirsArray =
|
||||||
|
env->NewObjectArray(dirs.size(), Common::Android::GetStringClass(),
|
||||||
|
Common::Android::ToJString(env, ""));
|
||||||
|
for (size_t i = 0; i < dirs.size(); ++i) {
|
||||||
|
env->SetObjectArrayElement(jdirsArray, i, Common::Android::ToJString(env, dirs[i]));
|
||||||
|
}
|
||||||
|
return jdirsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setExternalContentDirs(JNIEnv* env, jobject obj,
|
||||||
|
jobjectArray jdirs) {
|
||||||
|
Settings::values.external_content_dirs.clear();
|
||||||
|
const int size = env->GetArrayLength(jdirs);
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
auto jdir = static_cast<jstring>(env->GetObjectArrayElement(jdirs, i));
|
||||||
|
Settings::values.external_content_dirs.push_back(Common::Android::GetJString(env, jdir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -258,6 +258,11 @@ Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
|
LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
|
||||||
|
|
||||||
|
if (var_name == "FD_DEV_FEATURES") {
|
||||||
|
LOG_INFO(Frontend, "[Freedreno] FD_DEV_FEATURES enabled: {}", value);
|
||||||
|
}
|
||||||
|
|
||||||
return JNI_TRUE;
|
return JNI_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3s-3,-1.34 -3,-3S10.34,5 12,5zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22c0.03,-1.99 4,-3.08 6,-3.08c1.99,0 5.97,1.09 6,3.08C16.71,17.92 14.5,19.2 12,19.2z" />
|
||||||
|
</vector>
|
||||||
@@ -220,6 +220,23 @@
|
|||||||
android:textColor="?attr/colorOnPrimary"
|
android:textColor="?attr/colorOnPrimary"
|
||||||
app:backgroundTint="?attr/colorPrimary"
|
app:backgroundTint="?attr/colorPrimary"
|
||||||
app:iconTint="?attr/colorOnPrimary"
|
app:iconTint="?attr/colorOnPrimary"
|
||||||
|
app:rippleColor="#99FFFFFF"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/launch_qlaunch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="Launch QLaunch"
|
||||||
|
android:text="@string/qlaunch_applet"
|
||||||
|
app:icon="@drawable/ic_home"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:textColor="?attr/colorOnPrimary"
|
||||||
|
app:backgroundTint="?attr/colorPrimary"
|
||||||
|
app:iconTint="?attr/colorOnPrimary"
|
||||||
|
app:rippleColor="#99FFFFFF"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="vertical"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:layout_gravity="center_vertical">
|
android:layout_gravity="center_vertical">
|
||||||
|
|
||||||
@@ -23,12 +23,25 @@
|
|||||||
android:layout_gravity="center_vertical|start"
|
android:layout_gravity="center_vertical|start"
|
||||||
android:requiresFadingEdge="horizontal"
|
android:requiresFadingEdge="horizontal"
|
||||||
android:textAlignment="viewStart"
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toTopOf="@+id/type_indicator"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="@string/select_gpu_driver_default" />
|
tools:text="@string/select_gpu_driver_default" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/type_indicator"
|
||||||
|
style="@style/TextAppearance.Material3.LabelSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/button_layout"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/path"
|
||||||
|
tools:text="Games" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/button_layout"
|
android:id="@+id/button_layout"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="300dp">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/grid_avatars"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_empty"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="@string/profile_firmware_avatars_unavailable"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_new_user"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:title="@string/profile_new_user"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:titleCentered="false" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:id="@+id/scroll_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="88dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:paddingVertical="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="128dp"
|
||||||
|
android:layout_height="128dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="64dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_user_avatar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/profile_avatar"
|
||||||
|
tools:src="@drawable/ic_account_circle" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_select_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/profile_select_image"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:icon="@drawable/ic_add"
|
||||||
|
android:layout_marginEnd="4dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_firmware_avatars"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/profile_firmware_avatars"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton"
|
||||||
|
app:icon="@drawable/ic_account_circle"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_revert_image"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/profile_revert_image"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:hint="@string/profile_username">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/edit_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLength="32"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:minHeight="48dp" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
app:cardCornerRadius="16dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/profile_uuid"
|
||||||
|
android:textAppearance="?attr/textAppearanceLabelMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_uuid"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:textIsSelectable="true"
|
||||||
|
tools:text="12345678-1234-1234-1234-123456789012" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_generate_uuid"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/profile_generate"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
app:icon="@drawable/ic_refresh"
|
||||||
|
app:iconGravity="textStart" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/profile_uuid_description"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/button_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="0dp"
|
||||||
|
app:cardElevation="8dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:id="@+id/button_container"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:paddingBottom="24dp">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_cancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@android:string/cancel"
|
||||||
|
style="@style/Widget.Material3.Button.TonalButton" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_save"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/save"
|
||||||
|
style="@style/Widget.Material3.Button" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -214,6 +214,22 @@
|
|||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/launch_qlaunch"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:contentDescription="Launch QLaunch"
|
||||||
|
android:text="@string/qlaunch_applet"
|
||||||
|
app:icon="@drawable/ic_home"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:textColor="?attr/colorOnPrimary"
|
||||||
|
app:backgroundTint="?attr/colorPrimary"
|
||||||
|
app:iconTint="?attr/colorOnPrimary"
|
||||||
|
app:rippleColor="#99FFFFFF"
|
||||||
|
/>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
android:id="@+id/add_directory"
|
android:id="@+id/add_directory"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -227,6 +243,7 @@
|
|||||||
android:textColor="?attr/colorOnPrimary"
|
android:textColor="?attr/colorOnPrimary"
|
||||||
app:backgroundTint="?attr/colorPrimary"
|
app:backgroundTint="?attr/colorPrimary"
|
||||||
app:iconTint="?attr/colorOnPrimary"
|
app:iconTint="?attr/colorOnPrimary"
|
||||||
|
app:rippleColor="#99FFFFFF"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorSurface"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar_profiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
app:title="@string/profile_manager"
|
||||||
|
app:navigationIcon="@drawable/ic_back"
|
||||||
|
app:titleCentered="false" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_profiles"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="96dp"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
tools:listitem="@layout/list_item_profile" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
android:id="@+id/button_add_user"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="24dp"
|
||||||
|
android:text="@string/profile_add_user"
|
||||||
|
android:contentDescription="@string/profile_add_user"
|
||||||
|
app:icon="@drawable/ic_add"
|
||||||
|
app:iconGravity="start"
|
||||||
|
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="4dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:checkable="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_avatar"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="72dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:contentDescription="@string/profile_avatar" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginVertical="6dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
app:cardCornerRadius="16dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/avatar_container"
|
||||||
|
android:layout_width="56dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
style="@style/Widget.Material3.CardView.Elevated"
|
||||||
|
app:cardCornerRadius="28dp"
|
||||||
|
app:cardElevation="1dp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image_avatar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@drawable/ic_account_circle"
|
||||||
|
android:contentDescription="@string/profile_avatar" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:id="@+id/check_container"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
style="@style/Widget.Material3.CardView.Filled"
|
||||||
|
app:cardBackgroundColor="?attr/colorPrimary"
|
||||||
|
app:cardCornerRadius="10dp"
|
||||||
|
app:cardElevation="2dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/avatar_container"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/avatar_container"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon_check"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/ic_check"
|
||||||
|
app:tint="?attr/colorOnPrimary"
|
||||||
|
android:contentDescription="@string/profile_current_user" />
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/text_container"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatar_container"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_edit"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/avatar_container"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/avatar_container">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?attr/textAppearanceTitleMedium"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="User Name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_uuid"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBodySmall"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
tools:text="12345678-1234-1234-1234-123456789012" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_edit"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
app:icon="@drawable/ic_edit"
|
||||||
|
app:iconTint="?attr/colorOnSurfaceVariant"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/button_delete"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:contentDescription="@string/profile_edit" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_delete"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
style="@style/Widget.Material3.Button.IconButton"
|
||||||
|
app:icon="@drawable/ic_delete"
|
||||||
|
app:iconTint="?attr/colorError"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
android:contentDescription="@string/profile_delete" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -36,6 +36,9 @@
|
|||||||
<action
|
<action
|
||||||
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
|
||||||
app:destination="@id/gameFoldersFragment" />
|
app:destination="@id/gameFoldersFragment" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_homeSettingsFragment_to_profileManagerFragment"
|
||||||
|
app:destination="@id/profileManagerFragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
@@ -186,5 +189,17 @@
|
|||||||
app:nullable="true"
|
app:nullable="true"
|
||||||
android:defaultValue="@null" />
|
android:defaultValue="@null" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/profileManagerFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.ProfileManagerFragment"
|
||||||
|
android:label="ProfileManagerFragment" >
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_profileManagerFragment_to_newUserDialog"
|
||||||
|
app:destination="@id/newUserDialogFragment" />
|
||||||
|
</fragment>
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/newUserDialogFragment"
|
||||||
|
android:name="org.yuzu.yuzu_emu.fragments.EditUserDialogFragment"
|
||||||
|
android:label="NewUserDialogFragment" />
|
||||||
|
|
||||||
</navigation>
|
</navigation>
|
||||||
|
|||||||
@@ -381,9 +381,9 @@
|
|||||||
<string name="qlaunch_description">Launch applications from the system home screen</string>
|
<string name="qlaunch_description">Launch applications from the system home screen</string>
|
||||||
<string name="applets">Applet launcher</string>
|
<string name="applets">Applet launcher</string>
|
||||||
<string name="applets_description">Launch system applets using installed firmware</string>
|
<string name="applets_description">Launch system applets using installed firmware</string>
|
||||||
<string name="applets_error_firmware">Firmware not installed or invalid version</string>
|
<string name="applets_error_firmware">Firmware not installed</string>
|
||||||
<string name="applets_error_applet">Applet not available</string>
|
<string name="applets_error_applet">Applet not available</string>
|
||||||
<string name="applets_error_description"><![CDATA[Please ensure your <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> file and <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-system-firmware\">firmware</a> are installed and try again.<br>Additionally, ensure your firmware is of version 19.0.1 or older.]]></string>
|
<string name="applets_error_description"><![CDATA[Please ensure your <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> file and <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-system-firmware\">firmware</a> are installed and try again.]]></string>
|
||||||
<string name="album_applet">Album</string>
|
<string name="album_applet">Album</string>
|
||||||
<string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string>
|
<string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string>
|
||||||
<string name="mii_edit_applet">Mii edit</string>
|
<string name="mii_edit_applet">Mii edit</string>
|
||||||
@@ -493,6 +493,8 @@
|
|||||||
<string name="renderer_force_max_clock_description">Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied).</string>
|
<string name="renderer_force_max_clock_description">Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied).</string>
|
||||||
<string name="renderer_reactive_flushing">Use reactive flushing</string>
|
<string name="renderer_reactive_flushing">Use reactive flushing</string>
|
||||||
<string name="renderer_reactive_flushing_description">Improves rendering accuracy in some games at the cost of performance.</string>
|
<string name="renderer_reactive_flushing_description">Improves rendering accuracy in some games at the cost of performance.</string>
|
||||||
|
<string name="enable_buffer_history">Enable buffer history</string>
|
||||||
|
<string name="enable_buffer_history_description">Enables access to previous buffer states. This option may improve rendering quality and performance consistency in some games.</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="hacks">Hacks</string>
|
<string name="hacks">Hacks</string>
|
||||||
@@ -556,13 +558,11 @@
|
|||||||
<string name="flush_by_line_description">Flushes debugging logs on each line written, making debugging easier in cases of crashing or freezing.</string>
|
<string name="flush_by_line_description">Flushes debugging logs on each line written, making debugging easier in cases of crashing or freezing.</string>
|
||||||
|
|
||||||
<!-- GPU Logging strings -->
|
<!-- GPU Logging strings -->
|
||||||
<string name="gpu_logging">GPU Logging</string>
|
|
||||||
<string name="gpu_logging_header">GPU Logging</string>
|
<string name="gpu_logging_header">GPU Logging</string>
|
||||||
<string name="gpu_logging_enabled">Enable GPU Logging</string>
|
<string name="gpu_logging_enabled">Enable GPU Logging</string>
|
||||||
<string name="gpu_logging_enabled_description">Log GPU operations to eden_gpu.log for debugging Adreno drivers</string>
|
<string name="gpu_logging_enabled_description">Log GPU operations to eden_gpu.log for debugging Adreno drivers</string>
|
||||||
<string name="gpu_log_level">Log Level</string>
|
<string name="gpu_log_level">Log Level</string>
|
||||||
<string name="gpu_log_level_description">Detail level for GPU logs (higher = more detail, more overhead)</string>
|
<string name="gpu_log_level_description">Detail level for GPU logs (higher = more detail, more overhead)</string>
|
||||||
<string name="gpu_logging_features">Logging Features</string>
|
|
||||||
<string name="gpu_log_vulkan_calls">Log Vulkan API Calls</string>
|
<string name="gpu_log_vulkan_calls">Log Vulkan API Calls</string>
|
||||||
<string name="gpu_log_vulkan_calls_description">Track all Vulkan API calls in ring buffer</string>
|
<string name="gpu_log_vulkan_calls_description">Track all Vulkan API calls in ring buffer</string>
|
||||||
<string name="gpu_log_shader_dumps">Dump Shaders</string>
|
<string name="gpu_log_shader_dumps">Dump Shaders</string>
|
||||||
@@ -738,6 +738,8 @@
|
|||||||
<string name="preferences_graphics">Graphics</string>
|
<string name="preferences_graphics">Graphics</string>
|
||||||
<string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
|
<string name="preferences_graphics_description">Accuracy level, resolution, shader cache</string>
|
||||||
<string name="quick_settings">Quick Settings</string>
|
<string name="quick_settings">Quick Settings</string>
|
||||||
|
<string name="enable_quick_settings">Enable Quick Settings</string>
|
||||||
|
<string name="enable_quick_settings_description">Allow access to quick settings menu via swipe and menu button</string>
|
||||||
<string name="preferences_audio">Audio</string>
|
<string name="preferences_audio">Audio</string>
|
||||||
<string name="preferences_audio_description">Output engine, volume</string>
|
<string name="preferences_audio_description">Output engine, volume</string>
|
||||||
<string name="preferences_controls">Controls</string>
|
<string name="preferences_controls">Controls</string>
|
||||||
@@ -1163,6 +1165,12 @@
|
|||||||
<string name="use_black_backgrounds">Black backgrounds</string>
|
<string name="use_black_backgrounds">Black backgrounds</string>
|
||||||
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
|
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<string name="enable_folder_button">Folder</string>
|
||||||
|
<string name="enable_folder_button_description">Show the button to add game folders</string>
|
||||||
|
<string name="enable_qlaunch_button">QLaunch</string>
|
||||||
|
<string name="enable_qlaunch_button_description">Show the button to launch QLaunch</string>
|
||||||
|
|
||||||
<!-- App Language -->
|
<!-- App Language -->
|
||||||
<string name="app_language">App Language</string>
|
<string name="app_language">App Language</string>
|
||||||
<string name="app_language_description">Change the language of the app interface</string>
|
<string name="app_language_description">Change the language of the app interface</string>
|
||||||
@@ -1231,6 +1239,35 @@
|
|||||||
<string name="enable_overlay">Enable Overlay Applet</string>
|
<string name="enable_overlay">Enable Overlay Applet</string>
|
||||||
<string name="enable_overlay_description">Enables Horizon\'s built-in overlay applet. Press and hold the home button for 1 second to show it.</string>
|
<string name="enable_overlay_description">Enables Horizon\'s built-in overlay applet. Press and hold the home button for 1 second to show it.</string>
|
||||||
|
|
||||||
|
<!-- Profile Management -->
|
||||||
|
<string name="profile_manager">Profile Manager</string>
|
||||||
|
<string name="profile_manager_description">Manage user profiles</string>
|
||||||
|
<string name="profile_add_user">Add User</string>
|
||||||
|
<string name="profile_new_user">New User</string>
|
||||||
|
<string name="profile_edit_user">Edit User</string>
|
||||||
|
<string name="profile_edit">Edit</string>
|
||||||
|
<string name="profile_delete">Delete</string>
|
||||||
|
<string name="profile_username">Username</string>
|
||||||
|
<string name="profile_uuid">User ID (UUID)</string>
|
||||||
|
<string name="profile_uuid_description">This is the unique identifier for this user profile. It cannot be changed after creation.</string>
|
||||||
|
<string name="profile_generate">Generate</string>
|
||||||
|
<string name="profile_avatar">User Avatar</string>
|
||||||
|
<string name="profile_select_image">Select Image</string>
|
||||||
|
<string name="profile_firmware_avatars">Firmware Avatars</string>
|
||||||
|
<string name="profile_firmware_avatars_unavailable">Firmware avatars are not available. Please install firmware to use this feature.</string>
|
||||||
|
<string name="profile_revert_image">Revert to Default</string>
|
||||||
|
<string name="profile_current_user">Current User</string>
|
||||||
|
<string name="profile_max_users_title">Maximum Users Reached</string>
|
||||||
|
<string name="profile_max_users_message">You cannot create more than 8 user profiles. Please delete an existing profile to create a new one.</string>
|
||||||
|
<string name="profile_delete_confirm_title">Delete Profile?</string>
|
||||||
|
<string name="profile_delete_confirm_message">Are you sure you want to delete %1$s? All save data for this user will be deleted.</string>
|
||||||
|
<string name="profile_delete_current_user_message">Are you sure you want to delete %1$s? This is the currently selected user. The first available user will be selected instead.</string>
|
||||||
|
<string name="profile_create_failed">Failed to create user profile</string>
|
||||||
|
<string name="profile_update_failed">Failed to update user profile</string>
|
||||||
|
<string name="profile_image_load_error">Failed to load image: %1$s</string>
|
||||||
|
<string name="profile_image_save_error">Failed to save image: %1$s</string>
|
||||||
|
<string name="error">Error</string>
|
||||||
|
|
||||||
<!-- Licenses screen strings -->
|
<!-- Licenses screen strings -->
|
||||||
<string name="licenses">Licenses</string>
|
<string name="licenses">Licenses</string>
|
||||||
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
|
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
|
||||||
@@ -1737,4 +1774,8 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<string name="external_content">External Content</string>
|
||||||
|
<string name="add_folders">Add Folder</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-License-Identifier:
|
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-License-Identifier:
|
||||||
@@ -186,8 +186,6 @@ if(ARCHITECTURE_x86_64)
|
|||||||
common
|
common
|
||||||
PRIVATE x64/cpu_detect.cpp
|
PRIVATE x64/cpu_detect.cpp
|
||||||
x64/cpu_detect.h
|
x64/cpu_detect.h
|
||||||
x64/cpu_wait.cpp
|
|
||||||
x64/cpu_wait.h
|
|
||||||
x64/native_clock.cpp
|
x64/native_clock.cpp
|
||||||
x64/native_clock.h
|
x64/native_clock.h
|
||||||
x64/rdtsc.cpp
|
x64/rdtsc.cpp
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
@@ -516,7 +516,7 @@ namespace Common::Android {
|
|||||||
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
|
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
|
||||||
s_patch_constructor = env->GetMethodID(
|
s_patch_constructor = env->GetMethodID(
|
||||||
patch_class, "<init>",
|
patch_class, "<init>",
|
||||||
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
|
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;J)V");
|
||||||
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
|
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
|
||||||
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
|
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
|
||||||
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
|
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
|
#include <cstdlib>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ namespace {
|
|||||||
|
|
||||||
/// @brief Trims up to and including the last of ../, ..\, src/, src\ in a string
|
/// @brief Trims up to and including the last of ../, ..\, src/, src\ in a string
|
||||||
/// do not be fooled this isn't generating new strings on .rodata :)
|
/// do not be fooled this isn't generating new strings on .rodata :)
|
||||||
constexpr const char* TrimSourcePath(std::string_view source) {
|
constexpr const char* TrimSourcePath(std::string_view source) noexcept {
|
||||||
const auto rfind = [source](const std::string_view match) {
|
const auto rfind = [source](const std::string_view match) {
|
||||||
return source.rfind(match) == source.npos ? 0 : (source.rfind(match) + match.size());
|
return source.rfind(match) == source.npos ? 0 : (source.rfind(match) + match.size());
|
||||||
};
|
};
|
||||||
@@ -51,31 +52,31 @@ constexpr const char* TrimSourcePath(std::string_view source) {
|
|||||||
|
|
||||||
/// @brief Interface for logging backends.
|
/// @brief Interface for logging backends.
|
||||||
struct Backend {
|
struct Backend {
|
||||||
virtual ~Backend() = default;
|
virtual ~Backend() noexcept = default;
|
||||||
virtual void Write(const Entry& entry) = 0;
|
virtual void Write(const Entry& entry) noexcept = 0;
|
||||||
virtual void EnableForStacktrace() = 0;
|
virtual void EnableForStacktrace() noexcept= 0;
|
||||||
virtual void Flush() = 0;
|
virtual void Flush() noexcept = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @brief Backend that writes to stderr and with color
|
/// @brief Backend that writes to stderr and with color
|
||||||
struct ColorConsoleBackend final : public Backend {
|
struct ColorConsoleBackend final : public Backend {
|
||||||
explicit ColorConsoleBackend() = default;
|
explicit ColorConsoleBackend() noexcept = default;
|
||||||
~ColorConsoleBackend() override = default;
|
~ColorConsoleBackend() noexcept override = default;
|
||||||
|
|
||||||
void Write(const Entry& entry) override {
|
void Write(const Entry& entry) noexcept override {
|
||||||
if (enabled.load(std::memory_order_relaxed))
|
if (enabled.load(std::memory_order_relaxed))
|
||||||
PrintColoredMessage(entry);
|
PrintColoredMessage(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Flush() override {
|
void Flush() noexcept override {
|
||||||
// stderr shouldn't be buffered
|
// stderr shouldn't be buffered
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnableForStacktrace() override {
|
void EnableForStacktrace() noexcept override {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetEnabled(bool enabled_) {
|
void SetEnabled(bool enabled_) noexcept {
|
||||||
enabled = enabled_;
|
enabled = enabled_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +84,10 @@ private:
|
|||||||
std::atomic_bool enabled{false};
|
std::atomic_bool enabled{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifndef __OPENORBIS__
|
||||||
/// @brief Backend that writes to a file passed into the constructor
|
/// @brief Backend that writes to a file passed into the constructor
|
||||||
struct FileBackend final : public Backend {
|
struct FileBackend final : public Backend {
|
||||||
explicit FileBackend(const std::filesystem::path& filename) {
|
explicit FileBackend(const std::filesystem::path& filename) noexcept {
|
||||||
auto old_filename = filename;
|
auto old_filename = filename;
|
||||||
old_filename += ".old.txt";
|
old_filename += ".old.txt";
|
||||||
|
|
||||||
@@ -97,9 +99,9 @@ struct FileBackend final : public Backend {
|
|||||||
file = std::make_unique<FS::IOFile>(filename, FS::FileAccessMode::Write, FS::FileType::TextFile);
|
file = std::make_unique<FS::IOFile>(filename, FS::FileAccessMode::Write, FS::FileType::TextFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
~FileBackend() override = default;
|
~FileBackend() noexcept override = default;
|
||||||
|
|
||||||
void Write(const Entry& entry) override {
|
void Write(const Entry& entry) noexcept override {
|
||||||
if (!enabled)
|
if (!enabled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -147,11 +149,11 @@ struct FileBackend final : public Backend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Flush() override {
|
void Flush() noexcept override {
|
||||||
file->Flush();
|
file->Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnableForStacktrace() override {
|
void EnableForStacktrace() noexcept override {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
bytes_written = 0;
|
bytes_written = 0;
|
||||||
}
|
}
|
||||||
@@ -161,29 +163,30 @@ private:
|
|||||||
std::size_t bytes_written = 0;
|
std::size_t bytes_written = 0;
|
||||||
bool enabled = true;
|
bool enabled = true;
|
||||||
};
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
/// @brief Backend that writes to Visual Studio's output window
|
/// @brief Backend that writes to Visual Studio's output window
|
||||||
struct DebuggerBackend final : public Backend {
|
struct DebuggerBackend final : public Backend {
|
||||||
explicit DebuggerBackend() = default;
|
explicit DebuggerBackend() noexcept = default;
|
||||||
~DebuggerBackend() override = default;
|
~DebuggerBackend() noexcept override = default;
|
||||||
void Write(const Entry& entry) override {
|
void Write(const Entry& entry) noexcept override {
|
||||||
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
|
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
|
||||||
}
|
}
|
||||||
void Flush() override {}
|
void Flush() noexcept override {}
|
||||||
void EnableForStacktrace() override {}
|
void EnableForStacktrace() noexcept override {}
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
/// @brief Backend that writes to the Android logcat
|
/// @brief Backend that writes to the Android logcat
|
||||||
struct LogcatBackend : public Backend {
|
struct LogcatBackend : public Backend {
|
||||||
explicit LogcatBackend() = default;
|
explicit LogcatBackend() noexcept = default;
|
||||||
~LogcatBackend() override = default;
|
~LogcatBackend() noexcept override = default;
|
||||||
void Write(const Entry& entry) override {
|
void Write(const Entry& entry) noexcept override {
|
||||||
PrintMessageToLogcat(entry);
|
PrintMessageToLogcat(entry);
|
||||||
}
|
}
|
||||||
void Flush() override {}
|
void Flush() noexcept override {}
|
||||||
void EnableForStacktrace() override {}
|
void EnableForStacktrace() noexcept override {}
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -192,13 +195,13 @@ bool initialization_in_progress_suppress_logging = true;
|
|||||||
/// @brief Static state as a singleton.
|
/// @brief Static state as a singleton.
|
||||||
class Impl {
|
class Impl {
|
||||||
public:
|
public:
|
||||||
static Impl& Instance() {
|
static Impl& Instance() noexcept {
|
||||||
if (!instance)
|
if (!instance)
|
||||||
throw std::runtime_error("Using Logging instance before its initialization");
|
std::abort();
|
||||||
return *instance;
|
return *instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Initialize() {
|
static void Initialize() noexcept {
|
||||||
if (instance) {
|
if (instance) {
|
||||||
LOG_WARNING(Log, "Reinitializing logging backend");
|
LOG_WARNING(Log, "Reinitializing logging backend");
|
||||||
return;
|
return;
|
||||||
@@ -212,25 +215,25 @@ public:
|
|||||||
initialization_in_progress_suppress_logging = false;
|
initialization_in_progress_suppress_logging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Start() {
|
static void Start() noexcept {
|
||||||
instance->StartBackendThread();
|
instance->StartBackendThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Stop() {
|
static void Stop() noexcept {
|
||||||
instance->StopBackendThread();
|
instance->StopBackendThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
Impl(const Impl&) = delete;
|
Impl(const Impl&) noexcept = delete;
|
||||||
Impl& operator=(const Impl&) = delete;
|
Impl& operator=(const Impl&) noexcept = delete;
|
||||||
|
|
||||||
Impl(Impl&&) = delete;
|
Impl(Impl&&) noexcept = delete;
|
||||||
Impl& operator=(Impl&&) = delete;
|
Impl& operator=(Impl&&) noexcept = delete;
|
||||||
|
|
||||||
void SetGlobalFilter(const Filter& f) {
|
void SetGlobalFilter(const Filter& f) noexcept {
|
||||||
filter = f;
|
filter = f;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SetColorConsoleBackendEnabled(bool enabled) {
|
void SetColorConsoleBackendEnabled(bool enabled) noexcept {
|
||||||
color_console_backend.SetEnabled(enabled);
|
color_console_backend.SetEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,19 +241,21 @@ public:
|
|||||||
return filter.CheckMessage(log_class, log_level);
|
return filter.CheckMessage(log_class, log_level);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num,
|
void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, std::string&& message) noexcept {
|
||||||
const char* function, std::string&& message) noexcept {
|
message_queue.EmplaceWait(CreateEntry(log_class, log_level, TrimSourcePath(filename), line_num, function, std::move(message)));
|
||||||
message_queue.EmplaceWait(
|
|
||||||
CreateEntry(log_class, log_level, TrimSourcePath(filename), line_num, function, std::move(message)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Impl(const std::filesystem::path& file_backend_filename, const Filter& filter_)
|
Impl(const std::filesystem::path& file_backend_filename, const Filter& filter_) noexcept :
|
||||||
: filter{filter_}, file_backend{file_backend_filename} {}
|
filter{filter_}
|
||||||
|
#ifndef __OPENORBIS__
|
||||||
|
, file_backend{file_backend_filename}
|
||||||
|
#endif
|
||||||
|
{}
|
||||||
|
|
||||||
~Impl() = default;
|
~Impl() noexcept = default;
|
||||||
|
|
||||||
void StartBackendThread() {
|
void StartBackendThread() noexcept {
|
||||||
backend_thread = std::jthread([this](std::stop_token stop_token) {
|
backend_thread = std::jthread([this](std::stop_token stop_token) {
|
||||||
Common::SetCurrentThreadName("Logger");
|
Common::SetCurrentThreadName("Logger");
|
||||||
Entry entry;
|
Entry entry;
|
||||||
@@ -271,15 +276,14 @@ private:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void StopBackendThread() {
|
void StopBackendThread() noexcept {
|
||||||
backend_thread.request_stop();
|
backend_thread.request_stop();
|
||||||
if (backend_thread.joinable())
|
if (backend_thread.joinable())
|
||||||
backend_thread.join();
|
backend_thread.join();
|
||||||
ForEachBackend([](Backend& backend) { backend.Flush(); });
|
ForEachBackend([](Backend& backend) { backend.Flush(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr, const char* function, std::string&& message) const noexcept {
|
||||||
const char* function, std::string&& message) const {
|
|
||||||
using std::chrono::duration_cast;
|
using std::chrono::duration_cast;
|
||||||
using std::chrono::microseconds;
|
using std::chrono::microseconds;
|
||||||
using std::chrono::steady_clock;
|
using std::chrono::steady_clock;
|
||||||
@@ -294,9 +298,11 @@ private:
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void ForEachBackend(auto lambda) {
|
void ForEachBackend(auto lambda) noexcept {
|
||||||
lambda(static_cast<Backend&>(color_console_backend));
|
lambda(static_cast<Backend&>(color_console_backend));
|
||||||
|
#ifndef __OPENORBIS__
|
||||||
lambda(static_cast<Backend&>(file_backend));
|
lambda(static_cast<Backend&>(file_backend));
|
||||||
|
#endif
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
lambda(static_cast<Backend&>(debugger_backend));
|
lambda(static_cast<Backend&>(debugger_backend));
|
||||||
#endif
|
#endif
|
||||||
@@ -305,7 +311,7 @@ private:
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Deleter(Impl* ptr) {
|
static void Deleter(Impl* ptr) noexcept {
|
||||||
delete ptr;
|
delete ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +319,9 @@ private:
|
|||||||
|
|
||||||
Filter filter;
|
Filter filter;
|
||||||
ColorConsoleBackend color_console_backend{};
|
ColorConsoleBackend color_console_backend{};
|
||||||
|
#ifndef __OPENORBIS__
|
||||||
FileBackend file_backend;
|
FileBackend file_backend;
|
||||||
|
#endif
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
DebuggerBackend debugger_backend{};
|
DebuggerBackend debugger_backend{};
|
||||||
#endif
|
#endif
|
||||||
@@ -351,9 +359,7 @@ void SetColorConsoleBackendEnabled(bool enabled) {
|
|||||||
Impl::Instance().SetColorConsoleBackendEnabled(enabled);
|
Impl::Instance().SetColorConsoleBackendEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename,
|
void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, fmt::string_view format, const fmt::format_args& args) {
|
||||||
unsigned int line_num, const char* function, fmt::string_view format,
|
|
||||||
const fmt::format_args& args) {
|
|
||||||
if (!initialization_in_progress_suppress_logging) {
|
if (!initialization_in_progress_suppress_logging) {
|
||||||
auto& instance = Impl::Instance();
|
auto& instance = Impl::Instance();
|
||||||
if (instance.CanPushEntry(log_class, log_level))
|
if (instance.CanPushEntry(log_class, log_level))
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -9,17 +12,15 @@
|
|||||||
|
|
||||||
namespace Common::Log {
|
namespace Common::Log {
|
||||||
|
|
||||||
/**
|
/// @brief A log entry. Log entries are store in a structured format to permit more varied output
|
||||||
* A log entry. Log entries are store in a structured format to permit more varied output
|
/// formatting on different frontends, as well as facilitating filtering and aggregation.
|
||||||
* formatting on different frontends, as well as facilitating filtering and aggregation.
|
|
||||||
*/
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
std::chrono::microseconds timestamp;
|
std::chrono::microseconds timestamp;
|
||||||
Class log_class{};
|
Class log_class{};
|
||||||
Level log_level{};
|
Level log_level{};
|
||||||
const char* filename = nullptr;
|
const char* filename = nullptr;
|
||||||
unsigned int line_num = 0;
|
unsigned int line_num = 0;
|
||||||
std::string function;
|
const char* function = nullptr;
|
||||||
std::string message;
|
std::string message;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,24 +24,26 @@
|
|||||||
namespace Common::Log {
|
namespace Common::Log {
|
||||||
|
|
||||||
// Some IDEs prefer <file>:<line> instead, so let's just do that :)
|
// Some IDEs prefer <file>:<line> instead, so let's just do that :)
|
||||||
std::string FormatLogMessage(const Entry& entry) {
|
std::string FormatLogMessage(const Entry& entry) noexcept {
|
||||||
if (!entry.filename) return "";
|
if (!entry.filename) return "";
|
||||||
|
|
||||||
auto const time_seconds = uint32_t(entry.timestamp.count() / 1000000);
|
auto const time_seconds = uint32_t(entry.timestamp.count() / 1000000);
|
||||||
auto const time_fractional = uint32_t(entry.timestamp.count() % 1000000);
|
auto const time_fractional = uint32_t(entry.timestamp.count() % 1000000);
|
||||||
char const* class_name = GetLogClassName(entry.log_class);
|
auto const class_name = GetLogClassName(entry.log_class);
|
||||||
char const* level_name = GetLevelName(entry.log_level);
|
auto const level_name = GetLevelName(entry.log_level);
|
||||||
return fmt::format("[{:4d}.{:06d}] {} <{}> {}:{}:{}: {}", time_seconds, time_fractional,
|
return fmt::format("[{:4d}.{:06d}] {} <{}> {}:{}:{}: {}", time_seconds, time_fractional,
|
||||||
class_name, level_name, entry.filename, entry.line_num, entry.function,
|
class_name, level_name, entry.filename, entry.line_num, entry.function,
|
||||||
entry.message);
|
entry.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintMessage(const Entry& entry) {
|
/// @brief Formats and prints a log entry to stderr.
|
||||||
|
static void PrintMessage(const Entry& entry) noexcept {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
auto const str = FormatLogMessage(entry).append(1, '\n');
|
auto const str = FormatLogMessage(entry).append(1, '\n');
|
||||||
|
fwrite(str.c_str(), 1, str.size(), stderr);
|
||||||
#else
|
#else
|
||||||
#define ESC "\x1b"
|
#define ESC "\x1b"
|
||||||
auto str = std::string{[&entry]() -> const char* {
|
auto const color_str = [&entry]() -> const char* {
|
||||||
switch (entry.log_level) {
|
switch (entry.log_level) {
|
||||||
case Level::Debug: return ESC "[0;36m"; // Cyan
|
case Level::Debug: return ESC "[0;36m"; // Cyan
|
||||||
case Level::Info: return ESC "[0;37m"; // Bright gray
|
case Level::Info: return ESC "[0;37m"; // Bright gray
|
||||||
@@ -50,15 +52,19 @@ void PrintMessage(const Entry& entry) {
|
|||||||
case Level::Critical: return ESC "[1;35m"; // Bright magenta
|
case Level::Critical: return ESC "[1;35m"; // Bright magenta
|
||||||
default: return ESC "[1;30m"; // Grey
|
default: return ESC "[1;30m"; // Grey
|
||||||
}
|
}
|
||||||
}()};
|
}();
|
||||||
str.append(FormatLogMessage(entry));
|
auto const time_seconds = uint32_t(entry.timestamp.count() / 1000000);
|
||||||
str.append(ESC "[0m\n");
|
auto const time_fractional = uint32_t(entry.timestamp.count() % 1000000);
|
||||||
|
auto const class_name = GetLogClassName(entry.log_class);
|
||||||
|
auto const level_name = GetLevelName(entry.log_level);
|
||||||
|
fprintf(stderr, "%s[%4d.%06d] %s <%s> %s:%u:%s: %s" ESC "[0m\n", color_str,
|
||||||
|
time_seconds, time_fractional, class_name, level_name, entry.filename,
|
||||||
|
entry.line_num, entry.function, entry.message.c_str());
|
||||||
#undef ESC
|
#undef ESC
|
||||||
#endif
|
#endif
|
||||||
fwrite(str.c_str(), 1, str.size(), stderr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintColoredMessage(const Entry& entry) {
|
void PrintColoredMessage(const Entry& entry) noexcept {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
HANDLE console_handle = GetStdHandle(STD_ERROR_HANDLE);
|
HANDLE console_handle = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
if (console_handle == INVALID_HANDLE_VALUE)
|
if (console_handle == INVALID_HANDLE_VALUE)
|
||||||
@@ -84,7 +90,7 @@ void PrintColoredMessage(const Entry& entry) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintMessageToLogcat(const Entry& entry) {
|
void PrintMessageToLogcat(const Entry& entry) noexcept {
|
||||||
#ifdef ANDROID
|
#ifdef ANDROID
|
||||||
android_LogPriority android_log_priority = [&]() {
|
android_LogPriority android_log_priority = [&]() {
|
||||||
switch (entry.log_level) {
|
switch (entry.log_level) {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -10,11 +13,9 @@ namespace Common::Log {
|
|||||||
struct Entry;
|
struct Entry;
|
||||||
|
|
||||||
/// Formats a log entry into the provided text buffer.
|
/// Formats a log entry into the provided text buffer.
|
||||||
std::string FormatLogMessage(const Entry& entry);
|
std::string FormatLogMessage(const Entry& entry) noexcept;
|
||||||
/// Formats and prints a log entry to stderr.
|
|
||||||
void PrintMessage(const Entry& entry);
|
|
||||||
/// Prints the same message as `PrintMessage`, but colored according to the severity level.
|
/// Prints the same message as `PrintMessage`, but colored according to the severity level.
|
||||||
void PrintColoredMessage(const Entry& entry);
|
void PrintColoredMessage(const Entry& entry) noexcept;
|
||||||
/// Formats and prints a log entry to the android logcat.
|
/// Formats and prints a log entry to the android logcat.
|
||||||
void PrintMessageToLogcat(const Entry& entry);
|
void PrintMessageToLogcat(const Entry& entry) noexcept;
|
||||||
} // namespace Common::Log
|
} // namespace Common::Log
|
||||||
|
|||||||
+32
-34
@@ -5,7 +5,6 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
#include <version>
|
#include <version>
|
||||||
#include "common/settings_enums.h"
|
|
||||||
#if __cpp_lib_chrono >= 201907L
|
#if __cpp_lib_chrono >= 201907L
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
@@ -17,8 +16,10 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
|
#include <deque>
|
||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
|
|
||||||
|
#include "common/settings_enums.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/fs/fs_util.h"
|
#include "common/fs/fs_util.h"
|
||||||
#include "common/fs/path_util.h"
|
#include "common/fs/path_util.h"
|
||||||
@@ -110,36 +111,38 @@ std::string GetTimeZoneString(TimeZone time_zone) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void LogSettings() {
|
void LogSettings() {
|
||||||
const auto log_setting = [](std::string_view name, const auto& value) {
|
std::deque<std::string> settings_list;
|
||||||
LOG_INFO(Config, "{}: {}", name, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto log_path = [](std::string_view name, const std::filesystem::path& path) {
|
|
||||||
LOG_INFO(Config, "{}: {}", name, Common::FS::PathToUTF8String(path));
|
|
||||||
};
|
|
||||||
|
|
||||||
LOG_INFO(Config, "Eden Configuration:");
|
|
||||||
for (auto& [category, settings] : values.linkage.by_category) {
|
for (auto& [category, settings] : values.linkage.by_category) {
|
||||||
for (const auto& setting : settings) {
|
for (const auto& setting : settings) {
|
||||||
if (setting->Id() == values.eden_token.Id()) {
|
// Hide the token secret, for security reasons.
|
||||||
// Hide the token secret, for security reasons.
|
if (setting->Id() != values.eden_token.Id()) {
|
||||||
continue;
|
auto const is_default = setting->ToString() == setting->DefaultToString();
|
||||||
|
auto const name = fmt::format(
|
||||||
|
"{:c}{:c} {}.{}",
|
||||||
|
is_default ? '-' : 'M',
|
||||||
|
setting->UsingGlobal() ? '-' : 'C', TranslateCategory(category),
|
||||||
|
setting->GetLabel());
|
||||||
|
if (is_default)
|
||||||
|
settings_list.push_back(fmt::format("{}: {}\n", name, setting->Canonicalize()));
|
||||||
|
else
|
||||||
|
settings_list.push_front(fmt::format("{}: {}\n", name, setting->Canonicalize()));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto name = fmt::format(
|
|
||||||
"{:c}{:c} {}.{}", setting->ToString() == setting->DefaultToString() ? '-' : 'M',
|
|
||||||
setting->UsingGlobal() ? '-' : 'C', TranslateCategory(category),
|
|
||||||
setting->GetLabel());
|
|
||||||
|
|
||||||
log_setting(name, setting->Canonicalize());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log_path("DataStorage_CacheDir", Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir));
|
|
||||||
log_path("DataStorage_ConfigDir", Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir));
|
std::string settings_str{};
|
||||||
log_path("DataStorage_LoadDir", Common::FS::GetEdenPath(Common::FS::EdenPath::LoadDir));
|
for (auto const& e : settings_list)
|
||||||
log_path("DataStorage_NANDDir", Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir));
|
settings_str += e;
|
||||||
log_path("DataStorage_SaveDir", Common::FS::GetEdenPath(Common::FS::EdenPath::SaveDir));
|
LOG_INFO(Config, "Eden Configuration:\n{}", settings_str);
|
||||||
log_path("DataStorage_SDMCDir", Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir));
|
#define LOG_PATH(NAME) \
|
||||||
|
LOG_INFO(Config, #NAME ": {}", Common::FS::PathToUTF8String(Common::FS::GetEdenPath(Common::FS::EdenPath::NAME)))
|
||||||
|
LOG_PATH(CacheDir);
|
||||||
|
LOG_PATH(ConfigDir);
|
||||||
|
LOG_PATH(LoadDir);
|
||||||
|
LOG_PATH(NANDDir);
|
||||||
|
LOG_PATH(SaveDir);
|
||||||
|
LOG_PATH(SDMCDir);
|
||||||
|
#undef LOG_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
bool getDebugKnobAt(u8 i) {
|
bool getDebugKnobAt(u8 i) {
|
||||||
@@ -171,12 +174,10 @@ bool IsDMALevelSafe() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IsFastmemEnabled() {
|
bool IsFastmemEnabled() {
|
||||||
if (values.cpu_accuracy.GetValue() == Settings::CpuAccuracy::Debugging) {
|
if (values.cpu_accuracy.GetValue() == Settings::CpuAccuracy::Debugging)
|
||||||
return bool(values.cpuopt_fastmem);
|
return bool(values.cpuopt_fastmem);
|
||||||
}
|
else if (values.cpu_accuracy.GetValue() == CpuAccuracy::Unsafe)
|
||||||
if (values.cpu_accuracy.GetValue() == CpuAccuracy::Unsafe) {
|
|
||||||
return bool(values.cpuopt_unsafe_host_mmu);
|
return bool(values.cpuopt_unsafe_host_mmu);
|
||||||
}
|
|
||||||
#if !defined(__APPLE__) && !defined(__linux__) && !defined(__ANDROID__) && !defined(_WIN32)
|
#if !defined(__APPLE__) && !defined(__linux__) && !defined(__ANDROID__) && !defined(_WIN32)
|
||||||
return false;
|
return false;
|
||||||
#else
|
#else
|
||||||
@@ -341,10 +342,7 @@ void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo& info)
|
|||||||
info.down_shift = 0;
|
info.down_shift = 0;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ASSERT(false);
|
UNREACHABLE();
|
||||||
info.up_scale = 1;
|
|
||||||
info.down_shift = 0;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
info.up_factor = static_cast<f32>(info.up_scale) / (1U << info.down_shift);
|
info.up_factor = static_cast<f32>(info.up_scale) / (1U << info.down_shift);
|
||||||
info.down_factor = static_cast<f32>(1U << info.down_shift) / info.up_scale;
|
info.down_factor = static_cast<f32>(1U << info.down_shift) / info.up_scale;
|
||||||
|
|||||||
+11
-2
@@ -481,6 +481,14 @@ struct Values {
|
|||||||
SwitchableSetting<bool> barrier_feedback_loops{linkage, true, "barrier_feedback_loops",
|
SwitchableSetting<bool> barrier_feedback_loops{linkage, true, "barrier_feedback_loops",
|
||||||
Category::RendererAdvanced};
|
Category::RendererAdvanced};
|
||||||
|
|
||||||
|
SwitchableSetting<bool> enable_buffer_history{linkage,
|
||||||
|
false,
|
||||||
|
"enable_buffer_history",
|
||||||
|
Category::RendererAdvanced,
|
||||||
|
Specialization::Default,
|
||||||
|
true,
|
||||||
|
true};
|
||||||
|
|
||||||
// Renderer Hacks //
|
// Renderer Hacks //
|
||||||
SwitchableSetting<GpuOverclock> fast_gpu_time{linkage,
|
SwitchableSetting<GpuOverclock> fast_gpu_time{linkage,
|
||||||
GpuOverclock::Medium,
|
GpuOverclock::Medium,
|
||||||
@@ -707,6 +715,7 @@ struct Values {
|
|||||||
Category::DataStorage};
|
Category::DataStorage};
|
||||||
Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path",
|
Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path",
|
||||||
Category::DataStorage};
|
Category::DataStorage};
|
||||||
|
std::vector<std::string> external_content_dirs;
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
bool record_frame_times;
|
bool record_frame_times;
|
||||||
@@ -739,7 +748,7 @@ struct Values {
|
|||||||
Setting<bool> disable_web_applet{linkage, true, "disable_web_applet", Category::Debugging};
|
Setting<bool> disable_web_applet{linkage, true, "disable_web_applet", Category::Debugging};
|
||||||
|
|
||||||
// GPU Logging
|
// GPU Logging
|
||||||
Setting<bool> gpu_logging_enabled{linkage, true, "gpu_logging_enabled", Category::Debugging};
|
Setting<bool> gpu_logging_enabled{linkage, false, "gpu_logging_enabled", Category::Debugging};
|
||||||
SwitchableSetting<GpuLogLevel> gpu_log_level{linkage, GpuLogLevel::Standard, "gpu_log_level",
|
SwitchableSetting<GpuLogLevel> gpu_log_level{linkage, GpuLogLevel::Standard, "gpu_log_level",
|
||||||
Category::Debugging};
|
Category::Debugging};
|
||||||
Setting<bool> gpu_log_vulkan_calls{linkage, true, "gpu_log_vulkan_calls", Category::Debugging};
|
Setting<bool> gpu_log_vulkan_calls{linkage, true, "gpu_log_vulkan_calls", Category::Debugging};
|
||||||
@@ -778,7 +787,7 @@ struct Values {
|
|||||||
Category::WebService};
|
Category::WebService};
|
||||||
Setting<std::string> eden_username{linkage, "Eden", "eden_username",
|
Setting<std::string> eden_username{linkage, "Eden", "eden_username",
|
||||||
Category::WebService};
|
Category::WebService};
|
||||||
Setting<std::string> eden_token{linkage, "njausoolxygtpvraofqunuufhmupriifnpfggjxefntlyglr",
|
Setting<std::string> eden_token{linkage, "",
|
||||||
"eden_token", Category::WebService};
|
"eden_token", Category::WebService};
|
||||||
|
|
||||||
// Add-Ons
|
// Add-Ons
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
|
|||||||
ENUM(TemperatureUnits, Celsius, Fahrenheit)
|
ENUM(TemperatureUnits, Celsius, Fahrenheit)
|
||||||
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
|
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
|
||||||
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
|
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
|
||||||
|
ENUM(GameListMode, TreeView, GridView);
|
||||||
|
|
||||||
template <typename Type>
|
template <typename Type>
|
||||||
inline std::string_view CanonicalizeEnum(Type id) {
|
inline std::string_view CanonicalizeEnum(Type id) {
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
|
||||||
#include <intrin.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "common/x64/cpu_detect.h"
|
|
||||||
#include "common/x64/cpu_wait.h"
|
|
||||||
#include "common/x64/rdtsc.h"
|
|
||||||
|
|
||||||
namespace Common::X64 {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
// 100,000 cycles is a reasonable amount of time to wait to save on CPU resources.
|
|
||||||
// For reference:
|
|
||||||
// At 1 GHz, 100K cycles is 100us
|
|
||||||
// At 2 GHz, 100K cycles is 50us
|
|
||||||
// At 4 GHz, 100K cycles is 25us
|
|
||||||
constexpr auto PauseCycles = 100'000U;
|
|
||||||
|
|
||||||
} // Anonymous namespace
|
|
||||||
|
|
||||||
#if defined(_MSC_VER) && !defined(__clang__)
|
|
||||||
__forceinline static void TPAUSE() {
|
|
||||||
static constexpr auto RequestC02State = 0U;
|
|
||||||
_tpause(RequestC02State, FencedRDTSC() + PauseCycles);
|
|
||||||
}
|
|
||||||
|
|
||||||
__forceinline static void MWAITX() {
|
|
||||||
static constexpr auto EnableWaitTimeFlag = 1U << 1;
|
|
||||||
static constexpr auto RequestC1State = 0U;
|
|
||||||
|
|
||||||
// monitor_var should be aligned to a cache line.
|
|
||||||
alignas(64) u64 monitor_var{};
|
|
||||||
_mm_monitorx(&monitor_var, 0, 0);
|
|
||||||
_mm_mwaitx(EnableWaitTimeFlag, RequestC1State, PauseCycles);
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
static void TPAUSE() {
|
|
||||||
static constexpr auto RequestC02State = 0U;
|
|
||||||
const auto tsc = FencedRDTSC() + PauseCycles;
|
|
||||||
const auto eax = static_cast<u32>(tsc & 0xFFFFFFFF);
|
|
||||||
const auto edx = static_cast<u32>(tsc >> 32);
|
|
||||||
asm volatile("tpause %0" : : "r"(RequestC02State), "d"(edx), "a"(eax));
|
|
||||||
}
|
|
||||||
|
|
||||||
static void MWAITX() {
|
|
||||||
static constexpr auto EnableWaitTimeFlag = 1U << 1;
|
|
||||||
static constexpr auto RequestC1State = 0U;
|
|
||||||
|
|
||||||
// monitor_var should be aligned to a cache line.
|
|
||||||
alignas(64) u64 monitor_var{};
|
|
||||||
asm volatile("monitorx" : : "a"(&monitor_var), "c"(0), "d"(0));
|
|
||||||
asm volatile("mwaitx" : : "a"(RequestC1State), "b"(PauseCycles), "c"(EnableWaitTimeFlag));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void MicroSleep() {
|
|
||||||
static const bool has_waitpkg = GetCPUCaps().waitpkg;
|
|
||||||
static const bool has_monitorx = GetCPUCaps().monitorx;
|
|
||||||
|
|
||||||
if (has_waitpkg) {
|
|
||||||
TPAUSE();
|
|
||||||
} else if (has_monitorx) {
|
|
||||||
MWAITX();
|
|
||||||
} else {
|
|
||||||
std::this_thread::yield();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Common::X64
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
namespace Common::X64 {
|
|
||||||
|
|
||||||
void MicroSleep();
|
|
||||||
|
|
||||||
} // namespace Common::X64
|
|
||||||
+326
-153
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
@@ -20,9 +20,13 @@ namespace Core::NCE {
|
|||||||
Patcher::Patcher(Patcher&& other) noexcept
|
Patcher::Patcher(Patcher&& other) noexcept
|
||||||
: patch_cache(std::move(other.patch_cache)),
|
: patch_cache(std::move(other.patch_cache)),
|
||||||
m_patch_instructions(std::move(other.m_patch_instructions)),
|
m_patch_instructions(std::move(other.m_patch_instructions)),
|
||||||
|
m_patch_instructions_pre(std::move(other.m_patch_instructions_pre)),
|
||||||
c(m_patch_instructions),
|
c(m_patch_instructions),
|
||||||
|
c_pre(m_patch_instructions_pre),
|
||||||
m_save_context(other.m_save_context),
|
m_save_context(other.m_save_context),
|
||||||
m_load_context(other.m_load_context),
|
m_load_context(other.m_load_context),
|
||||||
|
m_save_context_pre(other.m_save_context_pre),
|
||||||
|
m_load_context_pre(other.m_load_context_pre),
|
||||||
mode(other.mode),
|
mode(other.mode),
|
||||||
total_program_size(other.total_program_size),
|
total_program_size(other.total_program_size),
|
||||||
m_relocate_module_index(other.m_relocate_module_index),
|
m_relocate_module_index(other.m_relocate_module_index),
|
||||||
@@ -42,20 +46,25 @@ using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
|
|||||||
constexpr size_t MaxRelativeBranch = 128_MiB;
|
constexpr size_t MaxRelativeBranch = 128_MiB;
|
||||||
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
|
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
|
||||||
|
|
||||||
Patcher::Patcher() : c(m_patch_instructions) {
|
Patcher::Patcher() : c(m_patch_instructions), c_pre(m_patch_instructions_pre) {
|
||||||
LOG_WARNING(Core_ARM, "Patcher initialized with LRU cache {}",
|
LOG_WARNING(Core_ARM, "Patcher initialized with LRU cache {}",
|
||||||
patch_cache.isEnabled() ? "enabled" : "disabled");
|
patch_cache.isEnabled() ? "enabled" : "disabled");
|
||||||
// The first word of the patch section is always a branch to the first instruction of the
|
// The first word of the patch section is always a branch to the first instruction of the
|
||||||
// module.
|
// module.
|
||||||
c.dw(0);
|
c.dw(0);
|
||||||
|
c_pre.dw(0);
|
||||||
|
|
||||||
// Write save context helper function.
|
// Write save context helper function.
|
||||||
c.l(m_save_context);
|
c.l(m_save_context);
|
||||||
WriteSaveContext();
|
WriteSaveContext();
|
||||||
|
c_pre.l(m_save_context_pre);
|
||||||
|
WriteSaveContext(c_pre);
|
||||||
|
|
||||||
// Write load context helper function.
|
// Write load context helper function.
|
||||||
c.l(m_load_context);
|
c.l(m_load_context);
|
||||||
WriteLoadContext();
|
WriteLoadContext();
|
||||||
|
c_pre.l(m_load_context_pre);
|
||||||
|
WriteLoadContext(c_pre);
|
||||||
}
|
}
|
||||||
|
|
||||||
Patcher::~Patcher() = default;
|
Patcher::~Patcher() = default;
|
||||||
@@ -64,7 +73,16 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
const Kernel::CodeSet::Segment& code) {
|
const Kernel::CodeSet::Segment& code) {
|
||||||
// If we have patched modules but cannot reach the new module, then it needs its own patcher.
|
// If we have patched modules but cannot reach the new module, then it needs its own patcher.
|
||||||
const size_t image_size = program_image.size();
|
const size_t image_size = program_image.size();
|
||||||
if (total_program_size + image_size > MaxRelativeBranch && total_program_size > 0) {
|
|
||||||
|
// Check if we need split mode for large modules. A64 max takes 128MB
|
||||||
|
// tests showed that, with update, some are larger. (In this case 208MB)
|
||||||
|
bool use_split = false;
|
||||||
|
if (image_size > MaxRelativeBranch) {
|
||||||
|
if (total_program_size > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
use_split = true;
|
||||||
|
} else if (total_program_size + image_size > MaxRelativeBranch && total_program_size > 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +92,12 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
|
|
||||||
// The first word of the patch section is always a branch to the first instruction of the
|
// The first word of the patch section is always a branch to the first instruction of the
|
||||||
// module.
|
// module.
|
||||||
curr_patch->m_branch_to_module_relocations.push_back({0, 0});
|
if (use_split) {
|
||||||
|
// curr_patch->m_branch_to_module_relocations.push_back({0, 0});
|
||||||
|
curr_patch->m_branch_to_module_relocations_pre.push_back({0, 0});
|
||||||
|
} else {
|
||||||
|
curr_patch->m_branch_to_module_relocations.push_back({0, 0});
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve text segment data.
|
// Retrieve text segment data.
|
||||||
const auto text = std::span{program_image}.subspan(code.offset, code.size);
|
const auto text = std::span{program_image}.subspan(code.offset, code.size);
|
||||||
@@ -85,12 +108,18 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
for (u32 i = ModuleCodeIndex; i < static_cast<u32>(text_words.size()); i++) {
|
for (u32 i = ModuleCodeIndex; i < static_cast<u32>(text_words.size()); i++) {
|
||||||
const u32 inst = text_words[i];
|
const u32 inst = text_words[i];
|
||||||
|
|
||||||
const auto AddRelocations = [&] {
|
const auto AddRelocations = [&](bool& pre_buffer) {
|
||||||
const uintptr_t this_offset = i * sizeof(u32);
|
const uintptr_t this_offset = i * sizeof(u32);
|
||||||
const uintptr_t next_offset = this_offset + sizeof(u32);
|
const uintptr_t next_offset = this_offset + sizeof(u32);
|
||||||
|
|
||||||
// Relocate from here to patch.
|
pre_buffer = use_split && (this_offset < MaxRelativeBranch);
|
||||||
this->BranchToPatch(this_offset);
|
|
||||||
|
// Relocate to pre- or post-patch
|
||||||
|
if (pre_buffer) {
|
||||||
|
this->BranchToPatchPre(this_offset);
|
||||||
|
} else {
|
||||||
|
this->BranchToPatch(this_offset);
|
||||||
|
}
|
||||||
|
|
||||||
// Relocate from patch to next instruction.
|
// Relocate from patch to next instruction.
|
||||||
return next_offset;
|
return next_offset;
|
||||||
@@ -98,7 +127,13 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
|
|
||||||
// SVC
|
// SVC
|
||||||
if (auto svc = SVC{inst}; svc.Verify()) {
|
if (auto svc = SVC{inst}; svc.Verify()) {
|
||||||
WriteSvcTrampoline(AddRelocations(), svc.GetValue());
|
bool pre_buffer = false;
|
||||||
|
auto ret = AddRelocations(pre_buffer);
|
||||||
|
if (pre_buffer) {
|
||||||
|
WriteSvcTrampoline(ret, svc.GetValue(), c_pre, m_save_context_pre, m_load_context_pre);
|
||||||
|
} else {
|
||||||
|
WriteSvcTrampoline(ret, svc.GetValue(), c, m_save_context, m_load_context);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,13 +144,25 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
const auto src_reg = mrs.GetSystemReg() == TpidrroEl0 ? oaknut::SystemReg::TPIDRRO_EL0
|
const auto src_reg = mrs.GetSystemReg() == TpidrroEl0 ? oaknut::SystemReg::TPIDRRO_EL0
|
||||||
: oaknut::SystemReg::TPIDR_EL0;
|
: oaknut::SystemReg::TPIDR_EL0;
|
||||||
const auto dest_reg = oaknut::XReg{static_cast<int>(mrs.GetRt())};
|
const auto dest_reg = oaknut::XReg{static_cast<int>(mrs.GetRt())};
|
||||||
WriteMrsHandler(AddRelocations(), dest_reg, src_reg);
|
bool pre_buffer = false;
|
||||||
|
auto ret = AddRelocations(pre_buffer);
|
||||||
|
if (pre_buffer) {
|
||||||
|
WriteMrsHandler(ret, dest_reg, src_reg, c_pre);
|
||||||
|
} else {
|
||||||
|
WriteMrsHandler(ret, dest_reg, src_reg, c);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MRS Xn, CNTPCT_EL0
|
// MRS Xn, CNTPCT_EL0
|
||||||
if (auto mrs = MRS{inst}; mrs.Verify() && mrs.GetSystemReg() == CntpctEl0) {
|
if (auto mrs = MRS{inst}; mrs.Verify() && mrs.GetSystemReg() == CntpctEl0) {
|
||||||
WriteCntpctHandler(AddRelocations(), oaknut::XReg{static_cast<int>(mrs.GetRt())});
|
bool pre_buffer = false;
|
||||||
|
auto ret = AddRelocations(pre_buffer);
|
||||||
|
if (pre_buffer) {
|
||||||
|
WriteCntpctHandler(ret, oaknut::XReg{static_cast<int>(mrs.GetRt())}, c_pre);
|
||||||
|
} else {
|
||||||
|
WriteCntpctHandler(ret, oaknut::XReg{static_cast<int>(mrs.GetRt())}, c);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +173,13 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
|
|
||||||
// MSR TPIDR_EL0, Xn
|
// MSR TPIDR_EL0, Xn
|
||||||
if (auto msr = MSR{inst}; msr.Verify() && msr.GetSystemReg() == TpidrEl0) {
|
if (auto msr = MSR{inst}; msr.Verify() && msr.GetSystemReg() == TpidrEl0) {
|
||||||
WriteMsrHandler(AddRelocations(), oaknut::XReg{static_cast<int>(msr.GetRt())});
|
bool pre_buffer = false;
|
||||||
|
auto ret = AddRelocations(pre_buffer);
|
||||||
|
if (pre_buffer) {
|
||||||
|
WriteMsrHandler(ret, oaknut::XReg{static_cast<int>(msr.GetRt())}, c_pre);
|
||||||
|
} else {
|
||||||
|
WriteMsrHandler(ret, oaknut::XReg{static_cast<int>(msr.GetRt())}, c);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +190,11 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
|
|||||||
|
|
||||||
// Determine patching mode for the final relocation step
|
// Determine patching mode for the final relocation step
|
||||||
total_program_size += image_size;
|
total_program_size += image_size;
|
||||||
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
|
if (use_split) {
|
||||||
|
this->mode = PatchMode::Split;
|
||||||
|
} else {
|
||||||
|
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +203,9 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
|
|||||||
Kernel::PhysicalMemory& program_image,
|
Kernel::PhysicalMemory& program_image,
|
||||||
EntryTrampolines* out_trampolines) {
|
EntryTrampolines* out_trampolines) {
|
||||||
const size_t patch_size = GetSectionSize();
|
const size_t patch_size = GetSectionSize();
|
||||||
const size_t image_size = program_image.size();
|
const size_t pre_patch_size = GetPreSectionSize();
|
||||||
|
|
||||||
|
const size_t image_size = (mode == PatchMode::Split) ? program_image.size() - pre_patch_size : program_image.size();
|
||||||
|
|
||||||
// Retrieve text segment data.
|
// Retrieve text segment data.
|
||||||
const auto text = std::span{program_image}.subspan(code.offset, code.size);
|
const auto text = std::span{program_image}.subspan(code.offset, code.size);
|
||||||
@@ -162,6 +221,16 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const auto ApplyBranchToPatchRelocationPre = [&](u32* target, const Relocation& rel) {
|
||||||
|
oaknut::CodeGenerator rc{target};
|
||||||
|
rc.B(static_cast<ptrdiff_t>(rel.patch_offset) - static_cast<ptrdiff_t>(pre_patch_size) - static_cast<ptrdiff_t>(rel.module_offset));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto ApplyBranchToPatchRelocationPostSplit = [&](u32* target, const Relocation& rel) {
|
||||||
|
oaknut::CodeGenerator rc{target};
|
||||||
|
rc.B(static_cast<ptrdiff_t>(image_size) + static_cast<ptrdiff_t>(rel.patch_offset) - static_cast<ptrdiff_t>(rel.module_offset));
|
||||||
|
};
|
||||||
|
|
||||||
const auto ApplyBranchToModuleRelocation = [&](u32* target, const Relocation& rel) {
|
const auto ApplyBranchToModuleRelocation = [&](u32* target, const Relocation& rel) {
|
||||||
oaknut::CodeGenerator rc{target};
|
oaknut::CodeGenerator rc{target};
|
||||||
if (mode == PatchMode::PreText) {
|
if (mode == PatchMode::PreText) {
|
||||||
@@ -171,6 +240,16 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const auto ApplyBranchToModuleRelocationPre = [&](u32* target, const Relocation& rel) {
|
||||||
|
oaknut::CodeGenerator rc{target};
|
||||||
|
rc.B(static_cast<ptrdiff_t>(pre_patch_size) + static_cast<ptrdiff_t>(rel.module_offset) - static_cast<ptrdiff_t>(rel.patch_offset));
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto ApplyBranchToModuleRelocationPostSplit = [&](u32* target, const Relocation& rel) {
|
||||||
|
oaknut::CodeGenerator rc{target};
|
||||||
|
rc.B(static_cast<ptrdiff_t>(rel.module_offset) - static_cast<ptrdiff_t>(image_size) - static_cast<ptrdiff_t>(rel.patch_offset));
|
||||||
|
};
|
||||||
|
|
||||||
const auto RebasePatch = [&](ptrdiff_t patch_offset) {
|
const auto RebasePatch = [&](ptrdiff_t patch_offset) {
|
||||||
if (mode == PatchMode::PreText) {
|
if (mode == PatchMode::PreText) {
|
||||||
return GetInteger(load_base) + patch_offset;
|
return GetInteger(load_base) + patch_offset;
|
||||||
@@ -182,28 +261,87 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
|
|||||||
const auto RebasePc = [&](uintptr_t module_offset) {
|
const auto RebasePc = [&](uintptr_t module_offset) {
|
||||||
if (mode == PatchMode::PreText) {
|
if (mode == PatchMode::PreText) {
|
||||||
return GetInteger(load_base) + patch_size + module_offset;
|
return GetInteger(load_base) + patch_size + module_offset;
|
||||||
} else {
|
|
||||||
return GetInteger(load_base) + module_offset;
|
|
||||||
}
|
}
|
||||||
|
if (mode == PatchMode::Split) {
|
||||||
|
return GetInteger(load_base) + pre_patch_size + module_offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetInteger(load_base) + module_offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
// We are now ready to relocate!
|
// We are now ready to relocate!
|
||||||
auto& patch = modules[m_relocate_module_index++];
|
auto& patch = modules[m_relocate_module_index++];
|
||||||
for (const Relocation& rel : patch.m_branch_to_patch_relocations) {
|
|
||||||
ApplyBranchToPatchRelocation(text_words.data() + rel.module_offset / sizeof(u32), rel);
|
if (mode == PatchMode::Split) {
|
||||||
}
|
for (const Relocation& rel : patch.m_branch_to_pre_patch_relocations) {
|
||||||
for (const Relocation& rel : patch.m_branch_to_module_relocations) {
|
ApplyBranchToPatchRelocationPre(text_words.data() + rel.module_offset / sizeof(u32), rel);
|
||||||
ApplyBranchToModuleRelocation(m_patch_instructions.data() + rel.patch_offset / sizeof(u32),
|
}
|
||||||
rel);
|
LOG_DEBUG(Core_ARM, "applied Pre: {}", patch.m_branch_to_pre_patch_relocations.size());
|
||||||
|
|
||||||
|
for (const Relocation& rel : patch.m_branch_to_patch_relocations) {
|
||||||
|
ApplyBranchToPatchRelocationPostSplit(text_words.data() + rel.module_offset / sizeof(u32), rel);
|
||||||
|
}
|
||||||
|
LOG_DEBUG(Core_ARM, "applied Post: {}", patch.m_branch_to_patch_relocations.size());
|
||||||
|
|
||||||
|
for (const Relocation& rel : patch.m_branch_to_module_relocations_pre) {
|
||||||
|
ApplyBranchToModuleRelocationPre(m_patch_instructions_pre.data() + rel.patch_offset / sizeof(u32), rel);
|
||||||
|
}
|
||||||
|
LOG_DEBUG(Core_ARM, "aplied Pre-module {}", patch.m_branch_to_module_relocations_pre.size());
|
||||||
|
|
||||||
|
for (const Relocation& rel : patch.m_branch_to_module_relocations) {
|
||||||
|
ApplyBranchToModuleRelocationPostSplit(m_patch_instructions.data() + rel.patch_offset / sizeof(u32), rel);
|
||||||
|
}
|
||||||
|
LOG_DEBUG(Core_ARM, "applied Post-module {}", patch.m_branch_to_module_relocations.size());
|
||||||
|
|
||||||
|
// Pre
|
||||||
|
for (const Relocation& rel : patch.m_write_module_pc_relocations_pre) {
|
||||||
|
oaknut::CodeGenerator rc{m_patch_instructions_pre.data() + rel.patch_offset / sizeof(u32)};
|
||||||
|
rc.dx(RebasePc(rel.module_offset));
|
||||||
|
}
|
||||||
|
// Post
|
||||||
|
for (const Relocation& rel : patch.m_write_module_pc_relocations) {
|
||||||
|
oaknut::CodeGenerator rc{m_patch_instructions.data() + rel.patch_offset / sizeof(u32)};
|
||||||
|
rc.dx(RebasePc(rel.module_offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trampolines (split pre + post)
|
||||||
|
for (const Trampoline& rel : patch.m_trampolines_pre) {
|
||||||
|
out_trampolines->insert({RebasePc(rel.module_offset),
|
||||||
|
GetInteger(load_base) + rel.patch_offset});
|
||||||
|
}
|
||||||
|
for (const Trampoline& rel : patch.m_trampolines) {
|
||||||
|
out_trampolines->insert({RebasePc(rel.module_offset),
|
||||||
|
GetInteger(load_base) + pre_patch_size + image_size + rel.patch_offset});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_patch_instructions_pre.empty()) {
|
||||||
|
u32 insn = m_patch_instructions_pre[0];
|
||||||
|
if ((insn & 0xFC000000) == 0x14000000) {
|
||||||
|
s32 imm26 = insn & 0x3FFFFFF;
|
||||||
|
// Sign extend
|
||||||
|
if (imm26 & 0x2000000) imm26 |= 0xFC000000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const Relocation& rel : patch.m_branch_to_patch_relocations) {
|
||||||
|
ApplyBranchToPatchRelocation(text_words.data() + rel.module_offset / sizeof(u32), rel);
|
||||||
|
}
|
||||||
|
for (const Relocation& rel : patch.m_branch_to_module_relocations) {
|
||||||
|
ApplyBranchToModuleRelocation(m_patch_instructions.data() + rel.patch_offset / sizeof(u32),
|
||||||
|
rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite PC constants
|
||||||
|
for (const Relocation& rel : patch.m_write_module_pc_relocations) {
|
||||||
|
oaknut::CodeGenerator rc{m_patch_instructions.data() + rel.patch_offset / sizeof(u32)};
|
||||||
|
rc.dx(RebasePc(rel.module_offset));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite PC constants and record post trampolines
|
if (mode != PatchMode::Split) {
|
||||||
for (const Relocation& rel : patch.m_write_module_pc_relocations) {
|
for (const Trampoline& rel : patch.m_trampolines) {
|
||||||
oaknut::CodeGenerator rc{m_patch_instructions.data() + rel.patch_offset / sizeof(u32)};
|
out_trampolines->insert({RebasePc(rel.module_offset), RebasePatch(rel.patch_offset)});
|
||||||
rc.dx(RebasePc(rel.module_offset));
|
}
|
||||||
}
|
|
||||||
for (const Trampoline& rel : patch.m_trampolines) {
|
|
||||||
out_trampolines->insert({RebasePc(rel.module_offset), RebasePatch(rel.patch_offset)});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cortex-A57 seems to treat all exclusives as ordered, but newer processors do not.
|
// Cortex-A57 seems to treat all exclusives as ordered, but newer processors do not.
|
||||||
@@ -223,6 +361,15 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
|
|||||||
ASSERT(image_size == total_program_size);
|
ASSERT(image_size == total_program_size);
|
||||||
std::memcpy(program_image.data(), m_patch_instructions.data(),
|
std::memcpy(program_image.data(), m_patch_instructions.data(),
|
||||||
m_patch_instructions.size() * sizeof(u32));
|
m_patch_instructions.size() * sizeof(u32));
|
||||||
|
} else if (this->mode == PatchMode::Split) {
|
||||||
|
const size_t current_size = program_image.size();
|
||||||
|
program_image.resize(current_size + patch_size);
|
||||||
|
// Copy pre-patch buffer to the beginning
|
||||||
|
std::memcpy(program_image.data(), m_patch_instructions_pre.data(),
|
||||||
|
m_patch_instructions_pre.size() * sizeof(u32));
|
||||||
|
// Same for post-patch buffer to the end
|
||||||
|
std::memcpy(program_image.data() + current_size, m_patch_instructions.data(),
|
||||||
|
m_patch_instructions.size() * sizeof(u32));
|
||||||
} else {
|
} else {
|
||||||
program_image.resize(image_size + patch_size);
|
program_image.resize(image_size + patch_size);
|
||||||
std::memcpy(program_image.data() + image_size, m_patch_instructions.data(),
|
std::memcpy(program_image.data() + image_size, m_patch_instructions.data(),
|
||||||
@@ -238,202 +385,225 @@ size_t Patcher::GetSectionSize() const noexcept {
|
|||||||
return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
|
return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::WriteLoadContext() {
|
size_t Patcher::GetPreSectionSize() const noexcept {
|
||||||
|
return Common::AlignUp(m_patch_instructions_pre.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patcher::WriteLoadContext(oaknut::VectorCodeGenerator& cg) {
|
||||||
// This function was called, which modifies X30, so use that as a scratch register.
|
// This function was called, which modifies X30, so use that as a scratch register.
|
||||||
// SP contains the guest X30, so save our return X30 to SP + 8, since we have allocated 16 bytes
|
// SP contains the guest X30, so save our return X30 to SP + 8, since we have allocated 16 bytes
|
||||||
// of stack.
|
// of stack.
|
||||||
c.STR(X30, SP, 8);
|
cg.STR(X30, SP, 8);
|
||||||
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
|
cg.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
|
||||||
// Load system registers.
|
// Load system registers.
|
||||||
c.LDR(W0, X30, offsetof(GuestContext, fpsr));
|
cg.LDR(W0, X30, offsetof(GuestContext, fpsr));
|
||||||
c.MSR(oaknut::SystemReg::FPSR, X0);
|
cg.MSR(oaknut::SystemReg::FPSR, X0);
|
||||||
c.LDR(W0, X30, offsetof(GuestContext, fpcr));
|
cg.LDR(W0, X30, offsetof(GuestContext, fpcr));
|
||||||
c.MSR(oaknut::SystemReg::FPCR, X0);
|
cg.MSR(oaknut::SystemReg::FPCR, X0);
|
||||||
c.LDR(W0, X30, offsetof(GuestContext, nzcv));
|
cg.LDR(W0, X30, offsetof(GuestContext, nzcv));
|
||||||
c.MSR(oaknut::SystemReg::NZCV, X0);
|
cg.MSR(oaknut::SystemReg::NZCV, X0);
|
||||||
|
|
||||||
// Load all vector registers.
|
// Load all vector registers.
|
||||||
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
|
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
|
||||||
for (int i = 0; i <= 30; i += 2) {
|
for (int i = 0; i <= 30; i += 2) {
|
||||||
c.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
|
cg.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load all general-purpose registers except X30.
|
// Load all general-purpose registers except X30.
|
||||||
for (int i = 0; i <= 28; i += 2) {
|
for (int i = 0; i <= 28; i += 2) {
|
||||||
c.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
|
cg.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload our return X30 from the stack and return.
|
// Reload our return X30 from the stack and return.
|
||||||
// The patch code will reload the guest X30 for us.
|
// The patch code will reload the guest X30 for us.
|
||||||
c.LDR(X30, SP, 8);
|
cg.LDR(X30, SP, 8);
|
||||||
c.RET();
|
cg.RET();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::WriteSaveContext() {
|
void Patcher::WriteSaveContext(oaknut::VectorCodeGenerator& cg) {
|
||||||
// This function was called, which modifies X30, so use that as a scratch register.
|
// This function was called, which modifies X30, so use that as a scratch register.
|
||||||
// SP contains the guest X30, so save our X30 to SP + 8, since we have allocated 16 bytes of
|
// SP contains the guest X30, so save our X30 to SP + 8, since we have allocated 16 bytes of
|
||||||
// stack.
|
// stack.
|
||||||
c.STR(X30, SP, 8);
|
cg.STR(X30, SP, 8);
|
||||||
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
|
cg.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
|
||||||
|
|
||||||
// Store all general-purpose registers except X30.
|
// Store all general-purpose registers except X30.
|
||||||
for (int i = 0; i <= 28; i += 2) {
|
for (int i = 0; i <= 28; i += 2) {
|
||||||
c.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
|
cg.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store all vector registers.
|
// Store all vector registers.
|
||||||
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
|
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
|
||||||
for (int i = 0; i <= 30; i += 2) {
|
for (int i = 0; i <= 30; i += 2) {
|
||||||
c.STP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
|
cg.STP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store guest system registers, X30 and SP, using X0 as a scratch register.
|
// Store guest system registers, X30 and SP, using X0 as a scratch register.
|
||||||
c.STR(X0, SP, PRE_INDEXED, -16);
|
cg.STR(X0, SP, PRE_INDEXED, -16);
|
||||||
c.LDR(X0, SP, 16);
|
cg.LDR(X0, SP, 16);
|
||||||
c.STR(X0, X30, 8 * 30);
|
cg.STR(X0, X30, 8 * 30);
|
||||||
c.ADD(X0, SP, 32);
|
cg.ADD(X0, SP, 32);
|
||||||
c.STR(X0, X30, offsetof(GuestContext, sp));
|
cg.STR(X0, X30, offsetof(GuestContext, sp));
|
||||||
c.MRS(X0, oaknut::SystemReg::FPSR);
|
cg.MRS(X0, oaknut::SystemReg::FPSR);
|
||||||
c.STR(W0, X30, offsetof(GuestContext, fpsr));
|
cg.STR(W0, X30, offsetof(GuestContext, fpsr));
|
||||||
c.MRS(X0, oaknut::SystemReg::FPCR);
|
cg.MRS(X0, oaknut::SystemReg::FPCR);
|
||||||
c.STR(W0, X30, offsetof(GuestContext, fpcr));
|
cg.STR(W0, X30, offsetof(GuestContext, fpcr));
|
||||||
c.MRS(X0, oaknut::SystemReg::NZCV);
|
cg.MRS(X0, oaknut::SystemReg::NZCV);
|
||||||
c.STR(W0, X30, offsetof(GuestContext, nzcv));
|
cg.STR(W0, X30, offsetof(GuestContext, nzcv));
|
||||||
c.LDR(X0, SP, POST_INDEXED, 16);
|
cg.LDR(X0, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
// Reload our return X30 from the stack, and return.
|
// Reload our return X30 from the stack, and return.
|
||||||
c.LDR(X30, SP, 8);
|
cg.LDR(X30, SP, 8);
|
||||||
c.RET();
|
cg.RET();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id) {
|
void Patcher::WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id, oaknut::VectorCodeGenerator& cg, oaknut::Label& save_ctx, oaknut::Label& load_ctx) {
|
||||||
|
// Determine if we're writing to the pre-patch buffer
|
||||||
|
const bool is_pre = (&cg == &c_pre);
|
||||||
|
|
||||||
// We are about to start saving state, so we need to lock the context.
|
// We are about to start saving state, so we need to lock the context.
|
||||||
this->LockContext();
|
this->LockContext(cg);
|
||||||
|
|
||||||
// Store guest X30 to the stack. Then, save the context and restore the stack.
|
// Store guest X30 to the stack. Then, save the context and restore the stack.
|
||||||
// This will save all registers except PC, but we know PC at patch time.
|
// This will save all registers except PC, but we know PC at patch time.
|
||||||
c.STR(X30, SP, PRE_INDEXED, -16);
|
cg.STR(X30, SP, PRE_INDEXED, -16);
|
||||||
c.BL(m_save_context);
|
cg.BL(save_ctx);
|
||||||
c.LDR(X30, SP, POST_INDEXED, 16);
|
cg.LDR(X30, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
// Now that we've saved all registers, we can use any registers as scratch.
|
// Now that we've saved all registers, we can use any registers as scratch.
|
||||||
// Store PC + 4 to arm interface, since we know the instruction offset from the entry point.
|
// Store PC + 4 to arm interface, since we know the instruction offset from the entry point.
|
||||||
oaknut::Label pc_after_svc;
|
oaknut::Label pc_after_svc;
|
||||||
c.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
|
cg.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
|
||||||
c.LDR(X2, pc_after_svc);
|
cg.LDR(X2, pc_after_svc);
|
||||||
c.STR(X2, X1, offsetof(GuestContext, pc));
|
cg.STR(X2, X1, offsetof(GuestContext, pc));
|
||||||
|
|
||||||
// Store SVC number to execute when we return
|
// Store SVC number to execute when we return
|
||||||
c.MOV(X2, svc_id);
|
cg.MOV(X2, svc_id);
|
||||||
c.STR(W2, X1, offsetof(GuestContext, svc));
|
cg.STR(W2, X1, offsetof(GuestContext, svc));
|
||||||
|
|
||||||
// We are calling a SVC. Clear esr_el1 and return it.
|
// We are calling a SVC. Clear esr_el1 and return it.
|
||||||
static_assert(std::is_same_v<std::underlying_type_t<HaltReason>, u64>);
|
static_assert(std::is_same_v<std::underlying_type_t<HaltReason>, u64>);
|
||||||
oaknut::Label retry;
|
oaknut::Label retry;
|
||||||
c.ADD(X2, X1, offsetof(GuestContext, esr_el1));
|
cg.ADD(X2, X1, offsetof(GuestContext, esr_el1));
|
||||||
c.l(retry);
|
cg.l(retry);
|
||||||
c.LDAXR(X0, X2);
|
cg.LDAXR(X0, X2);
|
||||||
c.STLXR(W3, XZR, X2);
|
cg.STLXR(W3, XZR, X2);
|
||||||
c.CBNZ(W3, retry);
|
cg.CBNZ(W3, retry);
|
||||||
|
|
||||||
// Add "calling SVC" flag. Since this is X0, this is now our return value.
|
// Add "calling SVC" flag. Since this is X0, this is now our return value.
|
||||||
c.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
|
cg.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
|
||||||
|
|
||||||
// Offset the GuestContext pointer to the HostContext member.
|
// Offset the GuestContext pointer to the HostContext member.
|
||||||
// STP has limited range of [-512, 504] which we can't reach otherwise
|
// STP has limited range of [-512, 504] which we can't reach otherwise
|
||||||
// NB: Due to this all offsets below are from the start of HostContext.
|
// NB: Due to this all offsets below are from the start of HostContext.
|
||||||
c.ADD(X1, X1, offsetof(GuestContext, host_ctx));
|
cg.ADD(X1, X1, offsetof(GuestContext, host_ctx));
|
||||||
|
|
||||||
// Reload host TPIDR_EL0 and SP.
|
// Reload host TPIDR_EL0 and SP.
|
||||||
static_assert(offsetof(HostContext, host_sp) + 8 == offsetof(HostContext, host_tpidr_el0));
|
static_assert(offsetof(HostContext, host_sp) + 8 == offsetof(HostContext, host_tpidr_el0));
|
||||||
c.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
|
cg.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
|
||||||
c.MOV(SP, X2);
|
cg.MOV(SP, X2);
|
||||||
c.MSR(oaknut::SystemReg::TPIDR_EL0, X3);
|
cg.MSR(oaknut::SystemReg::TPIDR_EL0, X3);
|
||||||
|
|
||||||
// Load callee-saved host registers and return to host.
|
// Load callee-saved host registers and return to host.
|
||||||
static constexpr size_t HOST_REGS_OFF = offsetof(HostContext, host_saved_regs);
|
static constexpr size_t HOST_REGS_OFF = offsetof(HostContext, host_saved_regs);
|
||||||
static constexpr size_t HOST_VREGS_OFF = offsetof(HostContext, host_saved_vregs);
|
static constexpr size_t HOST_VREGS_OFF = offsetof(HostContext, host_saved_vregs);
|
||||||
c.LDP(X19, X20, X1, HOST_REGS_OFF);
|
cg.LDP(X19, X20, X1, HOST_REGS_OFF);
|
||||||
c.LDP(X21, X22, X1, HOST_REGS_OFF + 2 * sizeof(u64));
|
cg.LDP(X21, X22, X1, HOST_REGS_OFF + 2 * sizeof(u64));
|
||||||
c.LDP(X23, X24, X1, HOST_REGS_OFF + 4 * sizeof(u64));
|
cg.LDP(X23, X24, X1, HOST_REGS_OFF + 4 * sizeof(u64));
|
||||||
c.LDP(X25, X26, X1, HOST_REGS_OFF + 6 * sizeof(u64));
|
cg.LDP(X25, X26, X1, HOST_REGS_OFF + 6 * sizeof(u64));
|
||||||
c.LDP(X27, X28, X1, HOST_REGS_OFF + 8 * sizeof(u64));
|
cg.LDP(X27, X28, X1, HOST_REGS_OFF + 8 * sizeof(u64));
|
||||||
c.LDP(X29, X30, X1, HOST_REGS_OFF + 10 * sizeof(u64));
|
cg.LDP(X29, X30, X1, HOST_REGS_OFF + 10 * sizeof(u64));
|
||||||
c.LDP(Q8, Q9, X1, HOST_VREGS_OFF);
|
cg.LDP(Q8, Q9, X1, HOST_VREGS_OFF);
|
||||||
c.LDP(Q10, Q11, X1, HOST_VREGS_OFF + 2 * sizeof(u128));
|
cg.LDP(Q10, Q11, X1, HOST_VREGS_OFF + 2 * sizeof(u128));
|
||||||
c.LDP(Q12, Q13, X1, HOST_VREGS_OFF + 4 * sizeof(u128));
|
cg.LDP(Q12, Q13, X1, HOST_VREGS_OFF + 4 * sizeof(u128));
|
||||||
c.LDP(Q14, Q15, X1, HOST_VREGS_OFF + 6 * sizeof(u128));
|
cg.LDP(Q14, Q15, X1, HOST_VREGS_OFF + 6 * sizeof(u128));
|
||||||
c.RET();
|
cg.RET();
|
||||||
|
|
||||||
// Write the post-SVC trampoline address, which will jump back to the guest after restoring its
|
// Write the post-SVC trampoline address, which will jump back to the guest after restoring its
|
||||||
// state.
|
// state.
|
||||||
curr_patch->m_trampolines.push_back({c.offset(), module_dest});
|
if (is_pre) {
|
||||||
|
curr_patch->m_trampolines_pre.push_back({cg.offset(), module_dest});
|
||||||
|
} else {
|
||||||
|
curr_patch->m_trampolines.push_back({cg.offset(), module_dest});
|
||||||
|
}
|
||||||
|
|
||||||
// Host called this location. Save the return address so we can
|
// Host called this location. Save the return address so we can
|
||||||
// unwind the stack properly when jumping back.
|
// unwind the stack properly when jumping back.
|
||||||
c.MRS(X2, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X2, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.LDR(X2, X2, offsetof(NativeExecutionParameters, native_context));
|
cg.LDR(X2, X2, offsetof(NativeExecutionParameters, native_context));
|
||||||
c.ADD(X0, X2, offsetof(GuestContext, host_ctx));
|
cg.ADD(X0, X2, offsetof(GuestContext, host_ctx));
|
||||||
c.STR(X30, X0, offsetof(HostContext, host_saved_regs) + 11 * sizeof(u64));
|
cg.STR(X30, X0, offsetof(HostContext, host_saved_regs) + 11 * sizeof(u64));
|
||||||
|
|
||||||
// Reload all guest registers except X30 and PC.
|
// Reload all guest registers except X30 and PC.
|
||||||
// The function also expects 16 bytes of stack already allocated.
|
// The function also expects 16 bytes of stack already allocated.
|
||||||
c.STR(X30, SP, PRE_INDEXED, -16);
|
cg.STR(X30, SP, PRE_INDEXED, -16);
|
||||||
c.BL(m_load_context);
|
cg.BL(load_ctx);
|
||||||
c.LDR(X30, SP, POST_INDEXED, 16);
|
cg.LDR(X30, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
// Use X1 as a scratch register to restore X30.
|
// Use X1 as a scratch register to restore X30.
|
||||||
c.STR(X1, SP, PRE_INDEXED, -16);
|
cg.STR(X1, SP, PRE_INDEXED, -16);
|
||||||
c.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
|
cg.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
|
||||||
c.LDR(X30, X1, offsetof(GuestContext, cpu_registers) + sizeof(u64) * 30);
|
cg.LDR(X30, X1, offsetof(GuestContext, cpu_registers) + sizeof(u64) * 30);
|
||||||
c.LDR(X1, SP, POST_INDEXED, 16);
|
cg.LDR(X1, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
// Unlock the context.
|
// Unlock the context.
|
||||||
this->UnlockContext();
|
this->UnlockContext(cg);
|
||||||
|
|
||||||
// Jump back to the instruction after the emulated SVC.
|
// Jump back to the instruction after the emulated SVC.
|
||||||
this->BranchToModule(module_dest);
|
if (&cg == &c_pre)
|
||||||
|
this->BranchToModulePre(module_dest);
|
||||||
|
else
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
|
|
||||||
// Store PC after call.
|
// Store PC after call.
|
||||||
c.l(pc_after_svc);
|
cg.l(pc_after_svc);
|
||||||
this->WriteModulePc(module_dest);
|
if (&cg == &c_pre)
|
||||||
|
this->WriteModulePcPre(module_dest);
|
||||||
|
else
|
||||||
|
this->WriteModulePc(module_dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
|
void Patcher::WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
|
||||||
oaknut::SystemReg src_reg) {
|
oaknut::SystemReg src_reg, oaknut::VectorCodeGenerator& cg) {
|
||||||
// Retrieve emulated TLS register from GuestContext.
|
// Retrieve emulated TLS register from GuestContext.
|
||||||
c.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
|
||||||
if (src_reg == oaknut::SystemReg::TPIDRRO_EL0) {
|
if (src_reg == oaknut::SystemReg::TPIDRRO_EL0) {
|
||||||
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
|
cg.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
|
||||||
} else {
|
} else {
|
||||||
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
|
cg.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jump back to the instruction after the emulated MRS.
|
// Jump back to the instruction after the emulated MRS.
|
||||||
this->BranchToModule(module_dest);
|
if (&cg == &c_pre)
|
||||||
|
this->BranchToModulePre(module_dest);
|
||||||
|
else
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg) {
|
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg, oaknut::VectorCodeGenerator& cg) {
|
||||||
const auto scratch_reg = src_reg.index() == 0 ? X1 : X0;
|
const auto scratch_reg = src_reg.index() == 0 ? X1 : X0;
|
||||||
c.STR(scratch_reg, SP, PRE_INDEXED, -16);
|
cg.STR(scratch_reg, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
// Save guest value to NativeExecutionParameters::tpidr_el0.
|
// Save guest value to NativeExecutionParameters::tpidr_el0.
|
||||||
c.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
|
cg.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
|
||||||
|
|
||||||
// Restore scratch register.
|
// Restore scratch register.
|
||||||
c.LDR(scratch_reg, SP, POST_INDEXED, 16);
|
cg.LDR(scratch_reg, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
// Jump back to the instruction after the emulated MSR.
|
// Jump back to the instruction after the emulated MSR.
|
||||||
this->BranchToModule(module_dest);
|
if (&cg == &c_pre)
|
||||||
|
this->BranchToModulePre(module_dest);
|
||||||
|
else
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg) {
|
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::VectorCodeGenerator& cg) {
|
||||||
static Common::Arm64::NativeClock clock{};
|
static Common::Arm64::NativeClock clock{};
|
||||||
const auto factor = clock.GetGuestCNTFRQFactor();
|
const auto factor = clock.GetGuestCNTFRQFactor();
|
||||||
const auto raw_factor = std::bit_cast<std::array<u64, 2>>(factor);
|
const auto raw_factor = std::bit_cast<std::array<u64, 2>>(factor);
|
||||||
@@ -446,80 +616,83 @@ void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_
|
|||||||
oaknut::Label factorhi;
|
oaknut::Label factorhi;
|
||||||
|
|
||||||
// Save scratches.
|
// Save scratches.
|
||||||
c.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
|
cg.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
// Load counter value.
|
// Load counter value.
|
||||||
c.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
|
cg.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
|
||||||
|
|
||||||
// Load scaling factor.
|
// Load scaling factor.
|
||||||
c.LDR(scratch0, factorlo);
|
cg.LDR(scratch0, factorlo);
|
||||||
c.LDR(scratch1, factorhi);
|
cg.LDR(scratch1, factorhi);
|
||||||
|
|
||||||
// Multiply low bits and get result.
|
// Multiply low bits and get result.
|
||||||
c.UMULH(scratch0, dest_reg, scratch0);
|
cg.UMULH(scratch0, dest_reg, scratch0);
|
||||||
|
|
||||||
// Multiply high bits and add low bit result.
|
// Multiply high bits and add low bit result.
|
||||||
c.MADD(dest_reg, dest_reg, scratch1, scratch0);
|
cg.MADD(dest_reg, dest_reg, scratch1, scratch0);
|
||||||
|
|
||||||
// Reload scratches.
|
// Reload scratches.
|
||||||
c.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
|
cg.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
|
||||||
|
|
||||||
// Jump back to the instruction after the emulated MRS.
|
// Jump back to the instruction after the emulated MRS.
|
||||||
this->BranchToModule(module_dest);
|
if (&cg == &c_pre)
|
||||||
|
this->BranchToModulePre(module_dest);
|
||||||
|
else
|
||||||
|
this->BranchToModule(module_dest);
|
||||||
|
|
||||||
// Scaling factor constant values.
|
// Scaling factor constant values.
|
||||||
c.l(factorlo);
|
cg.l(factorlo);
|
||||||
c.dx(raw_factor[0]);
|
cg.dx(raw_factor[0]);
|
||||||
c.l(factorhi);
|
cg.l(factorhi);
|
||||||
c.dx(raw_factor[1]);
|
cg.dx(raw_factor[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::LockContext() {
|
void Patcher::LockContext(oaknut::VectorCodeGenerator& cg) {
|
||||||
oaknut::Label retry;
|
oaknut::Label retry;
|
||||||
|
|
||||||
// Save scratches.
|
// Save scratches.
|
||||||
c.STP(X0, X1, SP, PRE_INDEXED, -16);
|
cg.STP(X0, X1, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
// Reload lock pointer.
|
// Reload lock pointer.
|
||||||
c.l(retry);
|
cg.l(retry);
|
||||||
c.CLREX();
|
cg.CLREX();
|
||||||
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
|
cg.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
|
||||||
|
|
||||||
static_assert(SpinLockLocked == 0);
|
static_assert(SpinLockLocked == 0);
|
||||||
|
|
||||||
// Load-linked with acquire ordering.
|
// Load-linked with acquire ordering.
|
||||||
c.LDAXR(W1, X0);
|
cg.LDAXR(W1, X0);
|
||||||
|
|
||||||
// If the value was SpinLockLocked, clear monitor and retry.
|
// If the value was SpinLockLocked, clear monitor and retry.
|
||||||
c.CBZ(W1, retry);
|
cg.CBZ(W1, retry);
|
||||||
|
|
||||||
// Store-conditional SpinLockLocked with relaxed ordering.
|
// Store-conditional SpinLockLocked with relaxed ordering.
|
||||||
c.STXR(W1, WZR, X0);
|
cg.STXR(W1, WZR, X0);
|
||||||
|
|
||||||
// If we failed to store, retry.
|
// If we failed to store, retry.
|
||||||
c.CBNZ(W1, retry);
|
cg.CBNZ(W1, retry);
|
||||||
|
|
||||||
// We succeeded! Reload scratches.
|
// We succeeded! Reload scratches.
|
||||||
c.LDP(X0, X1, SP, POST_INDEXED, 16);
|
cg.LDP(X0, X1, SP, POST_INDEXED, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Patcher::UnlockContext() {
|
void Patcher::UnlockContext(oaknut::VectorCodeGenerator& cg) {
|
||||||
// Save scratches.
|
// Save scratches.
|
||||||
c.STP(X0, X1, SP, PRE_INDEXED, -16);
|
cg.STP(X0, X1, SP, PRE_INDEXED, -16);
|
||||||
|
|
||||||
// Load lock pointer.
|
// Load lock pointer.
|
||||||
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
|
cg.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
|
||||||
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
|
cg.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
|
||||||
|
|
||||||
// Load SpinLockUnlocked.
|
// Load SpinLockUnlocked.
|
||||||
c.MOV(W1, SpinLockUnlocked);
|
cg.MOV(W1, SpinLockUnlocked);
|
||||||
|
|
||||||
// Store value with release ordering.
|
// Store value with release ordering.
|
||||||
c.STLR(W1, X0);
|
cg.STLR(W1, X0);
|
||||||
|
|
||||||
// Load scratches.
|
// Load scratches.
|
||||||
c.LDP(X0, X1, SP, POST_INDEXED, 16);
|
cg.LDP(X0, X1, SP, POST_INDEXED, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Core::NCE
|
} // namespace Core::NCE
|
||||||
|
|||||||
+44
-10
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -41,6 +41,7 @@ enum class PatchMode : u32 {
|
|||||||
None,
|
None,
|
||||||
PreText, ///< Patch section is inserted before .text
|
PreText, ///< Patch section is inserted before .text
|
||||||
PostData, ///< Patch section is inserted after .data
|
PostData, ///< Patch section is inserted after .data
|
||||||
|
Split, ///< Patch sections are inserted before .text and after .data
|
||||||
};
|
};
|
||||||
|
|
||||||
using ModuleTextAddress = u64;
|
using ModuleTextAddress = u64;
|
||||||
@@ -63,6 +64,7 @@ public:
|
|||||||
bool RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code,
|
bool RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code,
|
||||||
Kernel::PhysicalMemory& program_image, EntryTrampolines* out_trampolines);
|
Kernel::PhysicalMemory& program_image, EntryTrampolines* out_trampolines);
|
||||||
size_t GetSectionSize() const noexcept;
|
size_t GetSectionSize() const noexcept;
|
||||||
|
size_t GetPreSectionSize() const noexcept;
|
||||||
|
|
||||||
[[nodiscard]] PatchMode GetPatchMode() const noexcept {
|
[[nodiscard]] PatchMode GetPatchMode() const noexcept {
|
||||||
return mode;
|
return mode;
|
||||||
@@ -76,15 +78,25 @@ private:
|
|||||||
uintptr_t module_offset;
|
uintptr_t module_offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
void WriteLoadContext();
|
// Core implementations with explicit code generator
|
||||||
void WriteSaveContext();
|
void WriteLoadContext(oaknut::VectorCodeGenerator& code);
|
||||||
void LockContext();
|
void WriteSaveContext(oaknut::VectorCodeGenerator& code);
|
||||||
void UnlockContext();
|
void LockContext(oaknut::VectorCodeGenerator& code);
|
||||||
void WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id);
|
void UnlockContext(oaknut::VectorCodeGenerator& code);
|
||||||
void WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
|
void WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id, oaknut::VectorCodeGenerator& code, oaknut::Label& save_ctx, oaknut::Label& load_ctx);
|
||||||
oaknut::SystemReg src_reg);
|
void WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::SystemReg src_reg, oaknut::VectorCodeGenerator& code);
|
||||||
void WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg);
|
void WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg, oaknut::VectorCodeGenerator& code);
|
||||||
void WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg);
|
void WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::VectorCodeGenerator& code);
|
||||||
|
|
||||||
|
// Convenience wrappers using default code generator
|
||||||
|
void WriteLoadContext() { WriteLoadContext(c); }
|
||||||
|
void WriteSaveContext() { WriteSaveContext(c); }
|
||||||
|
void LockContext() { LockContext(c); }
|
||||||
|
void UnlockContext() { UnlockContext(c); }
|
||||||
|
void WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id) { WriteSvcTrampoline(module_dest, svc_id, c, m_save_context, m_load_context); }
|
||||||
|
void WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::SystemReg src_reg) { WriteMrsHandler(module_dest, dest_reg, src_reg, c); }
|
||||||
|
void WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg) { WriteMsrHandler(module_dest, src_reg, c); }
|
||||||
|
void WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg) { WriteCntpctHandler(module_dest, dest_reg, c); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr size_t CACHE_SIZE = 16384; // Cache size for patch entries
|
static constexpr size_t CACHE_SIZE = 16384; // Cache size for patch entries
|
||||||
@@ -111,19 +123,34 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BranchToPatchPre(uintptr_t module_dest) {
|
||||||
|
curr_patch->m_branch_to_pre_patch_relocations.push_back({c_pre.offset(), module_dest});
|
||||||
|
}
|
||||||
|
|
||||||
void BranchToModule(uintptr_t module_dest) {
|
void BranchToModule(uintptr_t module_dest) {
|
||||||
curr_patch->m_branch_to_module_relocations.push_back({c.offset(), module_dest});
|
curr_patch->m_branch_to_module_relocations.push_back({c.offset(), module_dest});
|
||||||
c.dw(0);
|
c.dw(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BranchToModulePre(uintptr_t module_dest) {
|
||||||
|
curr_patch->m_branch_to_module_relocations_pre.push_back({c_pre.offset(), module_dest});
|
||||||
|
c_pre.dw(0);
|
||||||
|
}
|
||||||
|
|
||||||
void WriteModulePc(uintptr_t module_dest) {
|
void WriteModulePc(uintptr_t module_dest) {
|
||||||
curr_patch->m_write_module_pc_relocations.push_back({c.offset(), module_dest});
|
curr_patch->m_write_module_pc_relocations.push_back({c.offset(), module_dest});
|
||||||
c.dx(0);
|
c.dx(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WriteModulePcPre(uintptr_t module_dest) {
|
||||||
|
curr_patch->m_write_module_pc_relocations_pre.push_back({c_pre.offset(), module_dest});
|
||||||
|
c_pre.dx(0);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// List of patch instructions we have generated.
|
// List of patch instructions we have generated.
|
||||||
std::vector<u32> m_patch_instructions{};
|
std::vector<u32> m_patch_instructions{};
|
||||||
|
std::vector<u32> m_patch_instructions_pre{};
|
||||||
|
|
||||||
// Relocation type for relative branch from module to patch.
|
// Relocation type for relative branch from module to patch.
|
||||||
struct Relocation {
|
struct Relocation {
|
||||||
@@ -133,15 +160,22 @@ private:
|
|||||||
|
|
||||||
struct ModulePatch {
|
struct ModulePatch {
|
||||||
std::vector<Trampoline> m_trampolines;
|
std::vector<Trampoline> m_trampolines;
|
||||||
|
std::vector<Trampoline> m_trampolines_pre;
|
||||||
std::vector<Relocation> m_branch_to_patch_relocations{};
|
std::vector<Relocation> m_branch_to_patch_relocations{};
|
||||||
|
std::vector<Relocation> m_branch_to_pre_patch_relocations{};
|
||||||
std::vector<Relocation> m_branch_to_module_relocations{};
|
std::vector<Relocation> m_branch_to_module_relocations{};
|
||||||
|
std::vector<Relocation> m_branch_to_module_relocations_pre{};
|
||||||
std::vector<Relocation> m_write_module_pc_relocations{};
|
std::vector<Relocation> m_write_module_pc_relocations{};
|
||||||
|
std::vector<Relocation> m_write_module_pc_relocations_pre{};
|
||||||
std::vector<ModuleTextAddress> m_exclusives{};
|
std::vector<ModuleTextAddress> m_exclusives{};
|
||||||
};
|
};
|
||||||
|
|
||||||
oaknut::VectorCodeGenerator c;
|
oaknut::VectorCodeGenerator c;
|
||||||
|
oaknut::VectorCodeGenerator c_pre;
|
||||||
oaknut::Label m_save_context{};
|
oaknut::Label m_save_context{};
|
||||||
oaknut::Label m_load_context{};
|
oaknut::Label m_load_context{};
|
||||||
|
oaknut::Label m_save_context_pre{};
|
||||||
|
oaknut::Label m_load_context_pre{};
|
||||||
PatchMode mode{PatchMode::None};
|
PatchMode mode{PatchMode::None};
|
||||||
size_t total_program_size{};
|
size_t total_program_size{};
|
||||||
size_t m_relocate_module_index{};
|
size_t m_relocate_module_index{};
|
||||||
|
|||||||
+3
-1
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -142,6 +142,7 @@ struct System::Impl {
|
|||||||
|
|
||||||
void ReinitializeIfNecessary(System& system) {
|
void ReinitializeIfNecessary(System& system) {
|
||||||
const bool must_reinitialize =
|
const bool must_reinitialize =
|
||||||
|
!device_memory.has_value() ||
|
||||||
is_multicore != Settings::values.use_multi_core.GetValue() ||
|
is_multicore != Settings::values.use_multi_core.GetValue() ||
|
||||||
extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() !=
|
extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() !=
|
||||||
Settings::MemoryLayout::Memory_4Gb);
|
Settings::MemoryLayout::Memory_4Gb);
|
||||||
@@ -414,6 +415,7 @@ struct System::Impl {
|
|||||||
cpu_manager.Shutdown();
|
cpu_manager.Shutdown();
|
||||||
debugger.reset();
|
debugger.reset();
|
||||||
kernel.Shutdown();
|
kernel.Shutdown();
|
||||||
|
device_memory.reset();
|
||||||
stop_event = {};
|
stop_event = {};
|
||||||
Network::RestartSocketOperations();
|
Network::RestartSocketOperations();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||||
@@ -13,10 +13,6 @@
|
|||||||
#include "common/windows/timer_resolution.h"
|
#include "common/windows/timer_resolution.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef ARCHITECTURE_x86_64
|
|
||||||
#include "common/x64/cpu_wait.h"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "core/core_timing.h"
|
#include "core/core_timing.h"
|
||||||
#include "core/hardware_properties.h"
|
#include "core/hardware_properties.h"
|
||||||
@@ -287,28 +283,7 @@ void CoreTiming::ThreadLoop() {
|
|||||||
if (next_time) {
|
if (next_time) {
|
||||||
// There are more events left in the queue, wait until the next event.
|
// There are more events left in the queue, wait until the next event.
|
||||||
auto wait_time = *next_time - GetGlobalTimeNs().count();
|
auto wait_time = *next_time - GetGlobalTimeNs().count();
|
||||||
if (wait_time > 0) {
|
event.WaitFor(std::chrono::nanoseconds(wait_time));
|
||||||
#ifdef _WIN32
|
|
||||||
while (!paused && !event.IsSet() && wait_time > 0) {
|
|
||||||
wait_time = *next_time - GetGlobalTimeNs().count();
|
|
||||||
if (wait_time >= timer_resolution_ns) {
|
|
||||||
Common::Windows::SleepForOneTick();
|
|
||||||
} else {
|
|
||||||
#ifdef ARCHITECTURE_x86_64
|
|
||||||
Common::X64::MicroSleep();
|
|
||||||
#else
|
|
||||||
std::this_thread::yield();
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.IsSet()) {
|
|
||||||
event.Reset();
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
event.WaitFor(std::chrono::nanoseconds(wait_time));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Queue is empty, wait until another event is scheduled and signals us to
|
// Queue is empty, wait until another event is scheduled and signals us to
|
||||||
// continue.
|
// continue.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -137,12 +137,127 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
|
|||||||
return exefs;
|
return exefs;
|
||||||
|
|
||||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
const auto update_disabled =
|
|
||||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
bool update_disabled = true;
|
||||||
|
std::optional<u32> enabled_version;
|
||||||
|
bool checked_external = false;
|
||||||
|
bool checked_manual = false;
|
||||||
|
|
||||||
|
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
|
||||||
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
|
|
||||||
|
if (content_union) {
|
||||||
|
// First, check ExternalContentProvider
|
||||||
|
const auto* external_provider = content_union->GetExternalProvider();
|
||||||
|
if (external_provider) {
|
||||||
|
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
|
||||||
|
|
||||||
|
if (!update_versions.empty()) {
|
||||||
|
checked_external = true;
|
||||||
|
for (const auto& update_entry : update_versions) {
|
||||||
|
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
||||||
|
update_disabled = false;
|
||||||
|
enabled_version = update_entry.version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check ManualContentProvider (for Android)
|
||||||
|
if (!checked_external) {
|
||||||
|
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
|
||||||
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
||||||
|
if (manual_provider) {
|
||||||
|
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
|
||||||
|
|
||||||
|
if (!manual_update_versions.empty()) {
|
||||||
|
checked_manual = true;
|
||||||
|
for (const auto& update_entry : manual_update_versions) {
|
||||||
|
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
||||||
|
update_disabled = false;
|
||||||
|
enabled_version = update_entry.version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for original NAND style
|
||||||
|
// Check NAND if: no external updates exist, OR all external updates are disabled
|
||||||
|
if (!checked_external && !checked_manual) {
|
||||||
|
// Only enable NAND update if it exists AND is not disabled
|
||||||
|
// We need to check if an update actually exists in the content provider
|
||||||
|
const bool has_nand_update = content_provider.HasEntry(update_tid, ContentRecordType::Program);
|
||||||
|
|
||||||
|
if (has_nand_update) {
|
||||||
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
||||||
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
||||||
|
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||||
|
|
||||||
|
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
|
||||||
|
update_disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (update_disabled && content_union) {
|
||||||
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
||||||
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
||||||
|
|
||||||
|
if (!nand_disabled || !sdmc_disabled) {
|
||||||
|
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
|
||||||
|
std::nullopt, TitleType::Update, ContentRecordType::Program, update_tid);
|
||||||
|
|
||||||
|
for (const auto& [slot, entry] : nand_sdmc_entries) {
|
||||||
|
if (slot == ContentProviderUnionSlot::UserNAND ||
|
||||||
|
slot == ContentProviderUnionSlot::SysNAND) {
|
||||||
|
if (!nand_disabled) {
|
||||||
|
update_disabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (slot == ContentProviderUnionSlot::SDMC) {
|
||||||
|
if (!sdmc_disabled) {
|
||||||
|
update_disabled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Game Updates
|
// Game Updates
|
||||||
const auto update_tid = GetUpdateTitleID(title_id);
|
std::unique_ptr<NCA> update = nullptr;
|
||||||
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
|
|
||||||
|
// If we have a specific enabled version from external provider, use it
|
||||||
|
if (enabled_version.has_value() && content_union) {
|
||||||
|
const auto* external_provider = content_union->GetExternalProvider();
|
||||||
|
if (external_provider) {
|
||||||
|
auto file = external_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
|
||||||
|
if (file != nullptr) {
|
||||||
|
update = std::make_unique<NCA>(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try ManualContentProvider
|
||||||
|
if (update == nullptr) {
|
||||||
|
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
|
||||||
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
||||||
|
if (manual_provider) {
|
||||||
|
auto file = manual_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
|
||||||
|
if (file != nullptr) {
|
||||||
|
update = std::make_unique<NCA>(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to regular content provider if no external update was loaded
|
||||||
|
if (update == nullptr && !update_disabled) {
|
||||||
|
update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
|
||||||
|
}
|
||||||
|
|
||||||
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
|
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
|
||||||
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
|
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
|
||||||
@@ -447,21 +562,103 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
|
|||||||
|
|
||||||
// Game Updates
|
// Game Updates
|
||||||
const auto update_tid = GetUpdateTitleID(title_id);
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
const auto update_raw = content_provider.GetEntryRaw(update_tid, type);
|
|
||||||
|
|
||||||
const auto& disabled = Settings::values.disabled_addons[title_id];
|
const auto& disabled = Settings::values.disabled_addons[title_id];
|
||||||
const auto update_disabled =
|
|
||||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
bool update_disabled = true;
|
||||||
|
std::optional<u32> enabled_version;
|
||||||
|
VirtualFile update_raw = nullptr;
|
||||||
|
bool checked_external = false;
|
||||||
|
bool checked_manual = false;
|
||||||
|
|
||||||
|
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
|
||||||
|
if (content_union) {
|
||||||
|
// First, check ExternalContentProvider
|
||||||
|
const auto* external_provider = content_union->GetExternalProvider();
|
||||||
|
if (external_provider) {
|
||||||
|
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
|
||||||
|
|
||||||
|
if (!update_versions.empty()) {
|
||||||
|
checked_external = true;
|
||||||
|
for (const auto& update_entry : update_versions) {
|
||||||
|
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
||||||
|
update_disabled = false;
|
||||||
|
enabled_version = update_entry.version;
|
||||||
|
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checked_external) {
|
||||||
|
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
|
||||||
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
||||||
|
if (manual_provider) {
|
||||||
|
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
|
||||||
|
|
||||||
|
if (!manual_update_versions.empty()) {
|
||||||
|
checked_manual = true;
|
||||||
|
for (const auto& update_entry : manual_update_versions) {
|
||||||
|
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
||||||
|
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
|
||||||
|
update_disabled = false;
|
||||||
|
enabled_version = update_entry.version;
|
||||||
|
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checked_external && !checked_manual) {
|
||||||
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
||||||
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
||||||
|
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||||
|
|
||||||
|
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
|
||||||
|
update_disabled = false;
|
||||||
|
}
|
||||||
|
if (!update_disabled) {
|
||||||
|
update_raw = content_provider.GetEntryRaw(update_tid, type);
|
||||||
|
}
|
||||||
|
} else if (update_disabled && content_union) {
|
||||||
|
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
|
||||||
|
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
|
||||||
|
|
||||||
|
if (!nand_disabled || !sdmc_disabled) {
|
||||||
|
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
|
||||||
|
std::nullopt, TitleType::Update, type, update_tid);
|
||||||
|
|
||||||
|
for (const auto& [slot, entry] : nand_sdmc_entries) {
|
||||||
|
if (slot == ContentProviderUnionSlot::UserNAND ||
|
||||||
|
slot == ContentProviderUnionSlot::SysNAND) {
|
||||||
|
if (!nand_disabled) {
|
||||||
|
update_disabled = false;
|
||||||
|
update_raw = content_provider.GetEntryRaw(update_tid, type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (slot == ContentProviderUnionSlot::SDMC) {
|
||||||
|
if (!sdmc_disabled) {
|
||||||
|
update_disabled = false;
|
||||||
|
update_raw = content_provider.GetEntryRaw(update_tid, type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
|
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
|
||||||
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
|
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
|
||||||
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
|
||||||
new_nca->GetRomFS() != nullptr) {
|
new_nca->GetRomFS() != nullptr) {
|
||||||
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
|
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
|
||||||
|
enabled_version.has_value() ? FormatTitleVersion(*enabled_version) :
|
||||||
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
|
||||||
romfs = new_nca->GetRomFS();
|
romfs = new_nca->GetRomFS();
|
||||||
const auto version =
|
|
||||||
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0));
|
|
||||||
}
|
}
|
||||||
} else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
|
} else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
|
||||||
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
|
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
|
||||||
@@ -490,34 +687,179 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
|
|
||||||
// Game Updates
|
// Game Updates
|
||||||
const auto update_tid = GetUpdateTitleID(title_id);
|
const auto update_tid = GetUpdateTitleID(title_id);
|
||||||
PatchManager update{update_tid, fs_controller, content_provider};
|
|
||||||
const auto metadata = update.GetControlMetadata();
|
|
||||||
const auto& nacp = metadata.first;
|
|
||||||
|
|
||||||
const auto update_disabled =
|
std::vector<Patch> external_update_patches;
|
||||||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
|
||||||
Patch update_patch = {.enabled = !update_disabled,
|
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
|
||||||
.name = "Update",
|
|
||||||
.version = "",
|
if (content_union) {
|
||||||
.type = PatchType::Update,
|
// First, check ExternalContentProvider for updates
|
||||||
.program_id = title_id,
|
const auto* external_provider = content_union->GetExternalProvider();
|
||||||
.title_id = title_id};
|
if (external_provider) {
|
||||||
|
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
|
||||||
|
|
||||||
|
for (const auto& update_entry : update_versions) {
|
||||||
|
std::string version_str = update_entry.version_string;
|
||||||
|
if (version_str.empty()) {
|
||||||
|
version_str = FormatTitleVersion(update_entry.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
||||||
|
const auto update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend();
|
||||||
|
|
||||||
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
|
.name = "Update",
|
||||||
|
.version = version_str,
|
||||||
|
.type = PatchType::Update,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = update_tid,
|
||||||
|
.source = PatchSource::External,
|
||||||
|
.numeric_version = update_entry.version};
|
||||||
|
|
||||||
|
external_update_patches.push_back(update_patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
|
||||||
|
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
|
||||||
|
if (manual_provider && external_update_patches.empty()) {
|
||||||
|
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
|
||||||
|
|
||||||
|
for (const auto& update_entry : manual_update_versions) {
|
||||||
|
std::string version_str = update_entry.version_string;
|
||||||
|
if (version_str.empty()) {
|
||||||
|
version_str = FormatTitleVersion(update_entry.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
|
||||||
|
const auto update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend();
|
||||||
|
|
||||||
|
|
||||||
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
|
.name = "Update",
|
||||||
|
.version = version_str,
|
||||||
|
.type = PatchType::Update,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = update_tid,
|
||||||
|
.source = PatchSource::External,
|
||||||
|
.numeric_version = update_entry.version};
|
||||||
|
|
||||||
|
external_update_patches.push_back(update_patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (external_update_patches.size() > 1) {
|
||||||
|
bool found_enabled = false;
|
||||||
|
for (auto& patch : external_update_patches) {
|
||||||
|
if (patch.enabled) {
|
||||||
|
if (found_enabled) {
|
||||||
|
patch.enabled = false;
|
||||||
|
} else {
|
||||||
|
found_enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& patch : external_update_patches) {
|
||||||
|
out.push_back(std::move(patch));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto all_updates = content_union->ListEntriesFilterOrigin(
|
||||||
|
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
|
||||||
|
|
||||||
|
for (const auto& [slot, entry] : all_updates) {
|
||||||
|
if (slot == ContentProviderUnionSlot::External) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchSource source_type = PatchSource::Unknown;
|
||||||
|
std::string source_suffix;
|
||||||
|
|
||||||
|
switch (slot) {
|
||||||
|
case ContentProviderUnionSlot::UserNAND:
|
||||||
|
case ContentProviderUnionSlot::SysNAND:
|
||||||
|
source_type = PatchSource::NAND;
|
||||||
|
source_suffix = " (NAND)";
|
||||||
|
break;
|
||||||
|
case ContentProviderUnionSlot::SDMC:
|
||||||
|
source_type = PatchSource::NAND;
|
||||||
|
source_suffix = " (SDMC)";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string version_str;
|
||||||
|
u32 numeric_ver = 0;
|
||||||
|
PatchManager update{update_tid, fs_controller, content_provider};
|
||||||
|
const auto metadata = update.GetControlMetadata();
|
||||||
|
const auto& nacp = metadata.first;
|
||||||
|
|
||||||
|
if (nacp != nullptr) {
|
||||||
|
version_str = nacp->GetVersionString();
|
||||||
|
}
|
||||||
|
|
||||||
if (nacp != nullptr) {
|
|
||||||
update_patch.version = nacp->GetVersionString();
|
|
||||||
out.push_back(update_patch);
|
|
||||||
} else {
|
|
||||||
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
|
||||||
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
||||||
if (meta_ver.value_or(0) == 0) {
|
if (meta_ver.has_value()) {
|
||||||
out.push_back(update_patch);
|
numeric_ver = *meta_ver;
|
||||||
} else {
|
if (version_str.empty() && numeric_ver != 0) {
|
||||||
update_patch.version = FormatTitleVersion(*meta_ver);
|
version_str = FormatTitleVersion(numeric_ver);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string patch_name = "Update" + source_suffix;
|
||||||
|
|
||||||
|
bool update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), patch_name) != disabled.cend();
|
||||||
|
|
||||||
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
|
.name = patch_name,
|
||||||
|
.version = version_str,
|
||||||
|
.type = PatchType::Update,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = update_tid,
|
||||||
|
.source = source_type,
|
||||||
|
.numeric_version = numeric_ver};
|
||||||
|
|
||||||
|
out.push_back(update_patch);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PatchManager update{update_tid, fs_controller, content_provider};
|
||||||
|
const auto metadata = update.GetControlMetadata();
|
||||||
|
const auto& nacp = metadata.first;
|
||||||
|
|
||||||
|
bool update_disabled =
|
||||||
|
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
|
||||||
|
Patch update_patch = {.enabled = !update_disabled,
|
||||||
|
.name = "Update",
|
||||||
|
.version = "",
|
||||||
|
.type = PatchType::Update,
|
||||||
|
.program_id = title_id,
|
||||||
|
.title_id = title_id,
|
||||||
|
.source = PatchSource::Unknown,
|
||||||
|
.numeric_version = 0};
|
||||||
|
|
||||||
|
if (nacp != nullptr) {
|
||||||
|
update_patch.version = nacp->GetVersionString();
|
||||||
|
out.push_back(update_patch);
|
||||||
|
} else {
|
||||||
|
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
|
||||||
|
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
|
||||||
|
if (meta_ver.value_or(0) == 0) {
|
||||||
|
out.push_back(update_patch);
|
||||||
|
} else {
|
||||||
|
update_patch.version = FormatTitleVersion(*meta_ver);
|
||||||
|
update_patch.numeric_version = *meta_ver;
|
||||||
|
out.push_back(update_patch);
|
||||||
|
}
|
||||||
|
} else if (update_raw != nullptr) {
|
||||||
|
update_patch.version = "PACKED";
|
||||||
|
update_patch.source = PatchSource::Packed;
|
||||||
out.push_back(update_patch);
|
out.push_back(update_patch);
|
||||||
}
|
}
|
||||||
} else if (update_raw != nullptr) {
|
|
||||||
update_patch.version = "PACKED";
|
|
||||||
out.push_back(update_patch);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -533,7 +875,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
.version = "Cheats",
|
.version = "Cheats",
|
||||||
.type = PatchType::Mod,
|
.type = PatchType::Mod,
|
||||||
.program_id = title_id,
|
.program_id = title_id,
|
||||||
.title_id = title_id
|
.title_id = title_id,
|
||||||
|
.source = PatchSource::Unknown
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,7 +897,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
|
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
|
||||||
file->GetName()) != EXEFS_FILE_NAMES.end()) {
|
file->GetName()) != EXEFS_FILE_NAMES.end()) {
|
||||||
layeredfs = true;
|
layeredfs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ips)
|
if (ips)
|
||||||
@@ -579,7 +922,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
.version = types,
|
.version = types,
|
||||||
.type = PatchType::Mod,
|
.type = PatchType::Mod,
|
||||||
.program_id = title_id,
|
.program_id = title_id,
|
||||||
.title_id = title_id});
|
.title_id = title_id,
|
||||||
|
.source = PatchSource::Unknown});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,7 +937,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
|
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
|
||||||
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) {
|
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) {
|
||||||
AppendCommaIfNotEmpty(types, "LayeredFS");
|
AppendCommaIfNotEmpty(types, "LayeredFS");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!types.empty()) {
|
if (!types.empty()) {
|
||||||
const auto mod_disabled =
|
const auto mod_disabled =
|
||||||
@@ -603,21 +947,44 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
.version = types,
|
.version = types,
|
||||||
.type = PatchType::Mod,
|
.type = PatchType::Mod,
|
||||||
.program_id = title_id,
|
.program_id = title_id,
|
||||||
.title_id = title_id});
|
.title_id = title_id,
|
||||||
|
.source = PatchSource::Unknown});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DLC
|
// DLC
|
||||||
const auto dlc_entries =
|
const auto dlc_entries =
|
||||||
content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
|
content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
|
||||||
|
|
||||||
std::vector<ContentProviderEntry> dlc_match;
|
std::vector<ContentProviderEntry> dlc_match;
|
||||||
dlc_match.reserve(dlc_entries.size());
|
dlc_match.reserve(dlc_entries.size());
|
||||||
std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
|
std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
|
||||||
[this](const ContentProviderEntry& entry) {
|
[this](const ContentProviderEntry& entry) {
|
||||||
return GetBaseTitleID(entry.title_id) == title_id &&
|
const auto base_tid = GetBaseTitleID(entry.title_id);
|
||||||
content_provider.GetEntry(entry)->GetStatus() ==
|
const bool matches_base = base_tid == title_id;
|
||||||
Loader::ResultStatus::Success;
|
|
||||||
|
if (!matches_base) {
|
||||||
|
LOG_DEBUG(Loader, "DLC {:016X} base {:016X} doesn't match title {:016X}",
|
||||||
|
entry.title_id, base_tid, title_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nca = content_provider.GetEntry(entry);
|
||||||
|
if (!nca) {
|
||||||
|
LOG_DEBUG(Loader, "Failed to get NCA for DLC {:016X}", entry.title_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto status = nca->GetStatus();
|
||||||
|
if (status != Loader::ResultStatus::Success) {
|
||||||
|
LOG_DEBUG(Loader, "DLC {:016X} NCA has status {}",
|
||||||
|
entry.title_id, static_cast<int>(status));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!dlc_match.empty()) {
|
if (!dlc_match.empty()) {
|
||||||
// Ensure sorted so DLC IDs show in order.
|
// Ensure sorted so DLC IDs show in order.
|
||||||
std::sort(dlc_match.begin(), dlc_match.end());
|
std::sort(dlc_match.begin(), dlc_match.end());
|
||||||
@@ -635,7 +1002,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
|
|||||||
.version = std::move(list),
|
.version = std::move(list),
|
||||||
.type = PatchType::DLC,
|
.type = PatchType::DLC,
|
||||||
.program_id = title_id,
|
.program_id = title_id,
|
||||||
.title_id = dlc_match.back().title_id});
|
.title_id = dlc_match.back().title_id,
|
||||||
|
.source = PatchSource::Unknown});
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -28,6 +31,13 @@ class NACP;
|
|||||||
|
|
||||||
enum class PatchType { Update, DLC, Mod };
|
enum class PatchType { Update, DLC, Mod };
|
||||||
|
|
||||||
|
enum class PatchSource {
|
||||||
|
Unknown,
|
||||||
|
NAND,
|
||||||
|
External,
|
||||||
|
Packed,
|
||||||
|
};
|
||||||
|
|
||||||
struct Patch {
|
struct Patch {
|
||||||
bool enabled;
|
bool enabled;
|
||||||
std::string name;
|
std::string name;
|
||||||
@@ -35,6 +45,8 @@ struct Patch {
|
|||||||
PatchType type;
|
PatchType type;
|
||||||
u64 program_id;
|
u64 program_id;
|
||||||
u64 title_id;
|
u64 title_id;
|
||||||
|
PatchSource source;
|
||||||
|
u32 numeric_version{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
// A centralized class to manage patches to games.
|
// A centralized class to manage patches to games.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -13,12 +13,15 @@
|
|||||||
#include "common/hex_util.h"
|
#include "common/hex_util.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/scope_exit.h"
|
#include "common/scope_exit.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
#include "core/crypto/key_manager.h"
|
#include "core/crypto/key_manager.h"
|
||||||
#include "core/file_sys/card_image.h"
|
#include "core/file_sys/card_image.h"
|
||||||
#include "core/file_sys/common_funcs.h"
|
#include "core/file_sys/common_funcs.h"
|
||||||
#include "core/file_sys/content_archive.h"
|
#include "core/file_sys/content_archive.h"
|
||||||
|
#include "core/file_sys/control_metadata.h"
|
||||||
#include "core/file_sys/nca_metadata.h"
|
#include "core/file_sys/nca_metadata.h"
|
||||||
#include "core/file_sys/registered_cache.h"
|
#include "core/file_sys/registered_cache.h"
|
||||||
|
#include "core/file_sys/romfs.h"
|
||||||
#include "core/file_sys/submission_package.h"
|
#include "core/file_sys/submission_package.h"
|
||||||
#include "core/file_sys/vfs/vfs_concat.h"
|
#include "core/file_sys/vfs/vfs_concat.h"
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
@@ -974,6 +977,22 @@ std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
|
|||||||
return iter->first;
|
return iter->first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const {
|
||||||
|
auto it = providers.find(ContentProviderUnionSlot::External);
|
||||||
|
if (it != providers.end() && it->second != nullptr) {
|
||||||
|
return dynamic_cast<const ExternalContentProvider*>(it->second);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnionSlot slot) const {
|
||||||
|
auto it = providers.find(slot);
|
||||||
|
if (it != providers.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
ManualContentProvider::~ManualContentProvider() = default;
|
ManualContentProvider::~ManualContentProvider() = default;
|
||||||
|
|
||||||
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
|
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
|
||||||
@@ -981,8 +1000,51 @@ void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType con
|
|||||||
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRecordType content_type,
|
||||||
|
u64 title_id, u32 version,
|
||||||
|
const std::string& version_string, VirtualFile file) {
|
||||||
|
if (title_type == TitleType::Update) {
|
||||||
|
auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(),
|
||||||
|
[title_id, version](const ExternalUpdateEntry& entry) {
|
||||||
|
return entry.title_id == title_id && entry.version == version;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (it != multi_version_entries.end()) {
|
||||||
|
// Update existing entry
|
||||||
|
it->files[content_type] = file;
|
||||||
|
if (!version_string.empty()) {
|
||||||
|
it->version_string = version_string;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add new entry
|
||||||
|
ExternalUpdateEntry new_entry;
|
||||||
|
new_entry.title_id = title_id;
|
||||||
|
new_entry.version = version;
|
||||||
|
new_entry.version_string = version_string;
|
||||||
|
new_entry.files[content_type] = file;
|
||||||
|
multi_version_entries.push_back(new_entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto existing = entries.find({title_type, content_type, title_id});
|
||||||
|
if (existing == entries.end()) {
|
||||||
|
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||||
|
} else {
|
||||||
|
// Check if this version is higher
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id && entry.version > version) {
|
||||||
|
return; // Don't replace with lower version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries.insert_or_assign({title_type, content_type, title_id}, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ManualContentProvider::ClearAllEntries() {
|
void ManualContentProvider::ClearAllEntries() {
|
||||||
entries.clear();
|
entries.clear();
|
||||||
|
multi_version_entries.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ManualContentProvider::Refresh() {}
|
void ManualContentProvider::Refresh() {}
|
||||||
@@ -1036,4 +1098,459 @@ std::vector<ContentProviderEntry> ManualContentProvider::ListEntriesFilter(
|
|||||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<ExternalUpdateEntry> ManualContentProvider::ListUpdateVersions(u64 title_id) const {
|
||||||
|
std::vector<ExternalUpdateEntry> out;
|
||||||
|
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id) {
|
||||||
|
out.push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(out.begin(), out.end(), [](const ExternalUpdateEntry& a, const ExternalUpdateEntry& b) {
|
||||||
|
return a.version > b.version;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id && entry.version == version) {
|
||||||
|
auto it = entry.files.find(type);
|
||||||
|
if (it != entry.files.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id && entry.files.count(type) > 0) {
|
||||||
|
count++;
|
||||||
|
if (count > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
|
||||||
|
: load_dirs(std::move(load_directories)) {
|
||||||
|
ExternalContentProvider::Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
ExternalContentProvider::~ExternalContentProvider() = default;
|
||||||
|
|
||||||
|
void ExternalContentProvider::AddDirectory(VirtualDir directory) {
|
||||||
|
if (directory != nullptr) {
|
||||||
|
load_dirs.push_back(std::move(directory));
|
||||||
|
ScanDirectory(load_dirs.back());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExternalContentProvider::ClearDirectories() {
|
||||||
|
load_dirs.clear();
|
||||||
|
entries.clear();
|
||||||
|
versions.clear();
|
||||||
|
multi_version_entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExternalContentProvider::Refresh() {
|
||||||
|
entries.clear();
|
||||||
|
versions.clear();
|
||||||
|
multi_version_entries.clear();
|
||||||
|
for (const auto& dir : load_dirs) {
|
||||||
|
if (dir != nullptr) {
|
||||||
|
ScanDirectory(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
|
||||||
|
if (dir == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& file : dir->GetFiles()) {
|
||||||
|
const auto filename = file->GetName();
|
||||||
|
const auto dot_pos = filename.find_last_of('.');
|
||||||
|
|
||||||
|
if (dot_pos == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto extension = Common::ToLower(filename.substr(dot_pos + 1));
|
||||||
|
|
||||||
|
if (extension == "nsp") {
|
||||||
|
ProcessNSP(file);
|
||||||
|
} else if (extension == "xci") {
|
||||||
|
ProcessXCI(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& subdir : dir->GetSubdirectories()) {
|
||||||
|
ScanDirectory(subdir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
|
||||||
|
auto nsp = NSP(file);
|
||||||
|
if (nsp.GetStatus() != Loader::ResultStatus::Success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
|
||||||
|
|
||||||
|
const auto ncas = nsp.GetNCAs();
|
||||||
|
|
||||||
|
std::map<u64, u32> nsp_versions;
|
||||||
|
std::map<u64, std::string> nsp_version_strings; // title_id -> NACP version string
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : ncas) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (content_type == ContentRecordType::Meta) {
|
||||||
|
const auto subdirs = nca->GetSubdirectories();
|
||||||
|
if (!subdirs.empty()) {
|
||||||
|
const auto section0 = subdirs[0];
|
||||||
|
const auto files = section0->GetFiles();
|
||||||
|
for (const auto& inner_file : files) {
|
||||||
|
if (inner_file->GetExtension() == "cnmt") {
|
||||||
|
const CNMT cnmt(inner_file);
|
||||||
|
const auto cnmt_title_id = cnmt.GetTitleID();
|
||||||
|
const auto version = cnmt.GetTitleVersion();
|
||||||
|
nsp_versions[cnmt_title_id] = version;
|
||||||
|
versions[cnmt_title_id] = version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content_type == ContentRecordType::Control && title_type == TitleType::Update) {
|
||||||
|
auto romfs = nca->GetRomFS();
|
||||||
|
if (romfs) {
|
||||||
|
auto extracted = ExtractRomFS(romfs);
|
||||||
|
if (extracted) {
|
||||||
|
auto nacp_file = extracted->GetFile("control.nacp");
|
||||||
|
if (!nacp_file) {
|
||||||
|
nacp_file = extracted->GetFile("Control.nacp");
|
||||||
|
}
|
||||||
|
if (nacp_file) {
|
||||||
|
NACP nacp(nacp_file);
|
||||||
|
auto ver_str = nacp.GetVersionString();
|
||||||
|
if (!ver_str.empty()) {
|
||||||
|
nsp_version_strings[title_id] = ver_str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::pair<u64, u32>, std::map<ContentRecordType, VirtualFile>> version_files;
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : ncas) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (title_type != TitleType::AOC && title_type != TitleType::Update) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nca_file = nsp.GetNCAFile(title_id, content_type, title_type);
|
||||||
|
if (nca_file != nullptr) {
|
||||||
|
entries[{title_id, content_type, title_type}] = nca_file;
|
||||||
|
|
||||||
|
if (title_type == TitleType::Update) {
|
||||||
|
u32 version = 0;
|
||||||
|
auto ver_it = nsp_versions.find(title_id);
|
||||||
|
if (ver_it != nsp_versions.end()) {
|
||||||
|
version = ver_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
version_files[{title_id, version}][content_type] = nca_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_FS, "Added entry - Title ID: {:016X}, Type: {}, Content: {}",
|
||||||
|
title_id, static_cast<int>(title_type), static_cast<int>(content_type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, files_map] : version_files) {
|
||||||
|
const auto& [title_id, version] = key;
|
||||||
|
|
||||||
|
std::string ver_str;
|
||||||
|
auto str_it = nsp_version_strings.find(title_id);
|
||||||
|
if (str_it != nsp_version_strings.end()) {
|
||||||
|
ver_str = str_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool version_exists = false;
|
||||||
|
for (auto& existing : multi_version_entries) {
|
||||||
|
if (existing.title_id == title_id && existing.version == version) {
|
||||||
|
for (const auto& [content_type, _file] : files_map) {
|
||||||
|
existing.files[content_type] = _file;
|
||||||
|
}
|
||||||
|
if (existing.version_string.empty() && !ver_str.empty()) {
|
||||||
|
existing.version_string = ver_str;
|
||||||
|
}
|
||||||
|
version_exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!version_exists && !files_map.empty()) {
|
||||||
|
ExternalUpdateEntry update_entry{
|
||||||
|
.title_id = title_id,
|
||||||
|
.version = version,
|
||||||
|
.version_string = ver_str,
|
||||||
|
.files = files_map
|
||||||
|
};
|
||||||
|
multi_version_entries.push_back(update_entry);
|
||||||
|
LOG_DEBUG(Service_FS, "Added multi-version update - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}",
|
||||||
|
title_id, version, ver_str, files_map.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
|
||||||
|
auto xci = XCI(file);
|
||||||
|
if (xci.GetStatus() != Loader::ResultStatus::Success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nsp = xci.GetSecurePartitionNSP();
|
||||||
|
if (nsp == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto ncas = nsp->GetNCAs();
|
||||||
|
|
||||||
|
std::map<u64, u32> xci_versions;
|
||||||
|
std::map<u64, std::string> xci_version_strings;
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : ncas) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (content_type == ContentRecordType::Meta) {
|
||||||
|
const auto subdirs = nca->GetSubdirectories();
|
||||||
|
if (!subdirs.empty()) {
|
||||||
|
const auto section0 = subdirs[0];
|
||||||
|
const auto files = section0->GetFiles();
|
||||||
|
for (const auto& inner_file : files) {
|
||||||
|
if (inner_file->GetExtension() == "cnmt") {
|
||||||
|
const CNMT cnmt(inner_file);
|
||||||
|
const auto cnmt_title_id = cnmt.GetTitleID();
|
||||||
|
const auto version = cnmt.GetTitleVersion();
|
||||||
|
xci_versions[cnmt_title_id] = version;
|
||||||
|
versions[cnmt_title_id] = version;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content_type == ContentRecordType::Control && title_type == TitleType::Update) {
|
||||||
|
auto romfs = nca->GetRomFS();
|
||||||
|
if (romfs) {
|
||||||
|
auto extracted = ExtractRomFS(romfs);
|
||||||
|
if (extracted) {
|
||||||
|
auto nacp_file = extracted->GetFile("control.nacp");
|
||||||
|
if (!nacp_file) {
|
||||||
|
nacp_file = extracted->GetFile("Control.nacp");
|
||||||
|
}
|
||||||
|
if (nacp_file) {
|
||||||
|
NACP nacp(nacp_file);
|
||||||
|
auto ver_str = nacp.GetVersionString();
|
||||||
|
if (!ver_str.empty()) {
|
||||||
|
xci_version_strings[title_id] = ver_str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::pair<u64, u32>, std::map<ContentRecordType, VirtualFile>> version_files;
|
||||||
|
|
||||||
|
for (const auto& [title_id, nca_map] : ncas) {
|
||||||
|
for (const auto& [type_pair, nca] : nca_map) {
|
||||||
|
const auto& [title_type, content_type] = type_pair;
|
||||||
|
|
||||||
|
if (title_type != TitleType::AOC && title_type != TitleType::Update) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nca_file = nsp->GetNCAFile(title_id, content_type, title_type);
|
||||||
|
if (nca_file != nullptr) {
|
||||||
|
entries[{title_id, content_type, title_type}] = nca_file;
|
||||||
|
|
||||||
|
if (title_type == TitleType::Update) {
|
||||||
|
u32 version = 0;
|
||||||
|
auto ver_it = xci_versions.find(title_id);
|
||||||
|
if (ver_it != xci_versions.end()) {
|
||||||
|
version = ver_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
version_files[{title_id, version}][content_type] = nca_file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, files_map] : version_files) {
|
||||||
|
const auto& [title_id, version] = key;
|
||||||
|
|
||||||
|
std::string ver_str;
|
||||||
|
auto str_it = xci_version_strings.find(title_id);
|
||||||
|
if (str_it != xci_version_strings.end()) {
|
||||||
|
ver_str = str_it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool version_exists = false;
|
||||||
|
for (auto& existing : multi_version_entries) {
|
||||||
|
if (existing.title_id == title_id && existing.version == version) {
|
||||||
|
for (const auto& [content_type, _file] : files_map) {
|
||||||
|
existing.files[content_type] = _file;
|
||||||
|
}
|
||||||
|
if (existing.version_string.empty() && !ver_str.empty()) {
|
||||||
|
existing.version_string = ver_str;
|
||||||
|
}
|
||||||
|
version_exists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!version_exists && !files_map.empty()) {
|
||||||
|
ExternalUpdateEntry update_entry{
|
||||||
|
.title_id = title_id,
|
||||||
|
.version = version,
|
||||||
|
.version_string = ver_str,
|
||||||
|
.files = files_map
|
||||||
|
};
|
||||||
|
multi_version_entries.push_back(update_entry);
|
||||||
|
LOG_DEBUG(Service_FS, "Added multi-version update from XCI - Title ID: {:016X}, Version: {}, VersionStr: {}, Content types: {}",
|
||||||
|
title_id, version, ver_str, files_map.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
|
||||||
|
return GetEntryRaw(title_id, type) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<u32> ExternalContentProvider::GetEntryVersion(u64 title_id) const {
|
||||||
|
const auto it = versions.find(title_id);
|
||||||
|
if (it != versions.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile ExternalContentProvider::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
|
||||||
|
return GetEntryRaw(title_id, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile ExternalContentProvider::GetEntryRaw(u64 title_id, ContentRecordType type) const {
|
||||||
|
// Try to find in AOC (DLC) entries
|
||||||
|
{
|
||||||
|
const auto it = entries.find({title_id, type, TitleType::AOC});
|
||||||
|
if (it != entries.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find in Update entries
|
||||||
|
{
|
||||||
|
const auto it = entries.find({title_id, type, TitleType::Update});
|
||||||
|
if (it != entries.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<NCA> ExternalContentProvider::GetEntry(u64 title_id,
|
||||||
|
ContentRecordType type) const {
|
||||||
|
const auto file = GetEntryRaw(title_id, type);
|
||||||
|
if (file == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return std::make_unique<NCA>(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ContentProviderEntry> ExternalContentProvider::ListEntriesFilter(
|
||||||
|
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||||
|
std::optional<u64> title_id) const {
|
||||||
|
std::vector<ContentProviderEntry> out;
|
||||||
|
|
||||||
|
for (const auto& [key, file] : entries) {
|
||||||
|
const auto& [e_title_id, e_content_type, e_title_type] = key;
|
||||||
|
|
||||||
|
if ((title_type == std::nullopt || e_title_type == *title_type) &&
|
||||||
|
(record_type == std::nullopt || e_content_type == *record_type) &&
|
||||||
|
(title_id == std::nullopt || e_title_id == *title_id)) {
|
||||||
|
out.emplace_back(ContentProviderEntry{e_title_id, e_content_type});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(out.begin(), out.end());
|
||||||
|
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ExternalUpdateEntry> ExternalContentProvider::ListUpdateVersions(u64 title_id) const {
|
||||||
|
std::vector<ExternalUpdateEntry> out;
|
||||||
|
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id) {
|
||||||
|
out.push_back(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(out.begin(), out.end(), [](const ExternalUpdateEntry& a, const ExternalUpdateEntry& b) {
|
||||||
|
return a.version > b.version;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id && entry.version == version) {
|
||||||
|
auto it = entry.files.find(type);
|
||||||
|
if (it != entry.files.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
|
||||||
|
size_t count = 0;
|
||||||
|
for (const auto& entry : multi_version_entries) {
|
||||||
|
if (entry.title_id == title_id && entry.files.count(type) > 0) {
|
||||||
|
count++;
|
||||||
|
if (count > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace FileSys
|
} // namespace FileSys
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -14,7 +17,8 @@
|
|||||||
#include "core/file_sys/vfs/vfs.h"
|
#include "core/file_sys/vfs/vfs.h"
|
||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
class CNMT;
|
class ExternalContentProvider;
|
||||||
|
class CNMT;
|
||||||
class NCA;
|
class NCA;
|
||||||
class NSP;
|
class NSP;
|
||||||
class XCI;
|
class XCI;
|
||||||
@@ -48,6 +52,13 @@ struct ContentProviderEntry {
|
|||||||
std::string DebugInfo() const;
|
std::string DebugInfo() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ExternalUpdateEntry {
|
||||||
|
u64 title_id;
|
||||||
|
u32 version;
|
||||||
|
std::string version_string;
|
||||||
|
std::map<ContentRecordType, VirtualFile> files;
|
||||||
|
};
|
||||||
|
|
||||||
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
|
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
|
||||||
return base_title_id | 0x800;
|
return base_title_id | 0x800;
|
||||||
}
|
}
|
||||||
@@ -208,6 +219,7 @@ enum class ContentProviderUnionSlot {
|
|||||||
UserNAND, ///< User NAND
|
UserNAND, ///< User NAND
|
||||||
SDMC, ///< SD Card
|
SDMC, ///< SD Card
|
||||||
FrontendManual, ///< Frontend-defined game list or similar
|
FrontendManual, ///< Frontend-defined game list or similar
|
||||||
|
External, ///< External content from NSP/XCI files in configured directories
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
|
||||||
@@ -228,6 +240,9 @@ public:
|
|||||||
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||||
std::optional<u64> title_id) const override;
|
std::optional<u64> title_id) const override;
|
||||||
|
|
||||||
|
const ExternalContentProvider* GetExternalProvider() const;
|
||||||
|
const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const;
|
||||||
|
|
||||||
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
|
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
|
||||||
std::optional<ContentProviderUnionSlot> origin = {},
|
std::optional<ContentProviderUnionSlot> origin = {},
|
||||||
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
|
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
|
||||||
@@ -246,6 +261,8 @@ public:
|
|||||||
|
|
||||||
void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||||
VirtualFile file);
|
VirtualFile file);
|
||||||
|
void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id,
|
||||||
|
u32 version, const std::string& version_string, VirtualFile file);
|
||||||
void ClearAllEntries();
|
void ClearAllEntries();
|
||||||
|
|
||||||
void Refresh() override;
|
void Refresh() override;
|
||||||
@@ -258,8 +275,46 @@ public:
|
|||||||
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||||
std::optional<u64> title_id) const override;
|
std::optional<u64> title_id) const override;
|
||||||
|
|
||||||
|
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
|
||||||
|
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
|
||||||
|
bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
|
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
|
||||||
|
std::vector<ExternalUpdateEntry> multi_version_entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExternalContentProvider : public ContentProvider {
|
||||||
|
public:
|
||||||
|
explicit ExternalContentProvider(std::vector<VirtualDir> load_directories = {});
|
||||||
|
~ExternalContentProvider() override;
|
||||||
|
|
||||||
|
void AddDirectory(VirtualDir directory);
|
||||||
|
void ClearDirectories();
|
||||||
|
|
||||||
|
void Refresh() override;
|
||||||
|
bool HasEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::optional<u32> GetEntryVersion(u64 title_id) const override;
|
||||||
|
VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const override;
|
||||||
|
VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::unique_ptr<NCA> GetEntry(u64 title_id, ContentRecordType type) const override;
|
||||||
|
std::vector<ContentProviderEntry> ListEntriesFilter(
|
||||||
|
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
|
||||||
|
std::optional<u64> title_id) const override;
|
||||||
|
|
||||||
|
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
|
||||||
|
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
|
||||||
|
bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ScanDirectory(const VirtualDir& dir);
|
||||||
|
void ProcessNSP(const VirtualFile& file);
|
||||||
|
void ProcessXCI(const VirtualFile& file);
|
||||||
|
|
||||||
|
std::vector<VirtualDir> load_dirs;
|
||||||
|
std::map<std::tuple<u64, ContentRecordType, TitleType>, VirtualFile> entries;
|
||||||
|
std::map<u64, u32> versions;
|
||||||
|
std::vector<ExternalUpdateEntry> multi_version_entries;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace FileSys
|
} // namespace FileSys
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -275,6 +278,14 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
|
|||||||
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] =
|
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] =
|
||||||
std::move(next_nca);
|
std::move(next_nca);
|
||||||
} else {
|
} else {
|
||||||
|
// fix for Bayonetta Origins in Bayonetta 3 and external content
|
||||||
|
// where multiple update NCAs exist for the same title and type.
|
||||||
|
auto& target_map = ncas[cnmt.GetTitleID()];
|
||||||
|
auto existing = target_map.find({cnmt.GetType(), rec.type});
|
||||||
|
|
||||||
|
if (existing != target_map.end() && rec.type == ContentRecordType::Program) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
ncas[cnmt.GetTitleID()][{cnmt.GetType(), rec.type}] = std::move(next_nca);
|
ncas[cnmt.GetTitleID()][{cnmt.GetType(), rec.type}] = std::move(next_nca);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -83,6 +86,14 @@ struct CodeSet final {
|
|||||||
const Segment& PatchSegment() const {
|
const Segment& PatchSegment() const {
|
||||||
return patch_segment;
|
return patch_segment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Segment& PostPatchSegment() {
|
||||||
|
return post_patch_segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Segment& PostPatchSegment() const {
|
||||||
|
return post_patch_segment;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// The overall data that backs this code set.
|
/// The overall data that backs this code set.
|
||||||
@@ -93,6 +104,7 @@ struct CodeSet final {
|
|||||||
|
|
||||||
#ifdef HAS_NCE
|
#ifdef HAS_NCE
|
||||||
Segment patch_segment;
|
Segment patch_segment;
|
||||||
|
Segment post_patch_segment;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/// The entry point address for this code set.
|
/// The entry point address for this code set.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||||
@@ -1258,6 +1258,7 @@ void KProcess::LoadModule(CodeSet code_set, KProcessAddress base_addr) {
|
|||||||
|
|
||||||
#ifdef HAS_NCE
|
#ifdef HAS_NCE
|
||||||
const auto& patch = code_set.PatchSegment();
|
const auto& patch = code_set.PatchSegment();
|
||||||
|
const auto& post_patch = code_set.PostPatchSegment();
|
||||||
if (this->IsApplication() && Settings::IsNceEnabled() && patch.size != 0) {
|
if (this->IsApplication() && Settings::IsNceEnabled() && patch.size != 0) {
|
||||||
auto& buffer = m_kernel.System().DeviceMemory().buffer;
|
auto& buffer = m_kernel.System().DeviceMemory().buffer;
|
||||||
const auto& code = code_set.CodeSegment();
|
const auto& code = code_set.CodeSegment();
|
||||||
@@ -1265,7 +1266,15 @@ void KProcess::LoadModule(CodeSet code_set, KProcessAddress base_addr) {
|
|||||||
Common::MemoryPermission::Read | Common::MemoryPermission::Execute);
|
Common::MemoryPermission::Read | Common::MemoryPermission::Execute);
|
||||||
buffer.Protect(GetInteger(base_addr + patch.addr), patch.size,
|
buffer.Protect(GetInteger(base_addr + patch.addr), patch.size,
|
||||||
Common::MemoryPermission::Read | Common::MemoryPermission::Execute);
|
Common::MemoryPermission::Read | Common::MemoryPermission::Execute);
|
||||||
|
// Protect post-patch segment if it exists like abve
|
||||||
|
if (post_patch.size != 0) {
|
||||||
|
buffer.Protect(GetInteger(base_addr + post_patch.addr), post_patch.size,
|
||||||
|
Common::MemoryPermission::Read | Common::MemoryPermission::Execute);
|
||||||
|
}
|
||||||
ReprotectSegment(code_set.PatchSegment(), Svc::MemoryPermission::None);
|
ReprotectSegment(code_set.PatchSegment(), Svc::MemoryPermission::None);
|
||||||
|
if (post_patch.size != 0) {
|
||||||
|
ReprotectSegment(code_set.PostPatchSegment(), Svc::MemoryPermission::None);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
@@ -36,7 +36,7 @@ IAppletCommonFunctions::IAppletCommonFunctions(Core::System& system_,
|
|||||||
{100, nullptr, "SetApplicationCoreUsageMode"},
|
{100, nullptr, "SetApplicationCoreUsageMode"},
|
||||||
{300, D<&IAppletCommonFunctions::GetCurrentApplicationId>, "GetCurrentApplicationId"},
|
{300, D<&IAppletCommonFunctions::GetCurrentApplicationId>, "GetCurrentApplicationId"},
|
||||||
{310, nullptr, "IsSystemAppletHomeMenu"}, //19.0.0+
|
{310, nullptr, "IsSystemAppletHomeMenu"}, //19.0.0+
|
||||||
{320, nullptr, "SetGpuTimeSliceBoost"}, //19.0.0+
|
{320, D<&IAppletCommonFunctions::SetGpuTimeSliceBoost>, "SetGpuTimeSliceBoost"}, //19.0.0+
|
||||||
{321, nullptr, "SetGpuTimeSliceBoostDueToApplication"}, //19.0.0+
|
{321, nullptr, "SetGpuTimeSliceBoostDueToApplication"}, //19.0.0+
|
||||||
{350, D<&IAppletCommonFunctions::Unknown350>, "Unknown350"} //20.0.0+
|
{350, D<&IAppletCommonFunctions::Unknown350>, "Unknown350"} //20.0.0+
|
||||||
};
|
};
|
||||||
@@ -82,6 +82,11 @@ Result IAppletCommonFunctions::GetCurrentApplicationId(Out<u64> out_application_
|
|||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Result IAppletCommonFunctions::SetGpuTimeSliceBoost(s64 time_span) {
|
||||||
|
LOG_WARNING(Service_AM, "(STUBBED) called, time_span={}", time_span);
|
||||||
|
R_SUCCEED();
|
||||||
|
}
|
||||||
|
|
||||||
Result IAppletCommonFunctions::Unknown350(Out<u16> out_unknown) {
|
Result IAppletCommonFunctions::Unknown350(Out<u16> out_unknown) {
|
||||||
LOG_WARNING(Service_AM, "(STUBBED) called");
|
LOG_WARNING(Service_AM, "(STUBBED) called");
|
||||||
*out_unknown = 0;
|
*out_unknown = 0;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
|
||||||
@@ -24,6 +24,7 @@ private:
|
|||||||
Result SetDisplayMagnification(f32 x, f32 y, f32 width, f32 height);
|
Result SetDisplayMagnification(f32 x, f32 y, f32 width, f32 height);
|
||||||
Result SetCpuBoostRequestPriority(s32 priority);
|
Result SetCpuBoostRequestPriority(s32 priority);
|
||||||
Result GetCurrentApplicationId(Out<u64> out_application_id);
|
Result GetCurrentApplicationId(Out<u64> out_application_id);
|
||||||
|
Result SetGpuTimeSliceBoost(s64 time_span);
|
||||||
Result Unknown350(Out<u16> out_unknown);
|
Result Unknown350(Out<u16> out_unknown);
|
||||||
|
|
||||||
const std::shared_ptr<Applet> applet;
|
const std::shared_ptr<Applet> applet;
|
||||||
|
|||||||
@@ -179,7 +179,14 @@ Result ILibraryAppletSelfAccessor::GetMainAppletStorageId(Out<FileSys::StorageId
|
|||||||
|
|
||||||
Result ILibraryAppletSelfAccessor::ExitProcessAndReturn() {
|
Result ILibraryAppletSelfAccessor::ExitProcessAndReturn() {
|
||||||
LOG_INFO(Service_AM, "called");
|
LOG_INFO(Service_AM, "called");
|
||||||
m_applet->process->Terminate();
|
|
||||||
|
if (const auto caller_applet = m_applet->caller_applet.lock(); caller_applet) {
|
||||||
|
m_applet->process->Terminate();
|
||||||
|
} else {
|
||||||
|
system.GetUserChannel() = m_applet->user_channel_launch_parameter;
|
||||||
|
system.ExecuteProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
R_SUCCEED();
|
R_SUCCEED();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/fs/fs.h"
|
#include "common/fs/fs.h"
|
||||||
#include "common/fs/path_util.h"
|
#include "common/fs/path_util.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/file_sys/bis_factory.h"
|
#include "core/file_sys/bis_factory.h"
|
||||||
@@ -507,6 +508,10 @@ FileSys::RegisteredCache* FileSystemController::GetSDMCContents() const {
|
|||||||
return sdmc_factory->GetSDMCContents();
|
return sdmc_factory->GetSDMCContents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileSys::ExternalContentProvider* FileSystemController::GetExternalContentProvider() const {
|
||||||
|
return external_provider.get();
|
||||||
|
}
|
||||||
|
|
||||||
FileSys::PlaceholderCache* FileSystemController::GetSystemNANDPlaceholder() const {
|
FileSys::PlaceholderCache* FileSystemController::GetSystemNANDPlaceholder() const {
|
||||||
LOG_TRACE(Service_FS, "Opening System NAND Placeholder");
|
LOG_TRACE(Service_FS, "Opening System NAND Placeholder");
|
||||||
|
|
||||||
@@ -684,6 +689,7 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
|||||||
if (overwrite) {
|
if (overwrite) {
|
||||||
bis_factory = nullptr;
|
bis_factory = nullptr;
|
||||||
sdmc_factory = nullptr;
|
sdmc_factory = nullptr;
|
||||||
|
external_provider = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
using EdenPath = Common::FS::EdenPath;
|
using EdenPath = Common::FS::EdenPath;
|
||||||
@@ -716,6 +722,36 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
|
|||||||
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
|
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
|
||||||
sdmc_factory->GetSDMCContents());
|
sdmc_factory->GetSDMCContents());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (external_provider == nullptr) {
|
||||||
|
std::vector<FileSys::VirtualDir> external_dirs;
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_FS, "Initializing ExternalContentProvider with {} configured directories",
|
||||||
|
Settings::values.external_content_dirs.size());
|
||||||
|
|
||||||
|
for (const auto& dir_path : Settings::values.external_content_dirs) {
|
||||||
|
if (!dir_path.empty()) {
|
||||||
|
LOG_DEBUG(Service_FS, "Attempting to open directory: {}", dir_path);
|
||||||
|
auto dir = vfs.OpenDirectory(dir_path, FileSys::OpenMode::Read);
|
||||||
|
if (dir != nullptr) {
|
||||||
|
external_dirs.push_back(std::move(dir));
|
||||||
|
LOG_DEBUG(Service_FS, "Successfully opened directory: {}", dir_path);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(Service_FS, "Failed to open directory: {}", dir_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_FS, "Creating ExternalContentProvider with {} opened directories",
|
||||||
|
external_dirs.size());
|
||||||
|
|
||||||
|
external_provider = std::make_unique<FileSys::ExternalContentProvider>(
|
||||||
|
std::move(external_dirs));
|
||||||
|
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::External,
|
||||||
|
external_provider.get());
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_FS, "ExternalContentProvider registered to content provider union");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSystemController::Reset() {
|
void FileSystemController::Reset() {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
@@ -17,6 +20,7 @@ class System;
|
|||||||
|
|
||||||
namespace FileSys {
|
namespace FileSys {
|
||||||
class BISFactory;
|
class BISFactory;
|
||||||
|
class ExternalContentProvider;
|
||||||
class NCA;
|
class NCA;
|
||||||
class RegisteredCache;
|
class RegisteredCache;
|
||||||
class RegisteredCacheUnion;
|
class RegisteredCacheUnion;
|
||||||
@@ -117,6 +121,8 @@ public:
|
|||||||
|
|
||||||
FileSys::VirtualDir GetBCATDirectory(u64 title_id) const;
|
FileSys::VirtualDir GetBCATDirectory(u64 title_id) const;
|
||||||
|
|
||||||
|
FileSys::ExternalContentProvider* GetExternalContentProvider() const;
|
||||||
|
|
||||||
// Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
|
// Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
|
||||||
// above is called.
|
// above is called.
|
||||||
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
||||||
@@ -138,6 +144,8 @@ private:
|
|||||||
std::unique_ptr<FileSys::SDMCFactory> sdmc_factory;
|
std::unique_ptr<FileSys::SDMCFactory> sdmc_factory;
|
||||||
std::unique_ptr<FileSys::BISFactory> bis_factory;
|
std::unique_ptr<FileSys::BISFactory> bis_factory;
|
||||||
|
|
||||||
|
std::unique_ptr<FileSys::ExternalContentProvider> external_provider;
|
||||||
|
|
||||||
std::unique_ptr<FileSys::XCI> gamecard;
|
std::unique_ptr<FileSys::XCI> gamecard;
|
||||||
std::unique_ptr<FileSys::RegisteredCache> gamecard_registered;
|
std::unique_ptr<FileSys::RegisteredCache> gamecard_registered;
|
||||||
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
|
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
@@ -99,11 +99,6 @@ Status BufferQueueConsumer::AcquireBuffer(BufferItem* out_buffer,
|
|||||||
slots[slot].acquire_called = true;
|
slots[slot].acquire_called = true;
|
||||||
slots[slot].needs_cleanup_on_release = false;
|
slots[slot].needs_cleanup_on_release = false;
|
||||||
slots[slot].buffer_state = BufferState::Acquired;
|
slots[slot].buffer_state = BufferState::Acquired;
|
||||||
|
|
||||||
// TODO: for now, avoid resetting the fence, so that when we next return this
|
|
||||||
// slot to the producer, it will wait for the fence to pass. We should fix this
|
|
||||||
// by properly waiting for the fence in the BufferItemConsumer.
|
|
||||||
// slots[slot].fence = Fence::NoFence();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the buffer has previously been acquired by the consumer, set graphic_buffer to nullptr to
|
// If the buffer has previously been acquired by the consumer, set graphic_buffer to nullptr to
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
@@ -17,6 +17,39 @@ BufferQueueCore::BufferQueueCore() = default;
|
|||||||
|
|
||||||
BufferQueueCore::~BufferQueueCore() = default;
|
BufferQueueCore::~BufferQueueCore() = default;
|
||||||
|
|
||||||
|
void BufferQueueCore::PushHistory(u64 frame_number, s64 queue_time, s64 presentation_time, BufferState state) {
|
||||||
|
std::lock_guard lk(buffer_history_mutex);
|
||||||
|
|
||||||
|
auto it = buffer_history_map.find(frame_number);
|
||||||
|
if (it != buffer_history_map.end()) {
|
||||||
|
it->second.state = state;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_history_map.emplace(frame_number, BufferHistoryInfo{
|
||||||
|
frame_number,
|
||||||
|
queue_time,
|
||||||
|
presentation_time,
|
||||||
|
state
|
||||||
|
});
|
||||||
|
buffer_history_order.push_back(frame_number);
|
||||||
|
|
||||||
|
if (buffer_history_order.size() > BUFFER_HISTORY_SIZE) {
|
||||||
|
u64 oldest_frame = buffer_history_order.front();
|
||||||
|
buffer_history_order.pop_front();
|
||||||
|
buffer_history_map.erase(oldest_frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BufferQueueCore::UpdateHistory(u64 frame_number, BufferState state) {
|
||||||
|
std::lock_guard lk(buffer_history_mutex);
|
||||||
|
|
||||||
|
auto it = buffer_history_map.find(frame_number);
|
||||||
|
if (it != buffer_history_map.end()) {
|
||||||
|
it->second.state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void BufferQueueCore::SignalDequeueCondition() {
|
void BufferQueueCore::SignalDequeueCondition() {
|
||||||
dequeue_possible.store(true);
|
dequeue_possible.store(true);
|
||||||
dequeue_condition.notify_all();
|
dequeue_condition.notify_all();
|
||||||
@@ -30,7 +63,6 @@ bool BufferQueueCore::WaitForDequeueCondition(std::unique_lock<std::mutex>& lk)
|
|||||||
}
|
}
|
||||||
|
|
||||||
s32 BufferQueueCore::GetMinUndequeuedBufferCountLocked(bool async) const {
|
s32 BufferQueueCore::GetMinUndequeuedBufferCountLocked(bool async) const {
|
||||||
// If DequeueBuffer is allowed to error out, we don't have to add an extra buffer.
|
|
||||||
if (!use_async_buffer) {
|
if (!use_async_buffer) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -55,8 +87,6 @@ s32 BufferQueueCore::GetMaxBufferCountLocked(bool async) const {
|
|||||||
return override_max_buffer_count;
|
return override_max_buffer_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any buffers that are dequeued by the producer or sitting in the queue waiting to be consumed
|
|
||||||
// need to have their slots preserved.
|
|
||||||
for (s32 slot = max_buffer_count; slot < BufferQueueDefs::NUM_BUFFER_SLOTS; ++slot) {
|
for (s32 slot = max_buffer_count; slot < BufferQueueDefs::NUM_BUFFER_SLOTS; ++slot) {
|
||||||
const auto state = slots[slot].buffer_state;
|
const auto state = slots[slot].buffer_state;
|
||||||
if (state == BufferState::Queued || state == BufferState::Dequeued) {
|
if (state == BufferState::Queued || state == BufferState::Dequeued) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
@@ -10,20 +10,31 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/hle/service/nvnflinger/buffer_item.h"
|
#include "core/hle/service/nvnflinger/buffer_item.h"
|
||||||
#include "core/hle/service/nvnflinger/buffer_queue_defs.h"
|
#include "core/hle/service/nvnflinger/buffer_queue_defs.h"
|
||||||
|
#include "core/hle/service/nvnflinger/buffer_slot.h"
|
||||||
#include "core/hle/service/nvnflinger/pixel_format.h"
|
#include "core/hle/service/nvnflinger/pixel_format.h"
|
||||||
#include "core/hle/service/nvnflinger/status.h"
|
#include "core/hle/service/nvnflinger/status.h"
|
||||||
#include "core/hle/service/nvnflinger/window.h"
|
#include "core/hle/service/nvnflinger/window.h"
|
||||||
|
|
||||||
namespace Service::android {
|
namespace Service::android {
|
||||||
|
|
||||||
|
struct BufferHistoryInfo {
|
||||||
|
u64 frame_number{};
|
||||||
|
s64 queue_time{};
|
||||||
|
s64 presentation_time{};
|
||||||
|
BufferState state{};
|
||||||
|
};
|
||||||
|
|
||||||
class IConsumerListener;
|
class IConsumerListener;
|
||||||
class IProducerListener;
|
class IProducerListener;
|
||||||
|
|
||||||
@@ -33,10 +44,14 @@ class BufferQueueCore final {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
static constexpr s32 INVALID_BUFFER_SLOT = BufferItem::INVALID_BUFFER_SLOT;
|
static constexpr s32 INVALID_BUFFER_SLOT = BufferItem::INVALID_BUFFER_SLOT;
|
||||||
|
static constexpr u32 BUFFER_HISTORY_SIZE = 8;
|
||||||
|
|
||||||
BufferQueueCore();
|
BufferQueueCore();
|
||||||
~BufferQueueCore();
|
~BufferQueueCore();
|
||||||
|
|
||||||
|
void PushHistory(u64 frame_number, s64 queue_time, s64 presentation_time, BufferState state);
|
||||||
|
void UpdateHistory(u64 frame_number, BufferState state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void SignalDequeueCondition();
|
void SignalDequeueCondition();
|
||||||
bool WaitForDequeueCondition(std::unique_lock<std::mutex>& lk);
|
bool WaitForDequeueCondition(std::unique_lock<std::mutex>& lk);
|
||||||
@@ -72,6 +87,11 @@ private:
|
|||||||
const s32 max_acquired_buffer_count{}; // This is always zero on HOS
|
const s32 max_acquired_buffer_count{}; // This is always zero on HOS
|
||||||
bool buffer_has_been_queued{};
|
bool buffer_has_been_queued{};
|
||||||
u64 frame_counter{};
|
u64 frame_counter{};
|
||||||
|
|
||||||
|
std::unordered_map<u64, BufferHistoryInfo> buffer_history_map{};
|
||||||
|
mutable std::mutex buffer_history_mutex{};
|
||||||
|
std::deque<u64> buffer_history_order;
|
||||||
|
|
||||||
u32 transform_hint{};
|
u32 transform_hint{};
|
||||||
bool is_allocating{};
|
bool is_allocating{};
|
||||||
mutable std::condition_variable_any is_allocating_condition;
|
mutable std::condition_variable_any is_allocating_condition;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
#include "common/settings.h"
|
||||||
#include "core/hle/kernel/k_event.h"
|
#include "core/hle/kernel/k_event.h"
|
||||||
#include "core/hle/kernel/k_readable_event.h"
|
#include "core/hle/kernel/k_readable_event.h"
|
||||||
#include "core/hle/kernel/kernel.h"
|
#include "core/hle/kernel/kernel.h"
|
||||||
@@ -26,7 +27,7 @@ BufferQueueProducer::BufferQueueProducer(Service::KernelHelpers::ServiceContext&
|
|||||||
std::shared_ptr<BufferQueueCore> buffer_queue_core_,
|
std::shared_ptr<BufferQueueCore> buffer_queue_core_,
|
||||||
Service::Nvidia::NvCore::NvMap& nvmap_)
|
Service::Nvidia::NvCore::NvMap& nvmap_)
|
||||||
: service_context{service_context_}, core{std::move(buffer_queue_core_)}, slots(core->slots),
|
: service_context{service_context_}, core{std::move(buffer_queue_core_)}, slots(core->slots),
|
||||||
nvmap(nvmap_) {
|
clock{Common::CreateOptimalClock()}, nvmap(nvmap_) {
|
||||||
buffer_wait_event = service_context.CreateEvent("BufferQueue:WaitEvent");
|
buffer_wait_event = service_context.CreateEvent("BufferQueue:WaitEvent");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,8 +429,7 @@ Status BufferQueueProducer::AttachBuffer(s32* out_slot,
|
|||||||
return return_flags;
|
return return_flags;
|
||||||
}
|
}
|
||||||
|
|
||||||
Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
|
Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, QueueBufferOutput* output) {
|
||||||
QueueBufferOutput* output) {
|
|
||||||
s64 timestamp{};
|
s64 timestamp{};
|
||||||
bool is_auto_timestamp{};
|
bool is_auto_timestamp{};
|
||||||
Common::Rectangle<s32> crop;
|
Common::Rectangle<s32> crop;
|
||||||
@@ -440,8 +440,7 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
|
|||||||
s32 swap_interval{};
|
s32 swap_interval{};
|
||||||
Fence fence{};
|
Fence fence{};
|
||||||
|
|
||||||
input.Deflate(×tamp, &is_auto_timestamp, &crop, &scaling_mode, &transform,
|
input.Deflate(×tamp, &is_auto_timestamp, &crop, &scaling_mode, &transform, &sticky_transform_, &async, &swap_interval, &fence);
|
||||||
&sticky_transform_, &async, &swap_interval, &fence);
|
|
||||||
|
|
||||||
switch (scaling_mode) {
|
switch (scaling_mode) {
|
||||||
case NativeWindowScalingMode::Freeze:
|
case NativeWindowScalingMode::Freeze:
|
||||||
@@ -455,10 +454,9 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
|
|||||||
return Status::BadValue;
|
return Status::BadValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<IConsumerListener> frame_available_listener;
|
|
||||||
std::shared_ptr<IConsumerListener> frame_replaced_listener;
|
|
||||||
s32 callback_ticket{};
|
|
||||||
BufferItem item;
|
BufferItem item;
|
||||||
|
std::shared_ptr<IConsumerListener> listener_available;
|
||||||
|
std::shared_ptr<IConsumerListener> listener_replaced;
|
||||||
|
|
||||||
{
|
{
|
||||||
std::scoped_lock lock{core->mutex};
|
std::scoped_lock lock{core->mutex};
|
||||||
@@ -469,127 +467,82 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const s32 max_buffer_count = core->GetMaxBufferCountLocked(async);
|
const s32 max_buffer_count = core->GetMaxBufferCountLocked(async);
|
||||||
if (async && core->override_max_buffer_count) {
|
|
||||||
if (core->override_max_buffer_count < max_buffer_count) {
|
|
||||||
LOG_ERROR(Service_Nvnflinger, "async mode is invalid with "
|
|
||||||
"buffer count override");
|
|
||||||
return Status::BadValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slot < 0 || slot >= max_buffer_count) {
|
if (slot < 0 || slot >= max_buffer_count) {
|
||||||
LOG_ERROR(Service_Nvnflinger, "slot index {} out of range [0, {})", slot,
|
LOG_ERROR(Service_Nvnflinger, "slot {} out of range [0, {})", slot, max_buffer_count);
|
||||||
max_buffer_count);
|
|
||||||
return Status::BadValue;
|
return Status::BadValue;
|
||||||
} else if (slots[slot].buffer_state != BufferState::Dequeued) {
|
}
|
||||||
LOG_ERROR(Service_Nvnflinger,
|
if (slots[slot].buffer_state != BufferState::Dequeued) {
|
||||||
"slot {} is not owned by the producer "
|
LOG_ERROR(Service_Nvnflinger, "slot {} is not owned by producer", slot);
|
||||||
"(state = {})",
|
|
||||||
slot, slots[slot].buffer_state);
|
|
||||||
return Status::BadValue;
|
return Status::BadValue;
|
||||||
} else if (!slots[slot].request_buffer_called) {
|
}
|
||||||
LOG_ERROR(Service_Nvnflinger,
|
if (!slots[slot].request_buffer_called) {
|
||||||
"slot {} was queued without requesting "
|
LOG_ERROR(Service_Nvnflinger, "slot {} was queued without request", slot);
|
||||||
"a buffer",
|
|
||||||
slot);
|
|
||||||
return Status::BadValue;
|
return Status::BadValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG(Service_Nvnflinger,
|
|
||||||
"slot={} frame={} time={} crop=[{},{},{},{}] transform={} scale={}", slot,
|
|
||||||
core->frame_counter + 1, timestamp, crop.Left(), crop.Top(), crop.Right(),
|
|
||||||
crop.Bottom(), transform, scaling_mode);
|
|
||||||
|
|
||||||
const std::shared_ptr<GraphicBuffer>& graphic_buffer(slots[slot].graphic_buffer);
|
|
||||||
Common::Rectangle<s32> buffer_rect(graphic_buffer->Width(), graphic_buffer->Height());
|
|
||||||
Common::Rectangle<s32> cropped_rect;
|
|
||||||
[[maybe_unused]] const bool unused = crop.Intersect(buffer_rect, &cropped_rect);
|
|
||||||
|
|
||||||
if (cropped_rect != crop) {
|
|
||||||
LOG_ERROR(Service_Nvnflinger, "crop rect is not contained within the buffer in slot {}",
|
|
||||||
slot);
|
|
||||||
return Status::BadValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
slots[slot].fence = fence;
|
|
||||||
slots[slot].buffer_state = BufferState::Queued;
|
|
||||||
++core->frame_counter;
|
++core->frame_counter;
|
||||||
|
slots[slot].buffer_state = BufferState::Queued;
|
||||||
slots[slot].frame_number = core->frame_counter;
|
slots[slot].frame_number = core->frame_counter;
|
||||||
|
slots[slot].queue_time = timestamp;
|
||||||
|
slots[slot].presentation_time = clock->GetTimeNS().count();
|
||||||
|
slots[slot].fence = fence;
|
||||||
|
|
||||||
item.acquire_called = slots[slot].acquire_called;
|
item.slot = slot;
|
||||||
item.graphic_buffer = slots[slot].graphic_buffer;
|
item.graphic_buffer = slots[slot].graphic_buffer;
|
||||||
item.crop = crop;
|
item.frame_number = core->frame_counter;
|
||||||
item.transform = transform & ~NativeWindowTransform::InverseDisplay;
|
|
||||||
item.transform_to_display_inverse =
|
|
||||||
(transform & NativeWindowTransform::InverseDisplay) != NativeWindowTransform::None;
|
|
||||||
item.scaling_mode = static_cast<u32>(scaling_mode);
|
|
||||||
item.timestamp = timestamp;
|
item.timestamp = timestamp;
|
||||||
item.is_auto_timestamp = is_auto_timestamp;
|
item.is_auto_timestamp = is_auto_timestamp;
|
||||||
item.frame_number = core->frame_counter;
|
item.crop = crop;
|
||||||
item.slot = slot;
|
item.transform = transform & ~NativeWindowTransform::InverseDisplay;
|
||||||
|
item.transform_to_display_inverse = (transform & NativeWindowTransform::InverseDisplay) != NativeWindowTransform::None;
|
||||||
|
item.scaling_mode = static_cast<u32>(scaling_mode);
|
||||||
item.fence = fence;
|
item.fence = fence;
|
||||||
item.is_droppable = core->dequeue_buffer_cannot_block || async;
|
item.is_droppable = core->dequeue_buffer_cannot_block || async;
|
||||||
item.swap_interval = swap_interval;
|
item.swap_interval = swap_interval;
|
||||||
|
item.acquire_called = slots[slot].acquire_called;
|
||||||
|
|
||||||
sticky_transform = sticky_transform_;
|
sticky_transform = sticky_transform_;
|
||||||
|
|
||||||
if (core->queue.empty()) {
|
if (core->queue.empty()) {
|
||||||
// When the queue is empty, we can simply queue this buffer
|
|
||||||
core->queue.push_back(item);
|
core->queue.push_back(item);
|
||||||
frame_available_listener = core->consumer_listener;
|
listener_available = core->consumer_listener;
|
||||||
} else {
|
} else {
|
||||||
// When the queue is not empty, we need to look at the front buffer
|
auto front = core->queue.begin();
|
||||||
// state to see if we need to replace it
|
if (front->is_droppable && core->StillTracking(*front)) {
|
||||||
auto front(core->queue.begin());
|
slots[front->slot].buffer_state = BufferState::Free;
|
||||||
|
if (Settings::values.enable_buffer_history.GetValue()) {
|
||||||
|
core->UpdateHistory(front->frame_number, BufferState::Free);
|
||||||
|
}
|
||||||
|
slots[front->slot].frame_number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (front->is_droppable) {
|
if (front->is_droppable) {
|
||||||
// If the front queued buffer is still being tracked, we first
|
|
||||||
// mark it as freed
|
|
||||||
if (core->StillTracking(*front)) {
|
|
||||||
slots[front->slot].buffer_state = BufferState::Free;
|
|
||||||
// Reset the frame number of the freed buffer so that it is the first in line to
|
|
||||||
// be dequeued again
|
|
||||||
slots[front->slot].frame_number = 0;
|
|
||||||
}
|
|
||||||
// Overwrite the droppable buffer with the incoming one
|
|
||||||
*front = item;
|
*front = item;
|
||||||
frame_replaced_listener = core->consumer_listener;
|
listener_replaced = core->consumer_listener;
|
||||||
} else {
|
} else {
|
||||||
core->queue.push_back(item);
|
core->queue.push_back(item);
|
||||||
frame_available_listener = core->consumer_listener;
|
listener_available = core->consumer_listener;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Settings::values.enable_buffer_history.GetValue()) {
|
||||||
|
core->PushHistory(core->frame_counter, slots[slot].queue_time, slots[slot].presentation_time, BufferState::Queued);
|
||||||
|
}
|
||||||
|
|
||||||
core->buffer_has_been_queued = true;
|
core->buffer_has_been_queued = true;
|
||||||
core->SignalDequeueCondition();
|
core->SignalDequeueCondition();
|
||||||
output->Inflate(core->default_width, core->default_height, core->transform_hint,
|
|
||||||
static_cast<u32>(core->queue.size()));
|
|
||||||
|
|
||||||
// Take a ticket for the callback functions
|
output->Inflate(core->default_width, core->default_height, core->transform_hint, static_cast<u32>(core->queue.size()));
|
||||||
callback_ticket = next_callback_ticket++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send the GraphicBuffer through the callback, and don't send the slot number, since the
|
|
||||||
// consumer shouldn't need it
|
|
||||||
item.graphic_buffer.reset();
|
item.graphic_buffer.reset();
|
||||||
item.slot = BufferItem::INVALID_BUFFER_SLOT;
|
item.slot = BufferItem::INVALID_BUFFER_SLOT;
|
||||||
|
|
||||||
// Call back without the main BufferQueue lock held, but with the callback lock held so we can
|
if (listener_available) {
|
||||||
// ensure that callbacks occur in order
|
listener_available->OnFrameAvailable(item);
|
||||||
{
|
} else if (listener_replaced) {
|
||||||
std::scoped_lock lock{callback_mutex};
|
listener_replaced->OnFrameReplaced(item);
|
||||||
while (callback_ticket != current_callback_ticket) {
|
|
||||||
callback_condition.wait(callback_mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame_available_listener != nullptr) {
|
|
||||||
frame_available_listener->OnFrameAvailable(item);
|
|
||||||
} else if (frame_replaced_listener != nullptr) {
|
|
||||||
frame_replaced_listener->OnFrameReplaced(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
++current_callback_ticket;
|
|
||||||
callback_condition.notify_all();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Status::NoError;
|
return Status::NoError;
|
||||||
@@ -810,6 +763,10 @@ Status BufferQueueProducer::SetPreallocatedBuffer(s32 slot,
|
|||||||
return Status::NoError;
|
return Status::NoError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Kernel::KReadableEvent* BufferQueueProducer::GetNativeHandle(u32 type_id) {
|
||||||
|
return &buffer_wait_event->GetReadableEvent();
|
||||||
|
}
|
||||||
|
|
||||||
void BufferQueueProducer::Transact(u32 code, std::span<const u8> parcel_data,
|
void BufferQueueProducer::Transact(u32 code, std::span<const u8> parcel_data,
|
||||||
std::span<u8> parcel_reply, u32 flags) {
|
std::span<u8> parcel_reply, u32 flags) {
|
||||||
// Values used by BnGraphicBufferProducer onTransact
|
// Values used by BnGraphicBufferProducer onTransact
|
||||||
@@ -929,9 +886,42 @@ void BufferQueueProducer::Transact(u32 code, std::span<const u8> parcel_data,
|
|||||||
status = SetBufferCount(buffer_count);
|
status = SetBufferCount(buffer_count);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TransactionId::GetBufferHistory:
|
case TransactionId::GetBufferHistory: {
|
||||||
LOG_DEBUG(Service_Nvnflinger, "(STUBBED) called, transaction=GetBufferHistory");
|
if (!Settings::values.enable_buffer_history.GetValue()) {
|
||||||
|
LOG_DEBUG(Service_Nvnflinger, "(STUBBED) called");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(Service_Nvnflinger, "called, transaction=GetBufferHistory");
|
||||||
|
|
||||||
|
const s32 request = parcel_in.Read<s32>();
|
||||||
|
if (request <= 0) {
|
||||||
|
parcel_out.Write(Status::BadValue);
|
||||||
|
parcel_out.Write<s32>(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<BufferHistoryInfo> snapshot;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::scoped_lock lk(core->buffer_history_mutex);
|
||||||
|
for (auto& [frame, info] : core->buffer_history_map) {
|
||||||
|
snapshot.push_back(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(snapshot.begin(), snapshot.end(), [](auto& a, auto& b){
|
||||||
|
return a.frame_number > b.frame_number;
|
||||||
|
});
|
||||||
|
|
||||||
|
const s32 limit = std::min(request, (s32)snapshot.size());
|
||||||
|
parcel_out.Write(Status::NoError);
|
||||||
|
parcel_out.Write<s32>(limit);
|
||||||
|
for (s32 i = 0; i < limit; ++i) {
|
||||||
|
parcel_out.Write(snapshot[i]);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
ASSERT_MSG(false, "Unimplemented TransactionId {}", code);
|
ASSERT_MSG(false, "Unimplemented TransactionId {}", code);
|
||||||
break;
|
break;
|
||||||
@@ -944,8 +934,4 @@ void BufferQueueProducer::Transact(u32 code, std::span<const u8> parcel_data,
|
|||||||
(std::min)(parcel_reply.size(), serialized.size()));
|
(std::min)(parcel_reply.size(), serialized.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Kernel::KReadableEvent* BufferQueueProducer::GetNativeHandle(u32 type_id) {
|
|
||||||
return &buffer_wait_event->GetReadableEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Service::android
|
} // namespace Service::android
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include "common/common_funcs.h"
|
#include "common/common_funcs.h"
|
||||||
|
#include "common/wall_clock.h"
|
||||||
#include "core/hle/service/nvdrv/nvdata.h"
|
#include "core/hle/service/nvdrv/nvdata.h"
|
||||||
#include "core/hle/service/nvnflinger/binder.h"
|
#include "core/hle/service/nvnflinger/binder.h"
|
||||||
#include "core/hle/service/nvnflinger/buffer_queue_defs.h"
|
#include "core/hle/service/nvnflinger/buffer_queue_defs.h"
|
||||||
@@ -88,6 +89,7 @@ private:
|
|||||||
s32 next_callback_ticket{};
|
s32 next_callback_ticket{};
|
||||||
s32 current_callback_ticket{};
|
s32 current_callback_ticket{};
|
||||||
std::condition_variable_any callback_condition;
|
std::condition_variable_any callback_condition;
|
||||||
|
std::unique_ptr<Common::WallClock> clock;
|
||||||
|
|
||||||
Service::Nvidia::NvCore::NvMap& nvmap;
|
Service::Nvidia::NvCore::NvMap& nvmap;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||||
@@ -37,6 +37,7 @@ struct BufferSlot final {
|
|||||||
bool needs_cleanup_on_release{};
|
bool needs_cleanup_on_release{};
|
||||||
bool attached_by_consumer{};
|
bool attached_by_consumer{};
|
||||||
bool is_preallocated{};
|
bool is_preallocated{};
|
||||||
|
s64 queue_time{}, presentation_time{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Service::android
|
} // namespace Service::android
|
||||||
|
|||||||
+32
-4
@@ -1,4 +1,4 @@
|
|||||||
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
@@ -102,6 +102,8 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
|||||||
auto* patch = &patches->operator[](patch_index);
|
auto* patch = &patches->operator[](patch_index);
|
||||||
if (patch->GetPatchMode() == Core::NCE::PatchMode::PreText) {
|
if (patch->GetPatchMode() == Core::NCE::PatchMode::PreText) {
|
||||||
return patch->GetSectionSize();
|
return patch->GetSectionSize();
|
||||||
|
} else if (patch->GetPatchMode() == Core::NCE::PatchMode::Split) {
|
||||||
|
return patch->GetPreSectionSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -178,12 +180,26 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
|||||||
}
|
}
|
||||||
} else if (patch) {
|
} else if (patch) {
|
||||||
// Relocate code patch and copy to the program_image.
|
// Relocate code patch and copy to the program_image.
|
||||||
|
// Save size before RelocateAndCopy (which may resize)
|
||||||
|
const size_t size_before_relocate = program_image.size();
|
||||||
if (patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers())) {
|
if (patch->RelocateAndCopy(load_base, code, program_image, &process.GetPostHandlers())) {
|
||||||
// Update patch section.
|
// Update patch section.
|
||||||
auto& patch_segment = codeset.PatchSegment();
|
auto& patch_segment = codeset.PatchSegment();
|
||||||
patch_segment.addr =
|
auto& post_patch_segment = codeset.PostPatchSegment();
|
||||||
patch->GetPatchMode() == Core::NCE::PatchMode::PreText ? 0 : image_size;
|
const auto patch_mode = patch->GetPatchMode();
|
||||||
patch_segment.size = static_cast<u32>(patch->GetSectionSize());
|
if (patch_mode == Core::NCE::PatchMode::PreText) {
|
||||||
|
patch_segment.addr = 0;
|
||||||
|
patch_segment.size = static_cast<u32>(patch->GetSectionSize());
|
||||||
|
} else if (patch_mode == Core::NCE::PatchMode::Split) {
|
||||||
|
// For Split-mode, we are using pre-patch buffer at start, post-patch buffer at end
|
||||||
|
patch_segment.addr = 0;
|
||||||
|
patch_segment.size = static_cast<u32>(patch->GetPreSectionSize());
|
||||||
|
post_patch_segment.addr = size_before_relocate;
|
||||||
|
post_patch_segment.size = static_cast<u32>(patch->GetSectionSize());
|
||||||
|
} else {
|
||||||
|
patch_segment.addr = image_size;
|
||||||
|
patch_segment.size = static_cast<u32>(patch->GetSectionSize());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh image_size to take account the patch section if it was added by RelocateAndCopy
|
// Refresh image_size to take account the patch section if it was added by RelocateAndCopy
|
||||||
@@ -193,6 +209,18 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
|
|||||||
|
|
||||||
// If we aren't actually loading (i.e. just computing the process code layout), we are done
|
// If we aren't actually loading (i.e. just computing the process code layout), we are done
|
||||||
if (!load_into_process) {
|
if (!load_into_process) {
|
||||||
|
#ifdef HAS_NCE
|
||||||
|
// Ok, so for Split mode, we need to account for pre-patch and post-patch space
|
||||||
|
// which will be added during RelocateAndCopy in the second pass. Where it crashed
|
||||||
|
// in Android Studio at PreText. May be a better way. Works for now.
|
||||||
|
if (patch && patch->GetPatchMode() == Core::NCE::PatchMode::Split) {
|
||||||
|
return load_base + patch->GetPreSectionSize() + image_size + patch->GetSectionSize();
|
||||||
|
} else if (patch && patch->GetPatchMode() == Core::NCE::PatchMode::PreText) {
|
||||||
|
return load_base + patch->GetSectionSize() + image_size;
|
||||||
|
} else if (patch && patch->GetPatchMode() == Core::NCE::PatchMode::PostData) {
|
||||||
|
return load_base + image_size + patch->GetSectionSize();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return load_base + image_size;
|
return load_base + image_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -433,9 +433,10 @@ int RegAlloc::AllocateRegister(const std::array<HostLocInfo, 32>& regs, const st
|
|||||||
std::vector<int> candidates;
|
std::vector<int> candidates;
|
||||||
std::copy_if(order.begin(), order.end(), std::back_inserter(candidates), [&](int i) { return regs[i].MaybeAllocatable(); });
|
std::copy_if(order.begin(), order.end(), std::back_inserter(candidates), [&](int i) { return regs[i].MaybeAllocatable(); });
|
||||||
|
|
||||||
// TODO: LRU
|
// TODO: The candidate was chosen randomly before, and a LRU was
|
||||||
std::uniform_int_distribution<size_t> dis{0, candidates.size() - 1};
|
// suggested as an improvement. However, using an incrementing index
|
||||||
return candidates[dis(rand_gen)];
|
// seems to be close enough. Determine if an LRU is still needed.
|
||||||
|
return candidates[alloc_candidate_index++ % candidates.size()];
|
||||||
}
|
}
|
||||||
|
|
||||||
void RegAlloc::SpillGpr(int index) {
|
void RegAlloc::SpillGpr(int index) {
|
||||||
|
|||||||
@@ -160,8 +160,12 @@ class RegAlloc final {
|
|||||||
public:
|
public:
|
||||||
using ArgumentInfo = std::array<Argument, IR::max_arg_count>;
|
using ArgumentInfo = std::array<Argument, IR::max_arg_count>;
|
||||||
|
|
||||||
explicit RegAlloc(oaknut::CodeGenerator& code, FpsrManager& fpsr_manager, std::vector<int> gpr_order, std::vector<int> fpr_order)
|
explicit RegAlloc(oaknut::CodeGenerator& code, FpsrManager& fpsr_manager, std::vector<int> gpr_order, std::vector<int> fpr_order) noexcept
|
||||||
: code{code}, fpsr_manager{fpsr_manager}, gpr_order{gpr_order}, fpr_order{fpr_order}, rand_gen{std::random_device{}()} {}
|
: code{code}
|
||||||
|
, fpsr_manager{fpsr_manager}
|
||||||
|
, gpr_order{gpr_order}
|
||||||
|
, fpr_order{fpr_order}
|
||||||
|
{}
|
||||||
|
|
||||||
ArgumentInfo GetArgumentInfo(IR::Inst* inst);
|
ArgumentInfo GetArgumentInfo(IR::Inst* inst);
|
||||||
bool WasValueDefined(IR::Inst* inst) const;
|
bool WasValueDefined(IR::Inst* inst) const;
|
||||||
@@ -331,7 +335,7 @@ private:
|
|||||||
HostLocInfo flags;
|
HostLocInfo flags;
|
||||||
std::array<HostLocInfo, SpillCount> spills;
|
std::array<HostLocInfo, SpillCount> spills;
|
||||||
|
|
||||||
mutable std::mt19937 rand_gen;
|
mutable size_t alloc_candidate_index = 0;
|
||||||
ankerl::unordered_dense::set<const IR::Inst*> defined_insts;
|
ankerl::unordered_dense::set<const IR::Inst*> defined_insts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
@@ -12,7 +12,9 @@ add_library(frontend_common STATIC
|
|||||||
firmware_manager.cpp
|
firmware_manager.cpp
|
||||||
data_manager.h data_manager.cpp
|
data_manager.h data_manager.cpp
|
||||||
play_time_manager.cpp
|
play_time_manager.cpp
|
||||||
play_time_manager.h)
|
play_time_manager.h
|
||||||
|
settings_generator.h settings_generator.cpp
|
||||||
|
mod_manager.h mod_manager.cpp)
|
||||||
|
|
||||||
if (ENABLE_UPDATE_CHECKER)
|
if (ENABLE_UPDATE_CHECKER)
|
||||||
target_link_libraries(frontend_common PRIVATE httplib::httplib)
|
target_link_libraries(frontend_common PRIVATE httplib::httplib)
|
||||||
@@ -29,4 +31,6 @@ if (ENABLE_UPDATE_CHECKER)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
create_target_directory_groups(frontend_common)
|
create_target_directory_groups(frontend_common)
|
||||||
target_link_libraries(frontend_common PUBLIC core SimpleIni::SimpleIni PRIVATE common Boost::headers)
|
target_link_libraries(frontend_common
|
||||||
|
PUBLIC core SimpleIni::SimpleIni frozen::frozen-headers
|
||||||
|
PRIVATE common Boost::headers)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include "common/fs/fs.h"
|
||||||
|
#include "common/fs/fs_types.h"
|
||||||
|
#include "common/logging/backend.h"
|
||||||
|
#include "frontend_common/data_manager.h"
|
||||||
|
#include "mod_manager.h"
|
||||||
|
|
||||||
|
namespace FrontendCommon {
|
||||||
|
|
||||||
|
// TODO: Handle cases where the folder appears to contain multiple mods.
|
||||||
|
std::vector<std::filesystem::path> GetModFolder(const std::string& root) {
|
||||||
|
std::vector<std::filesystem::path> paths;
|
||||||
|
|
||||||
|
auto callback = [&paths](const std::filesystem::directory_entry& entry) -> bool {
|
||||||
|
const auto name = entry.path().filename().string();
|
||||||
|
static const std::array<std::string, 5> valid_names = {"exefs",
|
||||||
|
"romfs"
|
||||||
|
"romfs_ext",
|
||||||
|
"cheats", "romfslite"};
|
||||||
|
|
||||||
|
if (std::ranges::find(valid_names, name) != valid_names.end()) {
|
||||||
|
paths.emplace_back(entry.path().parent_path());
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Common::FS::IterateDirEntriesRecursively(root, callback, Common::FS::DirEntryFilter::Directory);
|
||||||
|
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModInstallResult InstallMod(const std::filesystem::path& path, const u64 program_id, const bool copy) {
|
||||||
|
const auto program_id_string = fmt::format("{:016X}", program_id);
|
||||||
|
const auto mod_name = path.filename();
|
||||||
|
const auto mod_dir =
|
||||||
|
DataManager::GetDataDir(DataManager::DataDir::Mods) / program_id_string / mod_name;
|
||||||
|
|
||||||
|
// pre-emptively remove any existing mod here
|
||||||
|
std::filesystem::remove_all(mod_dir);
|
||||||
|
|
||||||
|
// now copy
|
||||||
|
try {
|
||||||
|
std::filesystem::copy(path, mod_dir, std::filesystem::copy_options::recursive);
|
||||||
|
if (!copy)
|
||||||
|
std::filesystem::remove_all(path);
|
||||||
|
} catch (std::exception& e) {
|
||||||
|
LOG_ERROR(Frontend, "Mod install failed with message {}", e.what());
|
||||||
|
return Failed;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(Frontend, "Copied mod from {} to {}", path.string(), mod_dir.string());
|
||||||
|
|
||||||
|
return Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace FrontendCommon
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace FrontendCommon {
|
||||||
|
|
||||||
|
enum ModInstallResult {
|
||||||
|
Cancelled,
|
||||||
|
Failed,
|
||||||
|
Success,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> GetModFolder(const std::string& root);
|
||||||
|
|
||||||
|
ModInstallResult InstallMod(const std::filesystem::path &path, const u64 program_id, const bool copy = true);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
#include <frozen/string.h>
|
||||||
|
#include "common/settings.h"
|
||||||
|
#include "settings_generator.h"
|
||||||
|
|
||||||
|
namespace FrontendCommon {
|
||||||
|
|
||||||
|
void GenerateSettings() {
|
||||||
|
static std::random_device rd;
|
||||||
|
|
||||||
|
// Web Token //
|
||||||
|
auto &token_setting = Settings::values.eden_token;
|
||||||
|
if (token_setting.GetValue().empty()) {
|
||||||
|
static constexpr const size_t token_length = 48;
|
||||||
|
static constexpr const frozen::string token_set = "abcdefghijklmnopqrstuvwxyz";
|
||||||
|
static std::uniform_int_distribution<int> token_dist(0, token_set.size() - 1);
|
||||||
|
std::string result;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < token_length; ++i) {
|
||||||
|
size_t idx = token_dist(rd);
|
||||||
|
result += token_set[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
token_setting.SetValue(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace FrontendCommon {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief GenerateSettings Generate platform-specific or randomized settings.
|
||||||
|
* Run this function at initialization time for your frontend.
|
||||||
|
*/
|
||||||
|
void GenerateSettings();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -176,6 +176,5 @@ std::optional<UpdateChecker::Update> UpdateChecker::GetUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
empty:
|
empty:
|
||||||
return UpdateChecker::Update{};
|
return std::nullopt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user