Compare commits

..

2 Commits

Author SHA1 Message Date
Caio Oliveira c1cc2f97ad [chore] license headers 2026-02-02 17:21:18 -03:00
wildcard 670764b535 [astc] add support for workgroup in astc 2026-02-02 17:20:58 -03:00
155 changed files with 1562 additions and 6810 deletions
+8 -9
View File
@@ -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_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R6/lib")
elseif (PLATFORM_NETBSD)
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 -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
endif()
@@ -178,9 +178,7 @@ endif()
# Disable Warnings as Errors for MSVC
if (MSVC AND NOT CXX_CLANG)
# 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-")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W3 /WX-")
endif()
# Set bundled sdl2/qt as dependent options.
@@ -246,12 +244,11 @@ cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib"
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
set(DEFAULT_ENABLE_OPENSSL ON)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN)
# - Windows defaults to the Schannel backend.
# - macOS defaults to the SecureTransport backend.
# - 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.
# - Solaris and OpenBSD have too old backends
# But you can force builds for those platforms to use OpenSSL if you have
# your own copy of it.
set(DEFAULT_ENABLE_OPENSSL OFF)
@@ -264,7 +261,7 @@ endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
if (EXT_DEFAULT OR PLATFORM_SUN)
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
endif()
@@ -590,7 +587,6 @@ if (NOT YUZU_STATIC_ROOM)
find_package(sirit)
find_package(gamemode)
find_package(mcl)
find_package(frozen)
if (ARCHITECTURE_riscv64)
find_package(biscuit)
@@ -702,6 +698,9 @@ if (ENABLE_QT)
# QuaZip
AddJsonPackage(quazip)
# frozen
AddJsonPackage(frozen)
endif()
if (NOT YUZU_STATIC_ROOM AND NOT (YUZU_USE_BUNDLED_FFMPEG OR YUZU_USE_EXTERNAL_FFMPEG))
+125 -192
View File
@@ -41,11 +41,6 @@ function(cpm_utils_message level name message)
message(${level} "[CPMUtil] ${name}: ${message}")
endfunction()
# propagate a variable to parent scope
macro(Propagate var)
set(${var} ${${var}} PARENT_SCOPE)
endmacro()
function(array_to_list array length out)
math(EXPR range "${length} - 1")
@@ -77,159 +72,6 @@ function(get_json_element object out member default)
set("${out}" "${outvar}" PARENT_SCOPE)
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
function(AddJsonPackage)
set(oneValueArgs
@@ -238,8 +80,7 @@ function(AddJsonPackage)
# these are overrides that can be generated at runtime,
# so can be defined separately from the json
DOWNLOAD_ONLY
BUNDLED_PACKAGE
FORCE_BUNDLED_PACKAGE)
BUNDLED_PACKAGE)
set(multiValueArgs OPTIONS)
@@ -270,9 +111,24 @@ function(AddJsonPackage)
cpm_utils_message(FATAL_ERROR ${JSON_NAME} "Not found in cpmfile")
endif()
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()
AddCIPackage(
VERSION ${version}
NAME ${name}
@@ -282,38 +138,116 @@ function(AddJsonPackage)
MIN_VERSION ${min_version}
DISABLED_PLATFORMS ${disabled_platforms})
else()
if (NOT DEFINED JSON_FORCE_BUNDLED_PACKAGE)
set(JSON_FORCE_BUNDLED_PACKAGE OFF)
endif()
# 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)
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})
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()
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()
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
Propagate(${package}_ADDED)
Propagate(${package}_SOURCE_DIR)
Propagate(${package}_BINARY_DIR)
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)
endfunction()
function(AddPackage)
@@ -409,7 +343,7 @@ function(AddPackage)
if(DEFINED PKG_ARGS_ARTIFACT)
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()
set(pkg_url
${pkg_git_url}/archive/refs/tags/${PKG_ARGS_TAG}.tar.gz)
@@ -691,8 +625,7 @@ function(AddCIPackage)
endif()
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(
NAME ${ARTIFACT_PACKAGE}
+6
View File
@@ -114,5 +114,11 @@
"QUAZIP_INSTALL OFF",
"QUAZIP_ENABLE_QTEXTCODEC OFF"
]
},
"frozen": {
"package": "frozen",
"repo": "serge-sans-paille/frozen",
"sha": "61dce5ae18",
"hash": "b8dfe741c82bc178dfc9749d4ab5a130cee718d9ee7b71d9b547cf5f7f23027ed0152ad250012a8546399fcc1e12187efc68d89d6731256c4d2df7d04eef8d5c"
}
}
+1 -1
View File
@@ -18,7 +18,7 @@
src/web_service @AleksandrPopovich
src/dynarmic @Lizzie
src/core @Lizzie @Maufeat @PavelBARABANOV @MrPurple666 @JPikachu
src/core/hle @Maufeat @PavelBARABANOV
src/core/hle @Maufeat @PavelBARABANOV @SDK-Chan
src/core/arm @Lizzie @MrPurple666
src/*_room @AleksandrPopovich
src/video_core @CamilleLaVey @MaranBr @Wildcard @weakboson
-41
View File
@@ -1,41 +0,0 @@
# 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.
-4
View File
@@ -31,10 +31,6 @@ The core of CPMUtil is the [`AddPackage`](./AddPackage.md) function. [`AddPackag
[`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.md) adds a specific version of Qt to your project.
-2
View File
@@ -85,8 +85,6 @@ If you have `quazip1_qt6_devel`, uninstall it. It may call `Core5Compat` on CMak
## 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`.
`-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
View File
@@ -277,7 +277,7 @@ For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv
```sh
pkg_add -u
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
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
```
[Caveats](./Caveats.md#openbsd).
+5 -5
View File
@@ -82,15 +82,15 @@ You may additionally need the `Qt Extension Pack` extension if building Qt.
# Build speedup
If you have an HDD, use ramdisk (build in RAM), approximatedly you need 4GB for a full build with debug symbols:
If you have an HDD, use ramdisk (build in RAM):
```sh
mkdir /tmp/ramdisk
chmod 777 /tmp/ramdisk
sudo mkdir /tmp/ramdisk
sudo chmod 777 /tmp/ramdisk
# about 8GB needed
mount -t tmpfs -o size=4G myramdisk /tmp/ramdisk
sudo mount -t tmpfs -o size=8G myramdisk /tmp/ramdisk
cmake -B /tmp/ramdisk
cmake --build /tmp/ramdisk -- -j32
umount /tmp/ramdisk
sudo umount /tmp/ramdisk
```
# Assets and large files
+6 -3
View File
@@ -84,10 +84,13 @@ endif()
AddJsonPackage(mcl)
# Vulkan stuff
AddDependentPackages(vulkan-headers vulkan-utility-libraries)
AddJsonPackage(vulkan-headers)
AddJsonPackage(vulkan-utility-libraries)
# frozen
AddJsonPackage(frozen)
# small hack
if (NOT VulkanUtilityLibraries_ADDED)
find_package(VulkanHeaders 1.3.274 REQUIRED)
endif()
# DiscordRPC
if (USE_DISCORD_PRESENCE)
+1 -7
View File
@@ -276,7 +276,7 @@
"vulkan-headers": {
"repo": "KhronosGroup/Vulkan-Headers",
"package": "VulkanHeaders",
"version": "1.4.317",
"version": "1.3.274",
"hash": "26e0ad8fa34ab65a91ca62ddc54cc4410d209a94f64f2817dcdb8061dc621539a4262eab6387e9b9aa421db3dbf2cf8e2a4b041b696d0d03746bae1f25191272",
"git_version": "1.4.342",
"tag": "v%VERSION%"
@@ -287,11 +287,5 @@
"hash": "8147370f964fd82c315d6bb89adeda30186098427bf3efaa641d36282d42a263f31e96e4586bfd7ae0410ff015379c19aa4512ba160630444d3d8553afd1ec14",
"git_version": "1.4.342",
"tag": "v%VERSION%"
},
"frozen": {
"package": "frozen",
"repo": "serge-sans-paille/frozen",
"sha": "61dce5ae18",
"hash": "b8dfe741c82bc178dfc9749d4ab5a130cee718d9ee7b71d9b547cf5f7f23027ed0152ad250012a8546399fcc1e12187efc68d89d6731256c4d2df7d04eef8d5c"
}
}
@@ -614,23 +614,4 @@ object NativeLibrary {
* Updates the device power state to global variables
*/
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 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -10,7 +10,6 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
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.viewholder.AbstractViewHolder
@@ -32,12 +31,7 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
binding.addonSwitch.isChecked = model.enabled
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
if (PatchType.from(model.type) == PatchType.Update && checked) {
addonViewModel.enableOnlyThisUpdate(model)
notifyDataSetChanged()
} else {
model.enabled = checked
}
model.enabled = checked
}
val deleteAction = {
@@ -1,55 +0,0 @@
// 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,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -10,10 +7,8 @@ import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.fragment.app.FragmentActivity
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.CardFolderBinding
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.GamesViewModel
import org.yuzu.yuzu_emu.utils.ViewUtils.marquee
@@ -36,12 +31,6 @@ class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesVie
path.text = Uri.parse(model.uriString).path
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 {
GameFolderPropertiesDialogFragment.newInstance(model)
.show(
@@ -1,112 +0,0 @@
// 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))
}
}
}
}
@@ -24,7 +24,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
ENABLE_BUFFER_HISTORY("enable_buffer_history"),
SYNC_MEMORY_OPERATIONS("sync_memory_operations"),
BUFFER_REORDER_DISABLE("disable_buffer_reorder"),
RENDERER_DEBUG("debug"),
@@ -745,13 +745,6 @@ abstract class SettingsItem(
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(
SwitchSetting(
BooleanSetting.SYNC_MEMORY_OPERATIONS,
@@ -275,7 +275,6 @@ class SettingsFragmentPresenter(
add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key)
add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key)
add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key)
add(BooleanSetting.ENABLE_BUFFER_HISTORY.key)
add(HeaderSetting(R.string.hacks))
@@ -1,459 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.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
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -110,7 +110,7 @@ class FreedrenoSettingsFragment : Fragment() {
val commonVars = listOf(
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
"TU_BREADCRUMBS", "FD_DEV_FEATURES"
"TU_BREADCRUMBS"
)
for (varName in commonVars) {
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -9,13 +6,11 @@ package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
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.GamesViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
@@ -30,18 +25,14 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
// Hide deepScan for external content, do automatically
if (gameDir.type == DirectoryType.EXTERNAL_CONTENT) {
binding.deepScanSwitch.visibility = View.GONE
} else {
// Restore checkbox state for game dirs
binding.deepScanSwitch.isChecked =
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
// Restore checkbox state
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
binding.deepScanSwitch.setOnClickListener {
deepScan = binding.deepScanSwitch.isChecked
}
}
return MaterialAlertDialogBuilder(requireContext())
@@ -50,10 +41,8 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
if (folderIndex != -1) {
if (gameDir.type == DirectoryType.GAME) {
gamesViewModel.folders.value[folderIndex].deepScan =
binding.deepScanSwitch.isChecked
}
gamesViewModel.folders.value[folderIndex].deepScan =
binding.deepScanSwitch.isChecked
gamesViewModel.updateGameDirs()
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -15,14 +15,11 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.FolderAdapter
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.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
@@ -76,25 +73,7 @@ class GameFoldersFragment : Fragment() {
val mainActivity = requireActivity() as MainActivity
binding.buttonAdd.setOnClickListener {
// 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()
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
setInsets()
@@ -1,9 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.Manifest
@@ -117,17 +114,6 @@ 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(
HomeSetting(
R.string.gpu_driver_manager,
@@ -1,190 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.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,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -51,68 +48,16 @@ class AddonViewModel : ViewModel() {
?: emptyArray()
).toMutableList()
patchList.sortBy { it.name }
// Ensure only one update is enabled
ensureSingleUpdateEnabled(patchList)
removeDuplicates(patchList)
_patchList.value = patchList
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?) {
_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) {
when (PatchType.from(patch.type)) {
PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
@@ -127,27 +72,13 @@ class AddonViewModel : ViewModel() {
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(
game!!.programId,
_patchList.value.mapNotNull {
if (it.enabled) {
null
} else {
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
}
it.name
}
}.toTypedArray()
)
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -12,14 +9,5 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class GameDir(
val uriString: String,
var deepScan: Boolean,
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
}
var deepScan: Boolean
) : Parcelable
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model
@@ -56,7 +56,7 @@ class GamesViewModel : ViewModel() {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
getGameDirsAndExternalContent()
getGameDirs()
reloadGames(directoriesChanged = false, firstStartup = true)
}
@@ -144,19 +144,11 @@ class GamesViewModel : ViewModel() {
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
viewModelScope.launch {
withContext(Dispatchers.IO) {
when (gameDir.type) {
DirectoryType.GAME -> {
NativeConfig.addGameDir(gameDir)
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.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()
}
}
NativeConfig.addGameDir(gameDir)
val isFirstTimeSetup = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.getBoolean(org.yuzu.yuzu_emu.features.settings.model.Settings.PREF_FIRST_APP_LAUNCH, true)
getGameDirs(!isFirstTimeSetup)
}
if (savedFromGameFragment) {
@@ -176,15 +168,8 @@ class GamesViewModel : ViewModel() {
val removedDirIndex = gameDirs.indexOf(gameDir)
if (removedDirIndex != -1) {
gameDirs.removeAt(removedDirIndex)
when (gameDir.type) {
DirectoryType.GAME -> {
NativeConfig.setGameDirs(gameDirs.filter { it.type == DirectoryType.GAME }.toTypedArray())
}
DirectoryType.EXTERNAL_CONTENT -> {
removeExternalContentDir(gameDir.uriString)
}
}
getGameDirsAndExternalContent()
NativeConfig.setGameDirs(gameDirs.toTypedArray())
getGameDirs()
}
}
}
@@ -192,16 +177,15 @@ class GamesViewModel : ViewModel() {
fun updateGameDirs() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
val gameDirs = _folders.value.filter { it.type == DirectoryType.GAME }
NativeConfig.setGameDirs(gameDirs.toTypedArray())
getGameDirsAndExternalContent()
NativeConfig.setGameDirs(_folders.value.toTypedArray())
getGameDirs()
}
}
fun onOpenGameFoldersFragment() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirsAndExternalContent()
getGameDirs()
}
}
@@ -209,36 +193,16 @@ class GamesViewModel : ViewModel() {
NativeConfig.saveGlobalConfig()
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirsAndExternalContent(true)
getGameDirs(true)
}
}
}
private fun getGameDirsAndExternalContent(reloadList: Boolean = false) {
val gameDirs = NativeConfig.getGameDirs().toMutableList()
val externalContentDirs = NativeConfig.getExternalContentDirs().map {
GameDir(it, false, DirectoryType.EXTERNAL_CONTENT)
}
gameDirs.addAll(externalContentDirs)
_folders.value = gameDirs
private fun getGameDirs(reloadList: Boolean = false) {
val gameDirs = NativeConfig.getGameDirs()
_folders.value = gameDirs.toMutableList()
if (reloadList) {
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,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -15,6 +12,5 @@ data class Patch(
val version: String,
val type: Int,
val programId: String,
val titleId: String,
val numericVersion: Long = 0
val titleId: String
)
@@ -1,21 +0,0 @@
// 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("-", "")
}
}
@@ -63,7 +63,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.documentfile.provider.DocumentFile
class MainActivity : AppCompatActivity(), ThemeProvider {
private lateinit var binding: ActivityMainBinding
@@ -200,7 +199,8 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
var artifact = tag
// Nightly builds have a slightly different format
if (NativeLibrary.isNightlyBuild()) {
artifact = tag.substringAfter('.', tag)
val splitTag = tag.split('.')
artifact = splitTag.subList(1, splitTag.size - 1).joinToString(".")
}
downloadAndInstallUpdate(tag, artifact)
}
@@ -389,13 +389,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
val getExternalContentDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
processExternalContentDir(result)
}
}
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
contentResolver.takePersistableUriPermission(
result,
@@ -417,27 +410,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
.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 ->
if (result != null) {
processKey(result, "keys")
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -25,7 +25,6 @@ object DirectoryInitialization {
initializeInternalStorage()
NativeLibrary.initializeSystem(false)
NativeConfig.initializeGlobalConfig()
NativeLibrary.reloadProfiles()
migrateSettings()
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-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
import android.content.SharedPreferences
@@ -49,17 +49,6 @@ object GameHelper {
// Remove previous filesystem provider information so we can get up to date version info
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>()
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
val gameDirUri = gameDir.uriString.toUri()
@@ -99,33 +88,6 @@ object GameHelper {
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(
games: MutableList<Game>,
files: Array<MinimalDocumentFile>,
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -204,12 +204,4 @@ object NativeConfig {
external fun getSdmcDir(): String
@Synchronized
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 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -144,15 +144,6 @@ 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(
name = "Performance - Default",
description = "Clear all debug options for performance",
@@ -168,7 +159,6 @@ object FreedrenoPresets {
CAPTURE_FRAMES,
SHADER_DEBUG,
GPU_HANG_TRACE,
DEV_FEATURES_UBWC_HINT,
PERFORMANCE_DEFAULT
)
}
@@ -1,9 +1,8 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <common/fs/path_util.h>
#include <common/logging/log.h>
#include <common/settings.h>
#include <input_common/main.h>
#include "android_config.h"
#include "android_settings.h"
@@ -70,18 +69,6 @@ void AndroidConfig::ReadPathValues() {
}
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"));
if (!nand_dir_setting.empty()) {
Common::FS::SetEdenPath(Common::FS::EdenPath::NANDDir, nand_dir_setting);
@@ -254,14 +241,6 @@ void AndroidConfig::SavePathValues() {
}
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
const auto nand_path = Common::FS::GetEdenPathString(Common::FS::EdenPath::NANDDir);
WriteStringSetting(std::string("nand_directory"), nand_path,
+13 -496
View File
@@ -49,17 +49,12 @@
#include "common/settings.h"
#include "common/string_util.h"
#include "frontend_common/play_time_manager.h"
#include "core/constants.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/control_metadata.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/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
@@ -215,109 +210,6 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
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);
if (!loader) {
return;
@@ -334,6 +226,17 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
m_manual_provider->AddEntry(FileSys::TitleType::Application,
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
program_id, file);
} else if (res2 == Loader::ResultStatus::Success &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
const auto nsp = file_type == Loader::FileType::NSP
? std::make_shared<FileSys::NSP>(file)
: FileSys::XCI{file}.GetSecurePartitionNSP();
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
entry.second->GetBaseFile());
}
}
}
}
@@ -1428,8 +1331,7 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
Common::Android::ToJString(env, patch.name),
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.title_id)),
static_cast<jlong>(patch.numeric_version));
Common::Android::ToJString(env, std::to_string(patch.title_id)));
env->SetObjectArrayElement(jpatchArray, i, jpatch);
++i;
}
@@ -1776,7 +1678,7 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
#endif
}
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", version_str, variant);
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
std::string{Common::g_build_auto_update_website},
std::string{Common::g_build_auto_update_repo},
@@ -1784,7 +1686,6 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
apk_filename);
env->ReleaseStringUTFChars(tag, version_str);
env->ReleaseStringUTFChars(artifact, artifact_str);
env->ReleaseStringUTFChars(packageId, package_id_str);
return env->NewStringUTF(url.c_str());
}
@@ -1796,388 +1697,4 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getBuildVersion(
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"
+2 -26
View File
@@ -1,19 +1,18 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <string>
#include <jni.h>
#include <common/fs/path_util.h>
#include "android_config.h"
#include "android_settings.h"
#include "common/android/android_common.h"
#include "common/android/id_cache.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "frontend_common/config.h"
#include "frontend_common/settings_generator.h"
#include "native.h"
std::unique_ptr<AndroidConfig> global_config;
@@ -38,7 +37,6 @@ extern "C" {
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
global_config = std::make_unique<AndroidConfig>();
FrontendCommon::GenerateSettings();
}
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
@@ -583,26 +581,4 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setSdmcDir(JNIEnv* env, jobject
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"
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/**
@@ -258,11 +258,6 @@ Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
}
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;
}
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="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>
@@ -11,7 +11,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:orientation="horizontal"
android:padding="16dp"
android:layout_gravity="center_vertical">
@@ -23,25 +23,12 @@
android:layout_gravity="center_vertical|start"
android:requiresFadingEdge="horizontal"
android:textAlignment="viewStart"
app:layout_constraintBottom_toTopOf="@+id/type_indicator"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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
android:id="@+id/button_layout"
android:layout_width="wrap_content"
@@ -1,39 +0,0 @@
<?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>
@@ -1,226 +0,0 @@
<?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>
@@ -1,49 +0,0 @@
<?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>
@@ -1,21 +0,0 @@
<?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>
@@ -1,125 +0,0 @@
<?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,9 +36,6 @@
<action
android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
app:destination="@id/gameFoldersFragment" />
<action
android:id="@+id/action_homeSettingsFragment_to_profileManagerFragment"
app:destination="@id/profileManagerFragment" />
</fragment>
<fragment
@@ -189,17 +186,5 @@
app:nullable="true"
android:defaultValue="@null" />
</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>
@@ -381,9 +381,9 @@
<string name="qlaunch_description">Launch applications from the system home screen</string>
<string name="applets">Applet launcher</string>
<string name="applets_description">Launch system applets using installed firmware</string>
<string name="applets_error_firmware">Firmware not installed</string>
<string name="applets_error_firmware">Firmware not installed or invalid version</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.]]></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="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="mii_edit_applet">Mii edit</string>
@@ -493,8 +493,6 @@
<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_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>
@@ -1239,35 +1237,6 @@
<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>
<!-- 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 -->
<string name="licenses">Licenses</string>
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
@@ -1774,8 +1743,4 @@ 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
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</string>
<string name="external_content">External Content</string>
<string name="add_folders">Add Folder</string>
</resources>
+3 -1
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-License-Identifier:
@@ -186,6 +186,8 @@ if(ARCHITECTURE_x86_64)
common
PRIVATE x64/cpu_detect.cpp
x64/cpu_detect.h
x64/cpu_wait.cpp
x64/cpu_wait.h
x64/native_clock.cpp
x64/native_clock.h
x64/rdtsc.cpp
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <jni.h>
@@ -516,7 +516,7 @@ namespace Common::Android {
s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
s_patch_constructor = env->GetMethodID(
patch_class, "<init>",
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;J)V");
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
+54 -60
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -7,7 +7,6 @@
#include <atomic>
#include <chrono>
#include <climits>
#include <cstdlib>
#include <regex>
#include <thread>
@@ -42,7 +41,7 @@ namespace {
/// @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 :)
constexpr const char* TrimSourcePath(std::string_view source) noexcept {
constexpr const char* TrimSourcePath(std::string_view source) {
const auto rfind = [source](const std::string_view match) {
return source.rfind(match) == source.npos ? 0 : (source.rfind(match) + match.size());
};
@@ -52,31 +51,31 @@ constexpr const char* TrimSourcePath(std::string_view source) noexcept {
/// @brief Interface for logging backends.
struct Backend {
virtual ~Backend() noexcept = default;
virtual void Write(const Entry& entry) noexcept = 0;
virtual void EnableForStacktrace() noexcept= 0;
virtual void Flush() noexcept = 0;
virtual ~Backend() = default;
virtual void Write(const Entry& entry) = 0;
virtual void EnableForStacktrace() = 0;
virtual void Flush() = 0;
};
/// @brief Backend that writes to stderr and with color
struct ColorConsoleBackend final : public Backend {
explicit ColorConsoleBackend() noexcept = default;
~ColorConsoleBackend() noexcept override = default;
explicit ColorConsoleBackend() = default;
~ColorConsoleBackend() override = default;
void Write(const Entry& entry) noexcept override {
void Write(const Entry& entry) override {
if (enabled.load(std::memory_order_relaxed))
PrintColoredMessage(entry);
}
void Flush() noexcept override {
void Flush() override {
// stderr shouldn't be buffered
}
void EnableForStacktrace() noexcept override {
void EnableForStacktrace() override {
enabled = true;
}
void SetEnabled(bool enabled_) noexcept {
void SetEnabled(bool enabled_) {
enabled = enabled_;
}
@@ -84,10 +83,9 @@ private:
std::atomic_bool enabled{false};
};
#ifndef __OPENORBIS__
/// @brief Backend that writes to a file passed into the constructor
struct FileBackend final : public Backend {
explicit FileBackend(const std::filesystem::path& filename) noexcept {
explicit FileBackend(const std::filesystem::path& filename) {
auto old_filename = filename;
old_filename += ".old.txt";
@@ -99,9 +97,9 @@ struct FileBackend final : public Backend {
file = std::make_unique<FS::IOFile>(filename, FS::FileAccessMode::Write, FS::FileType::TextFile);
}
~FileBackend() noexcept override = default;
~FileBackend() override = default;
void Write(const Entry& entry) noexcept override {
void Write(const Entry& entry) override {
if (!enabled)
return;
@@ -149,11 +147,11 @@ struct FileBackend final : public Backend {
}
}
void Flush() noexcept override {
void Flush() override {
file->Flush();
}
void EnableForStacktrace() noexcept override {
void EnableForStacktrace() override {
enabled = true;
bytes_written = 0;
}
@@ -163,30 +161,29 @@ private:
std::size_t bytes_written = 0;
bool enabled = true;
};
#endif
#ifdef _WIN32
/// @brief Backend that writes to Visual Studio's output window
struct DebuggerBackend final : public Backend {
explicit DebuggerBackend() noexcept = default;
~DebuggerBackend() noexcept override = default;
void Write(const Entry& entry) noexcept override {
explicit DebuggerBackend() = default;
~DebuggerBackend() override = default;
void Write(const Entry& entry) override {
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
}
void Flush() noexcept override {}
void EnableForStacktrace() noexcept override {}
void Flush() override {}
void EnableForStacktrace() override {}
};
#endif
#ifdef ANDROID
/// @brief Backend that writes to the Android logcat
struct LogcatBackend : public Backend {
explicit LogcatBackend() noexcept = default;
~LogcatBackend() noexcept override = default;
void Write(const Entry& entry) noexcept override {
explicit LogcatBackend() = default;
~LogcatBackend() override = default;
void Write(const Entry& entry) override {
PrintMessageToLogcat(entry);
}
void Flush() noexcept override {}
void EnableForStacktrace() noexcept override {}
void Flush() override {}
void EnableForStacktrace() override {}
};
#endif
@@ -195,13 +192,13 @@ bool initialization_in_progress_suppress_logging = true;
/// @brief Static state as a singleton.
class Impl {
public:
static Impl& Instance() noexcept {
static Impl& Instance() {
if (!instance)
std::abort();
throw std::runtime_error("Using Logging instance before its initialization");
return *instance;
}
static void Initialize() noexcept {
static void Initialize() {
if (instance) {
LOG_WARNING(Log, "Reinitializing logging backend");
return;
@@ -215,25 +212,25 @@ public:
initialization_in_progress_suppress_logging = false;
}
static void Start() noexcept {
static void Start() {
instance->StartBackendThread();
}
static void Stop() noexcept {
static void Stop() {
instance->StopBackendThread();
}
Impl(const Impl&) noexcept = delete;
Impl& operator=(const Impl&) noexcept = delete;
Impl(const Impl&) = delete;
Impl& operator=(const Impl&) = delete;
Impl(Impl&&) noexcept = delete;
Impl& operator=(Impl&&) noexcept = delete;
Impl(Impl&&) = delete;
Impl& operator=(Impl&&) = delete;
void SetGlobalFilter(const Filter& f) noexcept {
void SetGlobalFilter(const Filter& f) {
filter = f;
}
void SetColorConsoleBackendEnabled(bool enabled) noexcept {
void SetColorConsoleBackendEnabled(bool enabled) {
color_console_backend.SetEnabled(enabled);
}
@@ -241,21 +238,19 @@ public:
return filter.CheckMessage(log_class, log_level);
}
void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, std::string&& message) noexcept {
message_queue.EmplaceWait(CreateEntry(log_class, log_level, TrimSourcePath(filename), line_num, function, std::move(message)));
void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num,
const char* function, std::string&& message) noexcept {
message_queue.EmplaceWait(
CreateEntry(log_class, log_level, TrimSourcePath(filename), line_num, function, std::move(message)));
}
private:
Impl(const std::filesystem::path& file_backend_filename, const Filter& filter_) noexcept :
filter{filter_}
#ifndef __OPENORBIS__
, file_backend{file_backend_filename}
#endif
{}
Impl(const std::filesystem::path& file_backend_filename, const Filter& filter_)
: filter{filter_}, file_backend{file_backend_filename} {}
~Impl() noexcept = default;
~Impl() = default;
void StartBackendThread() noexcept {
void StartBackendThread() {
backend_thread = std::jthread([this](std::stop_token stop_token) {
Common::SetCurrentThreadName("Logger");
Entry entry;
@@ -276,14 +271,15 @@ private:
});
}
void StopBackendThread() noexcept {
void StopBackendThread() {
backend_thread.request_stop();
if (backend_thread.joinable())
backend_thread.join();
ForEachBackend([](Backend& backend) { backend.Flush(); });
}
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr, const char* function, std::string&& message) const noexcept {
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
const char* function, std::string&& message) const {
using std::chrono::duration_cast;
using std::chrono::microseconds;
using std::chrono::steady_clock;
@@ -298,11 +294,9 @@ private:
};
}
void ForEachBackend(auto lambda) noexcept {
void ForEachBackend(auto lambda) {
lambda(static_cast<Backend&>(color_console_backend));
#ifndef __OPENORBIS__
lambda(static_cast<Backend&>(file_backend));
#endif
#ifdef _WIN32
lambda(static_cast<Backend&>(debugger_backend));
#endif
@@ -311,7 +305,7 @@ private:
#endif
}
static void Deleter(Impl* ptr) noexcept {
static void Deleter(Impl* ptr) {
delete ptr;
}
@@ -319,9 +313,7 @@ private:
Filter filter;
ColorConsoleBackend color_console_backend{};
#ifndef __OPENORBIS__
FileBackend file_backend;
#endif
#ifdef _WIN32
DebuggerBackend debugger_backend{};
#endif
@@ -359,7 +351,9 @@ void SetColorConsoleBackendEnabled(bool enabled) {
Impl::Instance().SetColorConsoleBackendEnabled(enabled);
}
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) {
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) {
if (!initialization_in_progress_suppress_logging) {
auto& instance = Impl::Instance();
if (instance.CanPushEntry(log_class, log_level))
+5 -6
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -12,15 +9,17 @@
namespace Common::Log {
/// @brief 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.
/**
* 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.
*/
struct Entry {
std::chrono::microseconds timestamp;
Class log_class{};
Level log_level{};
const char* filename = nullptr;
unsigned int line_num = 0;
const char* function = nullptr;
std::string function;
std::string message;
};
+11 -17
View File
@@ -24,26 +24,24 @@
namespace Common::Log {
// Some IDEs prefer <file>:<line> instead, so let's just do that :)
std::string FormatLogMessage(const Entry& entry) noexcept {
std::string FormatLogMessage(const Entry& entry) {
if (!entry.filename) return "";
auto const time_seconds = uint32_t(entry.timestamp.count() / 1000000);
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);
char const* class_name = GetLogClassName(entry.log_class);
char const* level_name = GetLevelName(entry.log_level);
return fmt::format("[{:4d}.{:06d}] {} <{}> {}:{}:{}: {}", time_seconds, time_fractional,
class_name, level_name, entry.filename, entry.line_num, entry.function,
entry.message);
}
/// @brief Formats and prints a log entry to stderr.
static void PrintMessage(const Entry& entry) noexcept {
void PrintMessage(const Entry& entry) {
#ifdef _WIN32
auto const str = FormatLogMessage(entry).append(1, '\n');
fwrite(str.c_str(), 1, str.size(), stderr);
#else
#define ESC "\x1b"
auto const color_str = [&entry]() -> const char* {
auto str = std::string{[&entry]() -> const char* {
switch (entry.log_level) {
case Level::Debug: return ESC "[0;36m"; // Cyan
case Level::Info: return ESC "[0;37m"; // Bright gray
@@ -52,19 +50,15 @@ static void PrintMessage(const Entry& entry) noexcept {
case Level::Critical: return ESC "[1;35m"; // Bright magenta
default: return ESC "[1;30m"; // Grey
}
}();
auto const time_seconds = uint32_t(entry.timestamp.count() / 1000000);
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());
}()};
str.append(FormatLogMessage(entry));
str.append(ESC "[0m\n");
#undef ESC
#endif
fwrite(str.c_str(), 1, str.size(), stderr);
}
void PrintColoredMessage(const Entry& entry) noexcept {
void PrintColoredMessage(const Entry& entry) {
#ifdef _WIN32
HANDLE console_handle = GetStdHandle(STD_ERROR_HANDLE);
if (console_handle == INVALID_HANDLE_VALUE)
@@ -90,7 +84,7 @@ void PrintColoredMessage(const Entry& entry) noexcept {
#endif
}
void PrintMessageToLogcat(const Entry& entry) noexcept {
void PrintMessageToLogcat(const Entry& entry) {
#ifdef ANDROID
android_LogPriority android_log_priority = [&]() {
switch (entry.log_level) {
+5 -6
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -13,9 +10,11 @@ namespace Common::Log {
struct Entry;
/// Formats a log entry into the provided text buffer.
std::string FormatLogMessage(const Entry& entry) noexcept;
std::string FormatLogMessage(const Entry& entry);
/// 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.
void PrintColoredMessage(const Entry& entry) noexcept;
void PrintColoredMessage(const Entry& entry);
/// Formats and prints a log entry to the android logcat.
void PrintMessageToLogcat(const Entry& entry) noexcept;
void PrintMessageToLogcat(const Entry& entry);
} // namespace Common::Log
+34 -32
View File
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <version>
#include "common/settings_enums.h"
#if __cpp_lib_chrono >= 201907L
#include <chrono>
#include <exception>
@@ -16,10 +17,8 @@
#include <functional>
#include <string_view>
#include <type_traits>
#include <deque>
#include <fmt/core.h>
#include "common/settings_enums.h"
#include "common/assert.h"
#include "common/fs/fs_util.h"
#include "common/fs/path_util.h"
@@ -111,38 +110,36 @@ std::string GetTimeZoneString(TimeZone time_zone) {
}
void LogSettings() {
std::deque<std::string> settings_list;
const auto log_setting = [](std::string_view name, const auto& value) {
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 (const auto& setting : settings) {
// Hide the token secret, for security reasons.
if (setting->Id() != values.eden_token.Id()) {
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()));
if (setting->Id() == values.eden_token.Id()) {
// Hide the token secret, for security reasons.
continue;
}
const auto name = fmt::format(
"{:c}{:c} {}.{}", setting->ToString() == setting->DefaultToString() ? '-' : 'M',
setting->UsingGlobal() ? '-' : 'C', TranslateCategory(category),
setting->GetLabel());
log_setting(name, setting->Canonicalize());
}
}
std::string settings_str{};
for (auto const& e : settings_list)
settings_str += e;
LOG_INFO(Config, "Eden Configuration:\n{}", settings_str);
#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
log_path("DataStorage_CacheDir", Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir));
log_path("DataStorage_ConfigDir", Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir));
log_path("DataStorage_LoadDir", Common::FS::GetEdenPath(Common::FS::EdenPath::LoadDir));
log_path("DataStorage_NANDDir", Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir));
log_path("DataStorage_SaveDir", Common::FS::GetEdenPath(Common::FS::EdenPath::SaveDir));
log_path("DataStorage_SDMCDir", Common::FS::GetEdenPath(Common::FS::EdenPath::SDMCDir));
}
bool getDebugKnobAt(u8 i) {
@@ -174,10 +171,12 @@ bool IsDMALevelSafe() {
}
bool IsFastmemEnabled() {
if (values.cpu_accuracy.GetValue() == Settings::CpuAccuracy::Debugging)
if (values.cpu_accuracy.GetValue() == Settings::CpuAccuracy::Debugging) {
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);
}
#if !defined(__APPLE__) && !defined(__linux__) && !defined(__ANDROID__) && !defined(_WIN32)
return false;
#else
@@ -342,7 +341,10 @@ void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo& info)
info.down_shift = 0;
break;
default:
UNREACHABLE();
ASSERT(false);
info.up_scale = 1;
info.down_shift = 0;
break;
}
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;
+1 -10
View File
@@ -481,14 +481,6 @@ struct Values {
SwitchableSetting<bool> barrier_feedback_loops{linkage, true, "barrier_feedback_loops",
Category::RendererAdvanced};
SwitchableSetting<bool> enable_buffer_history{linkage,
false,
"enable_buffer_history",
Category::RendererAdvanced,
Specialization::Default,
true,
true};
// Renderer Hacks //
SwitchableSetting<GpuOverclock> fast_gpu_time{linkage,
GpuOverclock::Medium,
@@ -715,7 +707,6 @@ struct Values {
Category::DataStorage};
Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path",
Category::DataStorage};
std::vector<std::string> external_content_dirs;
// Debugging
bool record_frame_times;
@@ -787,7 +778,7 @@ struct Values {
Category::WebService};
Setting<std::string> eden_username{linkage, "Eden", "eden_username",
Category::WebService};
Setting<std::string> eden_token{linkage, "",
Setting<std::string> eden_token{linkage, "njausoolxygtpvraofqunuufhmupriifnpfggjxefntlyglr",
"eden_token", Category::WebService};
// Add-Ons
-1
View File
@@ -155,7 +155,6 @@ ENUM(GpuUnswizzleChunk, VeryLow, Low, Normal, Medium, High)
ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
ENUM(GameListMode, TreeView, GridView);
template <typename Type>
inline std::string_view CanonicalizeEnum(Type id) {
+78
View File
@@ -0,0 +1,78 @@
// 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
+10
View File
@@ -0,0 +1,10 @@
// 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
+153 -326
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <numeric>
@@ -20,13 +20,9 @@ namespace Core::NCE {
Patcher::Patcher(Patcher&& other) noexcept
: patch_cache(std::move(other.patch_cache)),
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_pre(m_patch_instructions_pre),
m_save_context(other.m_save_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),
total_program_size(other.total_program_size),
m_relocate_module_index(other.m_relocate_module_index),
@@ -46,25 +42,20 @@ using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
constexpr size_t MaxRelativeBranch = 128_MiB;
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
Patcher::Patcher() : c(m_patch_instructions), c_pre(m_patch_instructions_pre) {
Patcher::Patcher() : c(m_patch_instructions) {
LOG_WARNING(Core_ARM, "Patcher initialized with LRU cache {}",
patch_cache.isEnabled() ? "enabled" : "disabled");
// The first word of the patch section is always a branch to the first instruction of the
// module.
c.dw(0);
c_pre.dw(0);
// Write save context helper function.
c.l(m_save_context);
WriteSaveContext();
c_pre.l(m_save_context_pre);
WriteSaveContext(c_pre);
// Write load context helper function.
c.l(m_load_context);
WriteLoadContext();
c_pre.l(m_load_context_pre);
WriteLoadContext(c_pre);
}
Patcher::~Patcher() = default;
@@ -73,16 +64,7 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
const Kernel::CodeSet::Segment& code) {
// 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();
// 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) {
if (total_program_size + image_size > MaxRelativeBranch && total_program_size > 0) {
return false;
}
@@ -92,12 +74,7 @@ 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
// module.
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});
}
curr_patch->m_branch_to_module_relocations.push_back({0, 0});
// Retrieve text segment data.
const auto text = std::span{program_image}.subspan(code.offset, code.size);
@@ -108,18 +85,12 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
for (u32 i = ModuleCodeIndex; i < static_cast<u32>(text_words.size()); i++) {
const u32 inst = text_words[i];
const auto AddRelocations = [&](bool& pre_buffer) {
const auto AddRelocations = [&] {
const uintptr_t this_offset = i * sizeof(u32);
const uintptr_t next_offset = this_offset + sizeof(u32);
pre_buffer = use_split && (this_offset < MaxRelativeBranch);
// Relocate to pre- or post-patch
if (pre_buffer) {
this->BranchToPatchPre(this_offset);
} else {
this->BranchToPatch(this_offset);
}
// Relocate from here to patch.
this->BranchToPatch(this_offset);
// Relocate from patch to next instruction.
return next_offset;
@@ -127,13 +98,7 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// SVC
if (auto svc = SVC{inst}; svc.Verify()) {
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);
}
WriteSvcTrampoline(AddRelocations(), svc.GetValue());
continue;
}
@@ -144,25 +109,13 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
const auto src_reg = mrs.GetSystemReg() == TpidrroEl0 ? oaknut::SystemReg::TPIDRRO_EL0
: oaknut::SystemReg::TPIDR_EL0;
const auto dest_reg = oaknut::XReg{static_cast<int>(mrs.GetRt())};
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);
}
WriteMrsHandler(AddRelocations(), dest_reg, src_reg);
continue;
}
// MRS Xn, CNTPCT_EL0
if (auto mrs = MRS{inst}; mrs.Verify() && mrs.GetSystemReg() == CntpctEl0) {
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);
}
WriteCntpctHandler(AddRelocations(), oaknut::XReg{static_cast<int>(mrs.GetRt())});
continue;
}
@@ -173,13 +126,7 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// MSR TPIDR_EL0, Xn
if (auto msr = MSR{inst}; msr.Verify() && msr.GetSystemReg() == TpidrEl0) {
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);
}
WriteMsrHandler(AddRelocations(), oaknut::XReg{static_cast<int>(msr.GetRt())});
continue;
}
@@ -190,11 +137,7 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// Determine patching mode for the final relocation step
total_program_size += image_size;
if (use_split) {
this->mode = PatchMode::Split;
} else {
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
}
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
return true;
}
@@ -203,9 +146,7 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
Kernel::PhysicalMemory& program_image,
EntryTrampolines* out_trampolines) {
const size_t patch_size = GetSectionSize();
const size_t pre_patch_size = GetPreSectionSize();
const size_t image_size = (mode == PatchMode::Split) ? program_image.size() - pre_patch_size : program_image.size();
const size_t image_size = program_image.size();
// Retrieve text segment data.
const auto text = std::span{program_image}.subspan(code.offset, code.size);
@@ -221,16 +162,6 @@ 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) {
oaknut::CodeGenerator rc{target};
if (mode == PatchMode::PreText) {
@@ -240,16 +171,6 @@ 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) {
if (mode == PatchMode::PreText) {
return GetInteger(load_base) + patch_offset;
@@ -261,87 +182,28 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
const auto RebasePc = [&](uintptr_t module_offset) {
if (mode == PatchMode::PreText) {
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!
auto& patch = modules[m_relocate_module_index++];
if (mode == PatchMode::Split) {
for (const Relocation& rel : patch.m_branch_to_pre_patch_relocations) {
ApplyBranchToPatchRelocationPre(text_words.data() + rel.module_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));
}
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);
}
if (mode != PatchMode::Split) {
for (const Trampoline& rel : patch.m_trampolines) {
out_trampolines->insert({RebasePc(rel.module_offset), RebasePatch(rel.patch_offset)});
}
// Rewrite PC constants and record post trampolines
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));
}
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.
@@ -361,15 +223,6 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
ASSERT(image_size == total_program_size);
std::memcpy(program_image.data(), m_patch_instructions.data(),
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 {
program_image.resize(image_size + patch_size);
std::memcpy(program_image.data() + image_size, m_patch_instructions.data(),
@@ -385,225 +238,202 @@ size_t Patcher::GetSectionSize() const noexcept {
return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
}
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) {
void Patcher::WriteLoadContext() {
// 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
// of stack.
cg.STR(X30, SP, 8);
cg.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
c.STR(X30, SP, 8);
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
// Load system registers.
cg.LDR(W0, X30, offsetof(GuestContext, fpsr));
cg.MSR(oaknut::SystemReg::FPSR, X0);
cg.LDR(W0, X30, offsetof(GuestContext, fpcr));
cg.MSR(oaknut::SystemReg::FPCR, X0);
cg.LDR(W0, X30, offsetof(GuestContext, nzcv));
cg.MSR(oaknut::SystemReg::NZCV, X0);
c.LDR(W0, X30, offsetof(GuestContext, fpsr));
c.MSR(oaknut::SystemReg::FPSR, X0);
c.LDR(W0, X30, offsetof(GuestContext, fpcr));
c.MSR(oaknut::SystemReg::FPCR, X0);
c.LDR(W0, X30, offsetof(GuestContext, nzcv));
c.MSR(oaknut::SystemReg::NZCV, X0);
// Load all vector registers.
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
for (int i = 0; i <= 30; i += 2) {
cg.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
c.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
}
// Load all general-purpose registers except X30.
for (int i = 0; i <= 28; i += 2) {
cg.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
c.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
}
// Reload our return X30 from the stack and return.
// The patch code will reload the guest X30 for us.
cg.LDR(X30, SP, 8);
cg.RET();
c.LDR(X30, SP, 8);
c.RET();
}
void Patcher::WriteSaveContext(oaknut::VectorCodeGenerator& cg) {
void Patcher::WriteSaveContext() {
// 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
// stack.
cg.STR(X30, SP, 8);
cg.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
c.STR(X30, SP, 8);
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
// Store all general-purpose registers except X30.
for (int i = 0; i <= 28; i += 2) {
cg.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
c.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
}
// Store all vector registers.
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
for (int i = 0; i <= 30; i += 2) {
cg.STP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
c.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.
cg.STR(X0, SP, PRE_INDEXED, -16);
cg.LDR(X0, SP, 16);
cg.STR(X0, X30, 8 * 30);
cg.ADD(X0, SP, 32);
cg.STR(X0, X30, offsetof(GuestContext, sp));
cg.MRS(X0, oaknut::SystemReg::FPSR);
cg.STR(W0, X30, offsetof(GuestContext, fpsr));
cg.MRS(X0, oaknut::SystemReg::FPCR);
cg.STR(W0, X30, offsetof(GuestContext, fpcr));
cg.MRS(X0, oaknut::SystemReg::NZCV);
cg.STR(W0, X30, offsetof(GuestContext, nzcv));
cg.LDR(X0, SP, POST_INDEXED, 16);
c.STR(X0, SP, PRE_INDEXED, -16);
c.LDR(X0, SP, 16);
c.STR(X0, X30, 8 * 30);
c.ADD(X0, SP, 32);
c.STR(X0, X30, offsetof(GuestContext, sp));
c.MRS(X0, oaknut::SystemReg::FPSR);
c.STR(W0, X30, offsetof(GuestContext, fpsr));
c.MRS(X0, oaknut::SystemReg::FPCR);
c.STR(W0, X30, offsetof(GuestContext, fpcr));
c.MRS(X0, oaknut::SystemReg::NZCV);
c.STR(W0, X30, offsetof(GuestContext, nzcv));
c.LDR(X0, SP, POST_INDEXED, 16);
// Reload our return X30 from the stack, and return.
cg.LDR(X30, SP, 8);
cg.RET();
c.LDR(X30, SP, 8);
c.RET();
}
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);
void Patcher::WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id) {
// We are about to start saving state, so we need to lock the context.
this->LockContext(cg);
this->LockContext();
// 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.
cg.STR(X30, SP, PRE_INDEXED, -16);
cg.BL(save_ctx);
cg.LDR(X30, SP, POST_INDEXED, 16);
c.STR(X30, SP, PRE_INDEXED, -16);
c.BL(m_save_context);
c.LDR(X30, SP, POST_INDEXED, 16);
// 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.
oaknut::Label pc_after_svc;
cg.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
cg.LDR(X2, pc_after_svc);
cg.STR(X2, X1, offsetof(GuestContext, pc));
c.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
c.LDR(X2, pc_after_svc);
c.STR(X2, X1, offsetof(GuestContext, pc));
// Store SVC number to execute when we return
cg.MOV(X2, svc_id);
cg.STR(W2, X1, offsetof(GuestContext, svc));
c.MOV(X2, svc_id);
c.STR(W2, X1, offsetof(GuestContext, svc));
// We are calling a SVC. Clear esr_el1 and return it.
static_assert(std::is_same_v<std::underlying_type_t<HaltReason>, u64>);
oaknut::Label retry;
cg.ADD(X2, X1, offsetof(GuestContext, esr_el1));
cg.l(retry);
cg.LDAXR(X0, X2);
cg.STLXR(W3, XZR, X2);
cg.CBNZ(W3, retry);
c.ADD(X2, X1, offsetof(GuestContext, esr_el1));
c.l(retry);
c.LDAXR(X0, X2);
c.STLXR(W3, XZR, X2);
c.CBNZ(W3, retry);
// Add "calling SVC" flag. Since this is X0, this is now our return value.
cg.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
c.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
// Offset the GuestContext pointer to the HostContext member.
// 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.
cg.ADD(X1, X1, offsetof(GuestContext, host_ctx));
c.ADD(X1, X1, offsetof(GuestContext, host_ctx));
// Reload host TPIDR_EL0 and SP.
static_assert(offsetof(HostContext, host_sp) + 8 == offsetof(HostContext, host_tpidr_el0));
cg.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
cg.MOV(SP, X2);
cg.MSR(oaknut::SystemReg::TPIDR_EL0, X3);
c.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
c.MOV(SP, X2);
c.MSR(oaknut::SystemReg::TPIDR_EL0, X3);
// 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_VREGS_OFF = offsetof(HostContext, host_saved_vregs);
cg.LDP(X19, X20, X1, HOST_REGS_OFF);
cg.LDP(X21, X22, X1, HOST_REGS_OFF + 2 * sizeof(u64));
cg.LDP(X23, X24, X1, HOST_REGS_OFF + 4 * sizeof(u64));
cg.LDP(X25, X26, X1, HOST_REGS_OFF + 6 * sizeof(u64));
cg.LDP(X27, X28, X1, HOST_REGS_OFF + 8 * sizeof(u64));
cg.LDP(X29, X30, X1, HOST_REGS_OFF + 10 * sizeof(u64));
cg.LDP(Q8, Q9, X1, HOST_VREGS_OFF);
cg.LDP(Q10, Q11, X1, HOST_VREGS_OFF + 2 * sizeof(u128));
cg.LDP(Q12, Q13, X1, HOST_VREGS_OFF + 4 * sizeof(u128));
cg.LDP(Q14, Q15, X1, HOST_VREGS_OFF + 6 * sizeof(u128));
cg.RET();
c.LDP(X19, X20, X1, HOST_REGS_OFF);
c.LDP(X21, X22, X1, HOST_REGS_OFF + 2 * sizeof(u64));
c.LDP(X23, X24, X1, HOST_REGS_OFF + 4 * sizeof(u64));
c.LDP(X25, X26, X1, HOST_REGS_OFF + 6 * sizeof(u64));
c.LDP(X27, X28, X1, HOST_REGS_OFF + 8 * sizeof(u64));
c.LDP(X29, X30, X1, HOST_REGS_OFF + 10 * sizeof(u64));
c.LDP(Q8, Q9, X1, HOST_VREGS_OFF);
c.LDP(Q10, Q11, X1, HOST_VREGS_OFF + 2 * sizeof(u128));
c.LDP(Q12, Q13, X1, HOST_VREGS_OFF + 4 * sizeof(u128));
c.LDP(Q14, Q15, X1, HOST_VREGS_OFF + 6 * sizeof(u128));
c.RET();
// Write the post-SVC trampoline address, which will jump back to the guest after restoring its
// state.
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});
}
curr_patch->m_trampolines.push_back({c.offset(), module_dest});
// Host called this location. Save the return address so we can
// unwind the stack properly when jumping back.
cg.MRS(X2, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X2, X2, offsetof(NativeExecutionParameters, native_context));
cg.ADD(X0, X2, offsetof(GuestContext, host_ctx));
cg.STR(X30, X0, offsetof(HostContext, host_saved_regs) + 11 * sizeof(u64));
c.MRS(X2, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X2, X2, offsetof(NativeExecutionParameters, native_context));
c.ADD(X0, X2, offsetof(GuestContext, host_ctx));
c.STR(X30, X0, offsetof(HostContext, host_saved_regs) + 11 * sizeof(u64));
// Reload all guest registers except X30 and PC.
// The function also expects 16 bytes of stack already allocated.
cg.STR(X30, SP, PRE_INDEXED, -16);
cg.BL(load_ctx);
cg.LDR(X30, SP, POST_INDEXED, 16);
c.STR(X30, SP, PRE_INDEXED, -16);
c.BL(m_load_context);
c.LDR(X30, SP, POST_INDEXED, 16);
// Use X1 as a scratch register to restore X30.
cg.STR(X1, SP, PRE_INDEXED, -16);
cg.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
cg.LDR(X30, X1, offsetof(GuestContext, cpu_registers) + sizeof(u64) * 30);
cg.LDR(X1, SP, POST_INDEXED, 16);
c.STR(X1, SP, PRE_INDEXED, -16);
c.MRS(X1, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X1, X1, offsetof(NativeExecutionParameters, native_context));
c.LDR(X30, X1, offsetof(GuestContext, cpu_registers) + sizeof(u64) * 30);
c.LDR(X1, SP, POST_INDEXED, 16);
// Unlock the context.
this->UnlockContext(cg);
this->UnlockContext();
// Jump back to the instruction after the emulated SVC.
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
this->BranchToModule(module_dest);
// Store PC after call.
cg.l(pc_after_svc);
if (&cg == &c_pre)
this->WriteModulePcPre(module_dest);
else
this->WriteModulePc(module_dest);
c.l(pc_after_svc);
this->WriteModulePc(module_dest);
}
void Patcher::WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
oaknut::SystemReg src_reg, oaknut::VectorCodeGenerator& cg) {
oaknut::SystemReg src_reg) {
// Retrieve emulated TLS register from GuestContext.
cg.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
c.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
if (src_reg == oaknut::SystemReg::TPIDRRO_EL0) {
cg.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
} else {
cg.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
}
// Jump back to the instruction after the emulated MRS.
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
this->BranchToModule(module_dest);
}
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg, oaknut::VectorCodeGenerator& cg) {
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg) {
const auto scratch_reg = src_reg.index() == 0 ? X1 : X0;
cg.STR(scratch_reg, SP, PRE_INDEXED, -16);
c.STR(scratch_reg, SP, PRE_INDEXED, -16);
// Save guest value to NativeExecutionParameters::tpidr_el0.
cg.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
cg.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
c.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
c.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
// Restore scratch register.
cg.LDR(scratch_reg, SP, POST_INDEXED, 16);
c.LDR(scratch_reg, SP, POST_INDEXED, 16);
// Jump back to the instruction after the emulated MSR.
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
this->BranchToModule(module_dest);
}
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::VectorCodeGenerator& cg) {
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg) {
static Common::Arm64::NativeClock clock{};
const auto factor = clock.GetGuestCNTFRQFactor();
const auto raw_factor = std::bit_cast<std::array<u64, 2>>(factor);
@@ -616,83 +446,80 @@ void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_
oaknut::Label factorhi;
// Save scratches.
cg.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
c.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
// Load counter value.
cg.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
c.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
// Load scaling factor.
cg.LDR(scratch0, factorlo);
cg.LDR(scratch1, factorhi);
c.LDR(scratch0, factorlo);
c.LDR(scratch1, factorhi);
// Multiply low bits and get result.
cg.UMULH(scratch0, dest_reg, scratch0);
c.UMULH(scratch0, dest_reg, scratch0);
// Multiply high bits and add low bit result.
cg.MADD(dest_reg, dest_reg, scratch1, scratch0);
c.MADD(dest_reg, dest_reg, scratch1, scratch0);
// Reload scratches.
cg.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
c.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
// Jump back to the instruction after the emulated MRS.
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
this->BranchToModule(module_dest);
// Scaling factor constant values.
cg.l(factorlo);
cg.dx(raw_factor[0]);
cg.l(factorhi);
cg.dx(raw_factor[1]);
c.l(factorlo);
c.dx(raw_factor[0]);
c.l(factorhi);
c.dx(raw_factor[1]);
}
void Patcher::LockContext(oaknut::VectorCodeGenerator& cg) {
void Patcher::LockContext() {
oaknut::Label retry;
// Save scratches.
cg.STP(X0, X1, SP, PRE_INDEXED, -16);
c.STP(X0, X1, SP, PRE_INDEXED, -16);
// Reload lock pointer.
cg.l(retry);
cg.CLREX();
cg.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
cg.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
c.l(retry);
c.CLREX();
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
static_assert(SpinLockLocked == 0);
// Load-linked with acquire ordering.
cg.LDAXR(W1, X0);
c.LDAXR(W1, X0);
// If the value was SpinLockLocked, clear monitor and retry.
cg.CBZ(W1, retry);
c.CBZ(W1, retry);
// Store-conditional SpinLockLocked with relaxed ordering.
cg.STXR(W1, WZR, X0);
c.STXR(W1, WZR, X0);
// If we failed to store, retry.
cg.CBNZ(W1, retry);
c.CBNZ(W1, retry);
// We succeeded! Reload scratches.
cg.LDP(X0, X1, SP, POST_INDEXED, 16);
c.LDP(X0, X1, SP, POST_INDEXED, 16);
}
void Patcher::UnlockContext(oaknut::VectorCodeGenerator& cg) {
void Patcher::UnlockContext() {
// Save scratches.
cg.STP(X0, X1, SP, PRE_INDEXED, -16);
c.STP(X0, X1, SP, PRE_INDEXED, -16);
// Load lock pointer.
cg.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
cg.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
// Load SpinLockUnlocked.
cg.MOV(W1, SpinLockUnlocked);
c.MOV(W1, SpinLockUnlocked);
// Store value with release ordering.
cg.STLR(W1, X0);
c.STLR(W1, X0);
// Load scratches.
cg.LDP(X0, X1, SP, POST_INDEXED, 16);
c.LDP(X0, X1, SP, POST_INDEXED, 16);
}
} // namespace Core::NCE
+10 -44
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -41,7 +41,6 @@ enum class PatchMode : u32 {
None,
PreText, ///< Patch section is inserted before .text
PostData, ///< Patch section is inserted after .data
Split, ///< Patch sections are inserted before .text and after .data
};
using ModuleTextAddress = u64;
@@ -64,7 +63,6 @@ public:
bool RelocateAndCopy(Common::ProcessAddress load_base, const Kernel::CodeSet::Segment& code,
Kernel::PhysicalMemory& program_image, EntryTrampolines* out_trampolines);
size_t GetSectionSize() const noexcept;
size_t GetPreSectionSize() const noexcept;
[[nodiscard]] PatchMode GetPatchMode() const noexcept {
return mode;
@@ -78,25 +76,15 @@ private:
uintptr_t module_offset;
};
// Core implementations with explicit code generator
void WriteLoadContext(oaknut::VectorCodeGenerator& code);
void WriteSaveContext(oaknut::VectorCodeGenerator& code);
void LockContext(oaknut::VectorCodeGenerator& code);
void UnlockContext(oaknut::VectorCodeGenerator& code);
void WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id, oaknut::VectorCodeGenerator& code, oaknut::Label& save_ctx, oaknut::Label& load_ctx);
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, oaknut::VectorCodeGenerator& code);
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); }
void WriteLoadContext();
void WriteSaveContext();
void LockContext();
void UnlockContext();
void WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id);
void WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
oaknut::SystemReg src_reg);
void WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg);
void WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg);
private:
static constexpr size_t CACHE_SIZE = 16384; // Cache size for patch entries
@@ -123,34 +111,19 @@ 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) {
curr_patch->m_branch_to_module_relocations.push_back({c.offset(), module_dest});
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) {
curr_patch->m_write_module_pc_relocations.push_back({c.offset(), module_dest});
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:
// List of patch instructions we have generated.
std::vector<u32> m_patch_instructions{};
std::vector<u32> m_patch_instructions_pre{};
// Relocation type for relative branch from module to patch.
struct Relocation {
@@ -160,22 +133,15 @@ private:
struct ModulePatch {
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_pre_patch_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_pre{};
std::vector<ModuleTextAddress> m_exclusives{};
};
oaknut::VectorCodeGenerator c;
oaknut::VectorCodeGenerator c_pre;
oaknut::Label m_save_context{};
oaknut::Label m_load_context{};
oaknut::Label m_save_context_pre{};
oaknut::Label m_load_context_pre{};
PatchMode mode{PatchMode::None};
size_t total_program_size{};
size_t m_relocate_module_index{};
+1 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <array>
@@ -142,7 +142,6 @@ struct System::Impl {
void ReinitializeIfNecessary(System& system) {
const bool must_reinitialize =
!device_memory.has_value() ||
is_multicore != Settings::values.use_multi_core.GetValue() ||
extended_memory_layout != (Settings::values.memory_layout_mode.GetValue() !=
Settings::MemoryLayout::Memory_4Gb);
@@ -415,7 +414,6 @@ struct System::Impl {
cpu_manager.Shutdown();
debugger.reset();
kernel.Shutdown();
device_memory.reset();
stop_event = {};
Network::RestartSocketOperations();
+27 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -13,6 +13,10 @@
#include "common/windows/timer_resolution.h"
#endif
#ifdef ARCHITECTURE_x86_64
#include "common/x64/cpu_wait.h"
#endif
#include "common/settings.h"
#include "core/core_timing.h"
#include "core/hardware_properties.h"
@@ -283,7 +287,28 @@ void CoreTiming::ThreadLoop() {
if (next_time) {
// There are more events left in the queue, wait until the next event.
auto wait_time = *next_time - GetGlobalTimeNs().count();
event.WaitFor(std::chrono::nanoseconds(wait_time));
if (wait_time > 0) {
#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 {
// Queue is empty, wait until another event is scheduled and signals us to
// continue.
+43 -411
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -137,127 +137,12 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
return exefs;
const auto& disabled = Settings::values.disabled_addons[title_id];
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;
}
}
}
}
}
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
// Game Updates
std::unique_ptr<NCA> update = nullptr;
// 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);
}
const auto update_tid = GetUpdateTitleID(title_id);
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
@@ -562,103 +447,21 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
// Game Updates
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];
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;
}
}
}
}
}
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
new_nca->GetRomFS() != nullptr) {
LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
enabled_version.has_value() ? FormatTitleVersion(*enabled_version) :
FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
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) {
const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
@@ -687,179 +490,34 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
// Game Updates
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;
std::vector<Patch> external_update_patches;
const auto 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};
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider for updates
const auto* external_provider = content_union->GetExternalProvider();
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();
}
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
if (meta_ver.has_value()) {
numeric_ver = *meta_ver;
if (version_str.empty() && numeric_ver != 0) {
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);
}
if (nacp != nullptr) {
update_patch.version = nacp->GetVersionString();
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;
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);
out.push_back(update_patch);
}
} else if (update_raw != nullptr) {
update_patch.version = "PACKED";
out.push_back(update_patch);
}
}
@@ -875,8 +533,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = "Cheats",
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown
.title_id = title_id
});
}
@@ -897,7 +554,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
file->GetName()) != EXEFS_FILE_NAMES.end()) {
layeredfs = true;
}
}
}
if (ips)
@@ -922,8 +579,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown});
.title_id = title_id});
}
}
@@ -937,7 +593,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) {
AppendCommaIfNotEmpty(types, "LayeredFS");
}
}
if (!types.empty()) {
const auto mod_disabled =
@@ -947,44 +603,21 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown});
.title_id = title_id});
}
}
// DLC
const auto dlc_entries =
content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
std::vector<ContentProviderEntry> dlc_match;
dlc_match.reserve(dlc_entries.size());
std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
[this](const ContentProviderEntry& entry) {
const auto base_tid = GetBaseTitleID(entry.title_id);
const bool matches_base = base_tid == title_id;
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;
return GetBaseTitleID(entry.title_id) == title_id &&
content_provider.GetEntry(entry)->GetStatus() ==
Loader::ResultStatus::Success;
});
if (!dlc_match.empty()) {
// Ensure sorted so DLC IDs show in order.
std::sort(dlc_match.begin(), dlc_match.end());
@@ -1002,8 +635,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = std::move(list),
.type = PatchType::DLC,
.program_id = title_id,
.title_id = dlc_match.back().title_id,
.source = PatchSource::Unknown});
.title_id = dlc_match.back().title_id});
}
return out;
-12
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -31,13 +28,6 @@ class NACP;
enum class PatchType { Update, DLC, Mod };
enum class PatchSource {
Unknown,
NAND,
External,
Packed,
};
struct Patch {
bool enabled;
std::string name;
@@ -45,8 +35,6 @@ struct Patch {
PatchType type;
u64 program_id;
u64 title_id;
PatchSource source;
u32 numeric_version{0};
};
// A centralized class to manage patches to games.
+1 -518
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -13,15 +13,12 @@
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "common/string_util.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/common_funcs.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/registered_cache.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs/vfs_concat.h"
#include "core/loader/loader.h"
@@ -977,22 +974,6 @@ std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
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;
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
@@ -1000,51 +981,8 @@ void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType con
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() {
entries.clear();
multi_version_entries.clear();
}
void ManualContentProvider::Refresh() {}
@@ -1098,459 +1036,4 @@ std::vector<ContentProviderEntry> ManualContentProvider::ListEntriesFilter(
out.erase(std::unique(out.begin(), out.end()), out.end());
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
+1 -56
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -17,8 +14,7 @@
#include "core/file_sys/vfs/vfs.h"
namespace FileSys {
class ExternalContentProvider;
class CNMT;
class CNMT;
class NCA;
class NSP;
class XCI;
@@ -52,13 +48,6 @@ struct ContentProviderEntry {
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) {
return base_title_id | 0x800;
}
@@ -219,7 +208,6 @@ enum class ContentProviderUnionSlot {
UserNAND, ///< User NAND
SDMC, ///< SD Card
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.
@@ -240,9 +228,6 @@ public:
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
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::optional<ContentProviderUnionSlot> origin = {},
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
@@ -261,8 +246,6 @@ public:
void AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id,
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 Refresh() override;
@@ -275,46 +258,8 @@ public:
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:
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
-11
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -278,14 +275,6 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] =
std::move(next_nca);
} 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);
}
} else {
-12
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -86,14 +83,6 @@ struct CodeSet final {
const Segment& PatchSegment() const {
return patch_segment;
}
Segment& PostPatchSegment() {
return post_patch_segment;
}
const Segment& PostPatchSegment() const {
return post_patch_segment;
}
#endif
/// The overall data that backs this code set.
@@ -104,7 +93,6 @@ struct CodeSet final {
#ifdef HAS_NCE
Segment patch_segment;
Segment post_patch_segment;
#endif
/// The entry point address for this code set.
+1 -10
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -1258,7 +1258,6 @@ void KProcess::LoadModule(CodeSet code_set, KProcessAddress base_addr) {
#ifdef HAS_NCE
const auto& patch = code_set.PatchSegment();
const auto& post_patch = code_set.PostPatchSegment();
if (this->IsApplication() && Settings::IsNceEnabled() && patch.size != 0) {
auto& buffer = m_kernel.System().DeviceMemory().buffer;
const auto& code = code_set.CodeSegment();
@@ -1266,15 +1265,7 @@ void KProcess::LoadModule(CodeSet code_set, KProcessAddress base_addr) {
Common::MemoryPermission::Read | Common::MemoryPermission::Execute);
buffer.Protect(GetInteger(base_addr + patch.addr), patch.size,
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);
if (post_patch.size != 0) {
ReprotectSegment(code_set.PostPatchSegment(), Svc::MemoryPermission::None);
}
}
#endif
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
@@ -36,7 +36,7 @@ IAppletCommonFunctions::IAppletCommonFunctions(Core::System& system_,
{100, nullptr, "SetApplicationCoreUsageMode"},
{300, D<&IAppletCommonFunctions::GetCurrentApplicationId>, "GetCurrentApplicationId"},
{310, nullptr, "IsSystemAppletHomeMenu"}, //19.0.0+
{320, D<&IAppletCommonFunctions::SetGpuTimeSliceBoost>, "SetGpuTimeSliceBoost"}, //19.0.0+
{320, nullptr, "SetGpuTimeSliceBoost"}, //19.0.0+
{321, nullptr, "SetGpuTimeSliceBoostDueToApplication"}, //19.0.0+
{350, D<&IAppletCommonFunctions::Unknown350>, "Unknown350"} //20.0.0+
};
@@ -82,11 +82,6 @@ Result IAppletCommonFunctions::GetCurrentApplicationId(Out<u64> out_application_
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) {
LOG_WARNING(Service_AM, "(STUBBED) called");
*out_unknown = 0;
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
@@ -24,7 +24,6 @@ private:
Result SetDisplayMagnification(f32 x, f32 y, f32 width, f32 height);
Result SetCpuBoostRequestPriority(s32 priority);
Result GetCurrentApplicationId(Out<u64> out_application_id);
Result SetGpuTimeSliceBoost(s64 time_span);
Result Unknown350(Out<u16> out_unknown);
const std::shared_ptr<Applet> applet;
@@ -179,14 +179,7 @@ Result ILibraryAppletSelfAccessor::GetMainAppletStorageId(Out<FileSys::StorageId
Result ILibraryAppletSelfAccessor::ExitProcessAndReturn() {
LOG_INFO(Service_AM, "called");
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);
}
m_applet->process->Terminate();
R_SUCCEED();
}
+1 -37
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -9,7 +9,6 @@
#include "common/assert.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/file_sys/bis_factory.h"
@@ -508,10 +507,6 @@ FileSys::RegisteredCache* FileSystemController::GetSDMCContents() const {
return sdmc_factory->GetSDMCContents();
}
FileSys::ExternalContentProvider* FileSystemController::GetExternalContentProvider() const {
return external_provider.get();
}
FileSys::PlaceholderCache* FileSystemController::GetSystemNANDPlaceholder() const {
LOG_TRACE(Service_FS, "Opening System NAND Placeholder");
@@ -689,7 +684,6 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
if (overwrite) {
bis_factory = nullptr;
sdmc_factory = nullptr;
external_provider = nullptr;
}
using EdenPath = Common::FS::EdenPath;
@@ -722,36 +716,6 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::SDMC,
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() {
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -20,7 +17,6 @@ class System;
namespace FileSys {
class BISFactory;
class ExternalContentProvider;
class NCA;
class RegisteredCache;
class RegisteredCacheUnion;
@@ -121,8 +117,6 @@ public:
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
// above is called.
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
@@ -144,8 +138,6 @@ private:
std::unique_ptr<FileSys::SDMCFactory> sdmc_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::RegisteredCache> gamecard_registered;
std::unique_ptr<FileSys::PlaceholderCache> gamecard_placeholder;
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -99,6 +99,11 @@ Status BufferQueueConsumer::AcquireBuffer(BufferItem* out_buffer,
slots[slot].acquire_called = true;
slots[slot].needs_cleanup_on_release = false;
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
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -17,39 +17,6 @@ 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() {
dequeue_possible.store(true);
dequeue_condition.notify_all();
@@ -63,6 +30,7 @@ bool BufferQueueCore::WaitForDequeueCondition(std::unique_lock<std::mutex>& lk)
}
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) {
return 0;
}
@@ -87,6 +55,8 @@ s32 BufferQueueCore::GetMaxBufferCountLocked(bool async) const {
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) {
const auto state = slots[slot].buffer_state;
if (state == BufferState::Queued || state == BufferState::Dequeued) {
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -10,31 +10,20 @@
#pragma once
#include <condition_variable>
#include <deque>
#include <list>
#include <memory>
#include <mutex>
#include <set>
#include <vector>
#include <unordered_map>
#include <algorithm>
#include "core/hle/service/nvnflinger/buffer_item.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/status.h"
#include "core/hle/service/nvnflinger/window.h"
namespace Service::android {
struct BufferHistoryInfo {
u64 frame_number{};
s64 queue_time{};
s64 presentation_time{};
BufferState state{};
};
class IConsumerListener;
class IProducerListener;
@@ -44,14 +33,10 @@ class BufferQueueCore final {
public:
static constexpr s32 INVALID_BUFFER_SLOT = BufferItem::INVALID_BUFFER_SLOT;
static constexpr u32 BUFFER_HISTORY_SIZE = 8;
BufferQueueCore();
~BufferQueueCore();
void PushHistory(u64 frame_number, s64 queue_time, s64 presentation_time, BufferState state);
void UpdateHistory(u64 frame_number, BufferState state);
private:
void SignalDequeueCondition();
bool WaitForDequeueCondition(std::unique_lock<std::mutex>& lk);
@@ -87,11 +72,6 @@ private:
const s32 max_acquired_buffer_count{}; // This is always zero on HOS
bool buffer_has_been_queued{};
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{};
bool is_allocating{};
mutable std::condition_variable_any is_allocating_condition;
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -9,7 +9,6 @@
#include "common/assert.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "core/hle/kernel/k_event.h"
#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/kernel/kernel.h"
@@ -27,7 +26,7 @@ BufferQueueProducer::BufferQueueProducer(Service::KernelHelpers::ServiceContext&
std::shared_ptr<BufferQueueCore> buffer_queue_core_,
Service::Nvidia::NvCore::NvMap& nvmap_)
: service_context{service_context_}, core{std::move(buffer_queue_core_)}, slots(core->slots),
clock{Common::CreateOptimalClock()}, nvmap(nvmap_) {
nvmap(nvmap_) {
buffer_wait_event = service_context.CreateEvent("BufferQueue:WaitEvent");
}
@@ -429,7 +428,8 @@ Status BufferQueueProducer::AttachBuffer(s32* out_slot,
return return_flags;
}
Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, QueueBufferOutput* output) {
Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
QueueBufferOutput* output) {
s64 timestamp{};
bool is_auto_timestamp{};
Common::Rectangle<s32> crop;
@@ -440,7 +440,8 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
s32 swap_interval{};
Fence fence{};
input.Deflate(&timestamp, &is_auto_timestamp, &crop, &scaling_mode, &transform, &sticky_transform_, &async, &swap_interval, &fence);
input.Deflate(&timestamp, &is_auto_timestamp, &crop, &scaling_mode, &transform,
&sticky_transform_, &async, &swap_interval, &fence);
switch (scaling_mode) {
case NativeWindowScalingMode::Freeze:
@@ -454,9 +455,10 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
return Status::BadValue;
}
std::shared_ptr<IConsumerListener> frame_available_listener;
std::shared_ptr<IConsumerListener> frame_replaced_listener;
s32 callback_ticket{};
BufferItem item;
std::shared_ptr<IConsumerListener> listener_available;
std::shared_ptr<IConsumerListener> listener_replaced;
{
std::scoped_lock lock{core->mutex};
@@ -467,82 +469,127 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input,
}
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) {
LOG_ERROR(Service_Nvnflinger, "slot {} out of range [0, {})", slot, max_buffer_count);
LOG_ERROR(Service_Nvnflinger, "slot index {} out of range [0, {})", slot,
max_buffer_count);
return Status::BadValue;
}
if (slots[slot].buffer_state != BufferState::Dequeued) {
LOG_ERROR(Service_Nvnflinger, "slot {} is not owned by producer", slot);
} else if (slots[slot].buffer_state != BufferState::Dequeued) {
LOG_ERROR(Service_Nvnflinger,
"slot {} is not owned by the producer "
"(state = {})",
slot, slots[slot].buffer_state);
return Status::BadValue;
}
if (!slots[slot].request_buffer_called) {
LOG_ERROR(Service_Nvnflinger, "slot {} was queued without request", slot);
} else if (!slots[slot].request_buffer_called) {
LOG_ERROR(Service_Nvnflinger,
"slot {} was queued without requesting "
"a buffer",
slot);
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;
}
++core->frame_counter;
slots[slot].buffer_state = BufferState::Queued;
slots[slot].frame_number = core->frame_counter;
slots[slot].queue_time = timestamp;
slots[slot].presentation_time = clock->GetTimeNS().count();
slots[slot].fence = fence;
slots[slot].buffer_state = BufferState::Queued;
++core->frame_counter;
slots[slot].frame_number = core->frame_counter;
item.slot = slot;
item.acquire_called = slots[slot].acquire_called;
item.graphic_buffer = slots[slot].graphic_buffer;
item.frame_number = core->frame_counter;
item.timestamp = timestamp;
item.is_auto_timestamp = is_auto_timestamp;
item.crop = crop;
item.transform = transform & ~NativeWindowTransform::InverseDisplay;
item.transform_to_display_inverse = (transform & NativeWindowTransform::InverseDisplay) != NativeWindowTransform::None;
item.transform_to_display_inverse =
(transform & NativeWindowTransform::InverseDisplay) != NativeWindowTransform::None;
item.scaling_mode = static_cast<u32>(scaling_mode);
item.timestamp = timestamp;
item.is_auto_timestamp = is_auto_timestamp;
item.frame_number = core->frame_counter;
item.slot = slot;
item.fence = fence;
item.is_droppable = core->dequeue_buffer_cannot_block || async;
item.swap_interval = swap_interval;
item.acquire_called = slots[slot].acquire_called;
sticky_transform = sticky_transform_;
if (core->queue.empty()) {
// When the queue is empty, we can simply queue this buffer
core->queue.push_back(item);
listener_available = core->consumer_listener;
frame_available_listener = core->consumer_listener;
} else {
auto front = core->queue.begin();
if (front->is_droppable && core->StillTracking(*front)) {
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;
}
// When the queue is not empty, we need to look at the front buffer
// state to see if we need to replace it
auto front(core->queue.begin());
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;
listener_replaced = core->consumer_listener;
frame_replaced_listener = core->consumer_listener;
} else {
core->queue.push_back(item);
listener_available = core->consumer_listener;
frame_available_listener = 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->SignalDequeueCondition();
output->Inflate(core->default_width, core->default_height, core->transform_hint,
static_cast<u32>(core->queue.size()));
output->Inflate(core->default_width, core->default_height, core->transform_hint, static_cast<u32>(core->queue.size()));
// Take a ticket for the callback functions
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.slot = BufferItem::INVALID_BUFFER_SLOT;
if (listener_available) {
listener_available->OnFrameAvailable(item);
} else if (listener_replaced) {
listener_replaced->OnFrameReplaced(item);
// Call back without the main BufferQueue lock held, but with the callback lock held so we can
// ensure that callbacks occur in order
{
std::scoped_lock lock{callback_mutex};
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;
@@ -763,10 +810,6 @@ Status BufferQueueProducer::SetPreallocatedBuffer(s32 slot,
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,
std::span<u8> parcel_reply, u32 flags) {
// Values used by BnGraphicBufferProducer onTransact
@@ -886,42 +929,9 @@ void BufferQueueProducer::Transact(u32 code, std::span<const u8> parcel_data,
status = SetBufferCount(buffer_count);
break;
}
case TransactionId::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]);
}
case TransactionId::GetBufferHistory:
LOG_DEBUG(Service_Nvnflinger, "(STUBBED) called, transaction=GetBufferHistory");
break;
}
default:
ASSERT_MSG(false, "Unimplemented TransactionId {}", code);
break;
@@ -934,4 +944,8 @@ void BufferQueueProducer::Transact(u32 code, std::span<const u8> parcel_data,
(std::min)(parcel_reply.size(), serialized.size()));
}
Kernel::KReadableEvent* BufferQueueProducer::GetNativeHandle(u32 type_id) {
return &buffer_wait_event->GetReadableEvent();
}
} // namespace Service::android
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -14,7 +14,6 @@
#include <mutex>
#include "common/common_funcs.h"
#include "common/wall_clock.h"
#include "core/hle/service/nvdrv/nvdata.h"
#include "core/hle/service/nvnflinger/binder.h"
#include "core/hle/service/nvnflinger/buffer_queue_defs.h"
@@ -89,7 +88,6 @@ private:
s32 next_callback_ticket{};
s32 current_callback_ticket{};
std::condition_variable_any callback_condition;
std::unique_ptr<Common::WallClock> clock;
Service::Nvidia::NvCore::NvMap& nvmap;
};
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -37,7 +37,6 @@ struct BufferSlot final {
bool needs_cleanup_on_release{};
bool attached_by_consumer{};
bool is_preallocated{};
s64 queue_time{}, presentation_time{};
};
} // namespace Service::android
+4 -32
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -102,8 +102,6 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
auto* patch = &patches->operator[](patch_index);
if (patch->GetPatchMode() == Core::NCE::PatchMode::PreText) {
return patch->GetSectionSize();
} else if (patch->GetPatchMode() == Core::NCE::PatchMode::Split) {
return patch->GetPreSectionSize();
}
}
#endif
@@ -180,26 +178,12 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
}
} else if (patch) {
// 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())) {
// Update patch section.
auto& patch_segment = codeset.PatchSegment();
auto& post_patch_segment = codeset.PostPatchSegment();
const auto patch_mode = patch->GetPatchMode();
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());
}
patch_segment.addr =
patch->GetPatchMode() == Core::NCE::PatchMode::PreText ? 0 : 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
@@ -209,18 +193,6 @@ 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 (!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;
}
@@ -433,10 +433,9 @@ int RegAlloc::AllocateRegister(const std::array<HostLocInfo, 32>& regs, const st
std::vector<int> candidates;
std::copy_if(order.begin(), order.end(), std::back_inserter(candidates), [&](int i) { return regs[i].MaybeAllocatable(); });
// TODO: The candidate was chosen randomly before, and a LRU was
// suggested as an improvement. However, using an incrementing index
// seems to be close enough. Determine if an LRU is still needed.
return candidates[alloc_candidate_index++ % candidates.size()];
// TODO: LRU
std::uniform_int_distribution<size_t> dis{0, candidates.size() - 1};
return candidates[dis(rand_gen)];
}
void RegAlloc::SpillGpr(int index) {
@@ -160,12 +160,8 @@ class RegAlloc final {
public:
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) noexcept
: code{code}
, fpsr_manager{fpsr_manager}
, gpr_order{gpr_order}
, fpr_order{fpr_order}
{}
explicit RegAlloc(oaknut::CodeGenerator& code, FpsrManager& fpsr_manager, std::vector<int> gpr_order, std::vector<int> fpr_order)
: code{code}, fpsr_manager{fpsr_manager}, gpr_order{gpr_order}, fpr_order{fpr_order}, rand_gen{std::random_device{}()} {}
ArgumentInfo GetArgumentInfo(IR::Inst* inst);
bool WasValueDefined(IR::Inst* inst) const;
@@ -335,7 +331,7 @@ private:
HostLocInfo flags;
std::array<HostLocInfo, SpillCount> spills;
mutable size_t alloc_candidate_index = 0;
mutable std::mt19937 rand_gen;
ankerl::unordered_dense::set<const IR::Inst*> defined_insts;
};
+3 -7
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -12,9 +12,7 @@ add_library(frontend_common STATIC
firmware_manager.cpp
data_manager.h data_manager.cpp
play_time_manager.cpp
play_time_manager.h
settings_generator.h settings_generator.cpp
mod_manager.h mod_manager.cpp)
play_time_manager.h)
if (ENABLE_UPDATE_CHECKER)
target_link_libraries(frontend_common PRIVATE httplib::httplib)
@@ -31,6 +29,4 @@ if (ENABLE_UPDATE_CHECKER)
endif()
create_target_directory_groups(frontend_common)
target_link_libraries(frontend_common
PUBLIC core SimpleIni::SimpleIni frozen::frozen-headers
PRIVATE common Boost::headers)
target_link_libraries(frontend_common PUBLIC core SimpleIni::SimpleIni PRIVATE common Boost::headers)
-62
View File
@@ -1,62 +0,0 @@
// 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
-21
View File
@@ -1,21 +0,0 @@
// 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);
}
@@ -1,31 +0,0 @@
// 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);
}
}
}
-14
View File
@@ -1,14 +0,0 @@
// 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();
}
+7 -4
View File
@@ -22,8 +22,6 @@ add_library(qt_common STATIC
util/rom.h util/rom.cpp
util/applet.h util/applet.cpp
util/compress.h util/compress.cpp
util/fs.h util/fs.cpp
util/mod.h util/mod.cpp
abstract/frontend.h abstract/frontend.cpp
abstract/qt_progress_dialog.h abstract/qt_progress_dialog.cpp
@@ -31,7 +29,9 @@ add_library(qt_common STATIC
qt_string_lookup.h
qt_compat.h
discord/discord.h)
discord/discord.h
util/fs.h util/fs.cpp
)
if (UNIX)
target_sources(qt_common PRIVATE gui_settings.cpp gui_settings.h)
@@ -78,9 +78,12 @@ target_compile_definitions(qt_common PUBLIC
QT_NO_URL_CAST_FROM_STRING
)
# pass targets
find_package(frozen)
target_link_libraries(qt_common PRIVATE core Qt6::Core Qt6::Concurrent SimpleIni::SimpleIni QuaZip::QuaZip)
target_link_libraries(qt_common PUBLIC frozen::frozen-headers)
target_link_libraries(qt_common PRIVATE gamemode::headers frontend_common)
target_link_libraries(qt_common PRIVATE gamemode::headers)
if (NOT APPLE AND ENABLE_OPENGL)
target_compile_definitions(qt_common PUBLIC HAS_OPENGL)
+1 -26
View File
@@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QLineEdit>
#include "frontend.h"
#include "qt_common/qt_common.h"
@@ -9,9 +8,6 @@
#include <QFileDialog>
#endif
#include <QAbstractButton>
#include <QInputDialog>
namespace QtCommon::Frontend {
StandardButton ShowMessage(
@@ -54,25 +50,4 @@ const QString GetExistingDirectory(const QString& caption, const QString& dir,
#endif
}
int Choice(const QString& title, const QString& caption, const QStringList& options) {
QMessageBox box(rootObject);
box.setText(caption);
box.setWindowTitle(title);
for (const QString &opt : options) {
box.addButton(opt, QMessageBox::AcceptRole);
}
box.addButton(QMessageBox::Cancel);
box.exec();
auto button = box.clickedButton();
return options.indexOf(button->text());
}
const QString GetTextInput(const QString& title, const QString& caption,
const QString& defaultText) {
return QInputDialog::getText(rootObject, title, caption, QLineEdit::Normal, defaultText);
}
} // namespace QtCommon::Frontend
+1 -7
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef FRONTEND_H
@@ -139,11 +139,5 @@ const QString GetExistingDirectory(const QString &caption = QString(),
const QString &dir = QString(),
Options options = Option::ShowDirsOnly);
int Choice(const QString& title = QString(), const QString& caption = QString(),
const QStringList& options = {});
const QString GetTextInput(const QString& title = QString(), const QString& caption = QString(),
const QString& defaultText = QString());
} // namespace QtCommon::Frontend
#endif // FRONTEND_H
+1 -18
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -231,16 +231,6 @@ void QtConfig::ReadPathValues() {
QString::fromStdString(ReadStringSetting(std::string("recentFiles")))
.split(QStringLiteral(", "), Qt::SkipEmptyParts, Qt::CaseSensitive);
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();
ReadCategory(Settings::Category::Paths);
EndGroup();
@@ -456,13 +446,6 @@ void QtConfig::SavePathValues() {
WriteStringSetting(std::string("recentFiles"),
UISettings::values.recent_files.join(QStringLiteral(", ")).toStdString());
BeginArray(std::string("external_content_dirs"));
for (int i = 0; i < static_cast<int>(Settings::values.external_content_dirs.size()); ++i) {
SetArrayIndex(i);
WriteStringSetting(std::string("path"), Settings::values.external_content_dirs[i]);
}
EndArray();
EndGroup();
}
@@ -329,10 +329,6 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent)
barrier_feedback_loops,
tr("Barrier feedback loops"),
tr("Improves rendering of transparency effects in specific games."));
INSERT(Settings,
enable_buffer_history,
tr("Enable buffer history"),
tr("Enables access to previous buffer states.\nThis option may improve rendering quality and performance consistency in some games."));
INSERT(Settings,
fix_bloom_effects,
tr("Fix bloom effects"),
@@ -766,12 +762,6 @@ std::unique_ptr<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent)
PAIR(ExtendedDynamicState, EDS3, tr("ExtendedDynamicState 3")),
}});
translations->insert({Settings::EnumMetadata<Settings::GameListMode>::Index(),
{
PAIR(GameListMode, TreeView, tr("Tree View")),
PAIR(GameListMode, GridView, tr("Grid View")),
}});
#undef PAIR
#undef CTX_PAIR
+1 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
@@ -210,9 +210,6 @@ struct Values {
Setting<u32> folder_icon_size{linkage, 48, "folder_icon_size", Category::UiGameList};
Setting<u8> row_1_text_id{linkage, 3, "row_1_text_id", Category::UiGameList};
Setting<u8> row_2_text_id{linkage, 2, "row_2_text_id", Category::UiGameList};
Setting<Settings::GameListMode> game_list_mode{linkage, Settings::GameListMode::TreeView, "game_list_mode", Category::UiGameList};
Setting<bool> show_game_name{linkage, true, "show_game_name", Category::UiGameList};
std::atomic_bool is_game_list_reload_pending{false};
Setting<bool> cache_game_list{linkage, true, "cache_game_list", Category::UiGameList};
Setting<bool> favorites_expanded{linkage, true, "favorites_expanded", Category::UiGameList};
+3 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -98,7 +98,8 @@ static const constexpr frozen::map<StringKey, frozen::string, 29> strings = {
{FwCheckErrorFirmwareMissing,
QT_TR_NOOP(
"Firmware missing. Firmware is required to run certain games and use the Home Menu.")},
"Firmware missing. Firmware is required to run certain games and use the Home Menu. "
"Versions 19.0.1 or earlier are recommended, as 20.0.0+ is currently experimental.")},
{FwCheckErrorFirmwareCorrupted,
QT_TR_NOOP(
"Firmware reported as present, but was unable to be read. Check for decryption keys and "
+10 -6
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/game.h"
@@ -373,23 +373,27 @@ void RemoveCacheStorage(u64 program_id)
}
// Metadata //
void ResetMetadata(bool show_message) {
void ResetMetadata(bool show_message)
{
const QString title = tr("Reset Metadata Cache");
if (!Common::FS::Exists(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) /
"game_list/")) {
if (!Common::FS::Exists(Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir)
/ "game_list/")) {
if (show_message)
QtCommon::Frontend::Warning(rootObject, title,
QtCommon::Frontend::Warning(rootObject,
title,
tr("The metadata cache is already empty."));
} else if (Common::FS::RemoveDirRecursively(
Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "game_list")) {
if (show_message)
QtCommon::Frontend::Information(rootObject, title,
QtCommon::Frontend::Information(rootObject,
title,
tr("The operation completed successfully."));
UISettings::values.is_game_list_reload_pending.exchange(true);
} else {
if (show_message)
QtCommon::Frontend::Warning(
title,
tr("The metadata cache couldn't be deleted. It might be in use or non-existent."));
}
-139
View File
@@ -1,139 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <filesystem>
#include <JlCompress.h>
#include "frontend_common/mod_manager.h"
#include "mod.h"
#include "qt_common/abstract/frontend.h"
namespace QtCommon::Mod {
QStringList GetModFolders(const QString& root, const QString& fallbackName) {
namespace fs = std::filesystem;
const auto std_root = root.toStdString();
auto paths = FrontendCommon::GetModFolder(std_root);
// multi mod zip
if (paths.size() > 1) {
// We just have to assume it's properly formed here.
// If not, you're out of luck.
QStringList qpaths;
for (const fs::path& path : paths) {
qpaths << QString::fromStdString(path.string());
}
return qpaths;
}
// either frontend didn't detect any romfs/exefs, or is a single-mod zip
else {
fs::path std_path;
if (!paths.empty())
std_path = paths[0];
QString default_name;
if (!fallbackName.isEmpty())
default_name = fallbackName;
else if (!paths.empty())
default_name = QString::fromStdString(std_path.filename().string());
else
default_name = root.split(QLatin1Char('/')).last();
QString name = QtCommon::Frontend::GetTextInput(
tr("Mod Name"), tr("What should this mod be called?"), default_name);
if (name.isEmpty()) return {};
// if std_path is empty, frontend_common could not determine mod type and/or name.
// so we have to prompt the user and set up the structure ourselves
if (paths.empty()) {
// TODO: Carboxyl impl.
const QStringList choices = {
tr("RomFS"),
tr("ExeFS/Patch"),
tr("Cheat"),
};
int choice = QtCommon::Frontend::Choice(
tr("Mod Type"),
tr("Could not detect mod type automatically. Please manually "
"specify the type of mod you downloaded.\n\nMost mods are RomFS mods, but "
"patches "
"(.pchtxt) are typically ExeFS mods."),
choices);
std::string to_make;
switch (choice) {
case 0:
to_make = "romfs";
break;
case 1:
to_make = "exefs";
break;
case 2:
to_make = "cheats";
break;
default:
return {};
}
// now make a temp directory...
const auto mod_dir = fs::temp_directory_path() / "eden" / "mod" / name.toStdString();
const auto tmp = mod_dir / to_make;
fs::remove_all(mod_dir);
if (!fs::create_directories(tmp)) {
LOG_ERROR(Frontend, "Failed to create temporary directory {}", tmp.string());
return {};
}
std_path = mod_dir;
// ... and copy everything from the root to the temp dir
for (const auto& entry : fs::directory_iterator(root.toStdString())) {
const auto target = tmp / entry.path().filename();
fs::copy(entry.path(), target,
fs::copy_options::recursive | fs::copy_options::overwrite_existing);
}
} else {
// Rename the existing mod folder.
const auto new_path = std_path.parent_path() / name.toStdString();
fs::rename(std_path, new_path);
std_path = new_path;
}
return {QString::fromStdString(std_path.string())};
}
}
// TODO(crueter): Make this a common extract_to_tmp func
const QString ExtractMod(const QString& path) {
namespace fs = std::filesystem;
fs::path tmp{fs::temp_directory_path() / "eden" / "unzip_mod"};
fs::remove_all(tmp);
if (!fs::create_directories(tmp)) {
QtCommon::Frontend::Critical(tr("Mod Extract Failed"),
tr("Failed to create temporary directory %1")
.arg(QString::fromStdString(tmp.string())));
return QString();
}
QString qCacheDir = QString::fromStdString(tmp.string());
QFile zip{path};
// TODO(crueter): use QtCompress
QStringList result = JlCompress::extractDir(&zip, qCacheDir);
if (result.isEmpty()) {
QtCommon::Frontend::Critical(tr("Mod Extract Failed"),
tr("Zip file %1 is empty").arg(path));
return QString();
}
return qCacheDir;
}
} // namespace QtCommon::Mod
-16
View File
@@ -1,16 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QString>
#include "common/common_types.h"
#include "frontend_common/mod_manager.h"
namespace QtCommon::Mod {
QStringList GetModFolders(const QString &root, const QString &fallbackName);
const QString ExtractMod(const QString &path);
}
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -151,13 +148,10 @@ Id EmitConvertU32U64(EmitContext& ctx, Id value) {
}
Id EmitConvertF16F32(EmitContext& ctx, Id value) {
#ifdef ANDROID
return ctx.OpFConvert(ctx.F16[1], value);
#else
const auto result = ctx.OpFConvert(ctx.F16[1], value);
const auto isOverflowing = ctx.OpIsNan(ctx.U1, result);
return ctx.OpSelect(ctx.F16[1], isOverflowing, ctx.Constant(ctx.F16[1], 0), result);
#endif
//return ctx.OpFConvert(ctx.F16[1], value);
}
Id EmitConvertF32F16(EmitContext& ctx, Id value) {

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