Compare commits

..

24 Commits

Author SHA1 Message Date
lizzie 5cd6f7d6c0 [vk] Fix F32/U32 missing conversions where the format is A8R8G8B8_UINT and vk requires the format to actually be inbdexed by an integer composite op (texel fetch)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
2026-02-09 22:59:35 +00:00
lizzie f40e675aa2 Revert "[vk] Simplified layouts per driver configuration (#3271)"
This reverts commit 75fda70db2.
2026-02-09 21:01:55 +00:00
MrPurple666 866881d0e3 [android] add FD_DEV_FEATURES in env loader (#3493)
This environment variable fixes some glitches in OneUI 7 and HyperOS 3.

Thanks StevenMX for letting me know.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3493
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: MrPurple666 <antoniosacramento666usa@gmail.com>
Co-committed-by: MrPurple666 <antoniosacramento666usa@gmail.com>
2026-02-09 04:26:43 +01:00
lizzie a56b8d3de8 [core, windows] remove microSleep() and simply wait on events like on linux (#3498)
we shall see if the original code was put there for a reason

or if the microsleeps actually are horrid

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3498
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-09 01:14:32 +01:00
Maufeat e544cb3cf6 [nce] add split patch mode to support modules bigger than 128MB (#3489)
nce patcher was extended to support modules larger than 128MB due to ARM64 branch limit. now added a pre-patch and (existing) post-patch module code. Allowwing MRS/MSR/SVC handler to remain within branch branch range

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3489
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
2026-02-07 22:59:38 +01:00
crueter ca9f2d43be [desktop] Add icon-only mode to grid and improve design (#3485)
- Move Game Icon Size to the main toolbar. It's cleaner that way
- Add a "Show Game Name" toggle that does as it says. Disabling it
  basically creates an "icons-only" mode. Useful for controller-only
  nav with big icons (TODO: maybe make a 192 size?)
- Fixed a crash with controller nav. Oops
- Rounded corners of the game icon in grid mode
- Fixed the scroll bar creating extra clamping range on the grid icons
- Item can be deselected if user clicks on the blank space outside of the view

As a bonus fixed a crash on mod manager

Future TODOs for design:
- [ ] Row 1 type. Not sure what to do here tbh.
- [ ] Move around game list settings in configure_ui to make it clear
  that nothing there affects the grid view.
- [ ] 192x192 size? 256 feels too big on my 1440p screen whereas 128
  feels too small.
- Set text space as a function of fontMetrics.

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3485
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
2026-02-07 22:48:39 +01:00
wildcard e10f55d9db [host_shaders] Store the value of results instead of discarding it (#3464)
the function generated the value but we never stored it effectively discarding the result.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3464
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: wildcard <wildcard@eden-emu.dev>
Co-committed-by: wildcard <wildcard@eden-emu.dev>
2026-02-07 22:44:32 +01:00
PavelBARABANOV 71e035f83b [android] Fix nightly build download URLs (#3487)
It should work now, I hope.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3487
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
Co-committed-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
2026-02-07 03:02:36 +01:00
Maufeat 16f2564478 [am] terminate app, if caller applet is available (#3486)
Changes ExitProcessAndReturn to terminate process if caller is available (e.g. a parent applet) and start program_index 0 if not.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3486
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
2026-02-07 02:12:04 +01:00
Nekle 75fda70db2 [vk] Simplified layouts per driver configuration (#3271)
Replaces VK_PIPELINE_STAGE_ALL_COMMANDS_BIT with more specific pipeline stage flags in Vulkan pipeline barriers across renderer modules.

Co-authored-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3271
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: Nekle <224100951+ne-kle@users.noreply.github.com>
Co-committed-by: Nekle <224100951+ne-kle@users.noreply.github.com>
2026-02-07 01:37:47 +01:00
crueter b9e052b3a7 [desktop] Basic grid view implementation (#3479)
Closes #3441

Basic impl of a grid view on the game list. The ideal solution here
would be to use QSortFilterProxyModel and abstract the game list model
out to a QStandardItemModel, but that is too much effort for me rn.
Adapted the "card" design from QML, can 1000% be improved but QPainter
is just such a pain to deal with. Implanting a Qt Quick scene into there
would legitimately be easier.

Anyways, margins and text sizes lgtm at all sizes, though please give
feedback on both that and the general card design.

Future TODOs:
- [ ] Auto size mode
- [ ] Refactor to use models

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3479
2026-02-06 19:51:01 +01:00
Maufeat 69aff83ef4 [fs/core] Load external content without NAND install (#2862)
Adds the capability to add DLC and Updates without installing them to NAND. This was tested on Windows only and needs Android integration.

Co-authored-by: crueter <crueter@eden-emu.dev>
Co-authored-by: wildcard <wildcard@eden-emu.dev>
Co-authored-by: nekle <nekle@protonmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2862
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
2026-02-06 14:05:44 +01:00
crueter e07e269bd7 [desktop] Add mod importer from folder and zip (#3472)
Closes #3125

Adds buttons to the addons page that imports a mod (or mods) from zip or folder.

Currently known to work with mods that provide proper romfs/exefs things, unsure about cheats and such. Also works on mods that just stuff things into the root of the zip.

TODO:
- [ ] test folder more thoroughly
- [ ] cheats
- [ ] test all sorts of mod pack types

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3472
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
2026-02-06 06:37:30 +01:00
Maufeat 08232ce642 [core, am] stub SetGpuTimeSliceBoost & fix ExitProcessAndReturn (#3475)
A few things noticed on "Super Mario Allstars", this stubs SetGpuTimeSliceBoost and adds a fix for the "Return to title selection" menü.

Basically ExitProcessAndReturn now starts/restarts the process on program_index 0 (main menu / title screen) and fixes a device memory clear when using that method.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3475
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
2026-02-05 23:46:52 +01:00
lizzie d536a66010 [port, cmake] fix NetBSD/OpenBSD 7.8 build failure, update documentation (#3272)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3272
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 22:47:17 +01:00
lizzie 8907523ee4 [tools/windows] fix Windows on ARM installs for vulkan (#3201)
while we dont support MSVC on ARM64 yet, one day we may, and when that day comes someone will be annoyed their vulkan sdk won't install properly

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3201
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 22:44:09 +01:00
lizzie 9b1f6801a5 [vk] blacklist on NVIDIA drivers older than 580.119.02 the VK_EXT_vertex_input_dynamic_state (#3379)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3379
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 22:38:59 +01:00
lizzie ff03ef657a [cmake, macos] use CMAKE_SOURCE_DIR instead of relative icon paths (#3370)
should be a pretty innocuous change I believe

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3370
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 22:38:55 +01:00
John 5acddfde16 [video_core] Partial Revert #3246 to Improve Performance (#3456)
Improves performance and stuttering in MP4 noticeable on the Steamdeck.
https://git.eden-emu.dev/eden-emu/eden/pulls/3456#issuecomment-19064

LM3 Fixed in #3457
Attempt to fix a flickering issue noticed with LM3
<video src="/attachments/6bf12c4b-a67b-469a-9806-db8390be02ff" title="Flicker" controls></video>

Co-authored-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3456
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: John <john@eden-emu.dev>
Co-committed-by: John <john@eden-emu.dev>
2026-02-05 18:08:24 +01:00
lizzie 913a74dbd3 [common] remove uneeded allocations in logs (#3409)
+ make filename be a const char
also mark as `noexcept` to avoid emission of uneeded unwind paths :)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3409
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 05:47:43 +01:00
lizzie cbb92e75d3 [common] log modified settings first (#3132)
basically i want to see the modified options first :)
one of the few times i use deque without resorting to boost::container::deque, haha

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3132
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 05:22:47 +01:00
lizzie bd7d0e0466 [dynarmic] remove RNG from ARM64 regalloc, use incremental LRU (#3416)
- Backported from Azahar's dynarmic <https://github.com/azahar-emu/dynarmic/commit/526227eebe1efff3fb14dbf494b9c5b44c2e9c1f>

> From: PabloMK7 <hackyglitch2@gmail.com>
> Date: Sun, 4 Jan 2026 13:57:44 +0100
> Subject: [PATCH] backend: Do not use random generator for reg allocation (#7)

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3416
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-05 05:09:46 +01:00
Caio Oliveira cce77b8d11 [frontend] Remove 19.0.1+ warnings (#3463)
Signed-off-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3463
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: crueter <crueter@eden-emu.dev>
Co-authored-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Co-committed-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
2026-02-05 05:09:04 +01:00
nekle c2f4449b1c [android] Add profile management (#3461)
There could be an issue with save files being wiped if updating from an older version, this is due to profiles being hard set on android previously but am not sure, needs testing

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3461
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: nekle <nekle@protonmail.com>
Co-committed-by: nekle <nekle@protonmail.com>
2026-02-05 02:17:08 +01:00
128 changed files with 5767 additions and 1152 deletions
+5 -4
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")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I${CMAKE_SYSROOT}/usr/X11R7/include -I${CMAKE_SYSROOT}/usr/pkg/include/c++/v1")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L${CMAKE_SYSROOT}/usr/X11R7/lib")
endif()
@@ -246,11 +246,12 @@ 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)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
# - 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)
@@ -263,7 +264,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)
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
endif()
+2
View File
@@ -85,6 +85,8 @@ 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 llvm-19.1.7p3 qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl2 libusb1-1.0.27
pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gmake qt6 jq fmt nlohmann-json enet boost vulkan-utility-libraries vulkan-headers spirv-headers spirv-tools catch2 sdl2 libusb1-1.0.29
```
[Caveats](./Caveats.md#openbsd).
+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):
If you have an HDD, use ramdisk (build in RAM), approximatedly you need 4GB for a full build with debug symbols:
```sh
sudo mkdir /tmp/ramdisk
sudo chmod 777 /tmp/ramdisk
mkdir /tmp/ramdisk
chmod 777 /tmp/ramdisk
# about 8GB needed
sudo mount -t tmpfs -o size=8G myramdisk /tmp/ramdisk
mount -t tmpfs -o size=4G myramdisk /tmp/ramdisk
cmake -B /tmp/ramdisk
cmake --build /tmp/ramdisk -- -j32
sudo umount /tmp/ramdisk
umount /tmp/ramdisk
```
# Assets and large files
@@ -614,4 +614,23 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -10,6 +10,7 @@ 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
@@ -31,7 +32,12 @@ class AddonAdapter(val addonViewModel: AddonViewModel) :
binding.addonSwitch.isChecked = model.enabled
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
model.enabled = checked
if (PatchType.from(model.type) == PatchType.Update && checked) {
addonViewModel.enableOnlyThisUpdate(model)
notifyDataSetChanged()
} else {
model.enabled = checked
}
}
val deleteAction = {
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.adapters
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.databinding.ItemFirmwareAvatarBinding
class FirmwareAvatarAdapter(
private val avatars: List<Bitmap>,
private val onAvatarSelected: (Bitmap) -> Unit
) : RecyclerView.Adapter<FirmwareAvatarAdapter.AvatarViewHolder>() {
private var selectedPosition = -1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AvatarViewHolder {
val binding = ItemFirmwareAvatarBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return AvatarViewHolder(binding)
}
override fun onBindViewHolder(holder: AvatarViewHolder, position: Int) {
holder.bind(avatars[position], position == selectedPosition)
}
override fun getItemCount(): Int = avatars.size
inner class AvatarViewHolder(
private val binding: ItemFirmwareAvatarBinding
) : RecyclerView.ViewHolder(binding.root) {
fun bind(avatar: Bitmap, isSelected: Boolean) {
binding.imageAvatar.setImageBitmap(avatar)
binding.root.isChecked = isSelected
binding.root.setOnClickListener {
val previousSelected = selectedPosition
selectedPosition = bindingAdapterPosition
if (previousSelected != -1) {
notifyItemChanged(previousSelected)
}
notifyItemChanged(selectedPosition)
onAvatarSelected(avatar)
}
}
}
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -7,8 +10,10 @@ 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
@@ -31,6 +36,12 @@ 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(
@@ -0,0 +1,112 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.adapters
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ListItemProfileBinding
import org.yuzu.yuzu_emu.model.UserProfile
import java.io.File
import org.yuzu.yuzu_emu.NativeLibrary
class ProfileAdapter(
private val onProfileClick: (UserProfile) -> Unit,
private val onEditClick: (UserProfile) -> Unit,
private val onDeleteClick: (UserProfile) -> Unit
) : RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
private var currentUserUUID: String = ""
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<UserProfile>() {
override fun areItemsTheSame(oldItem: UserProfile, newItem: UserProfile): Boolean {
return oldItem.uuid == newItem.uuid
}
override fun areContentsTheSame(oldItem: UserProfile, newItem: UserProfile): Boolean {
return oldItem == newItem
}
})
fun submitList(list: List<UserProfile>) {
differ.submitList(list)
}
fun setCurrentUser(uuid: String) {
currentUserUUID = uuid
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
val binding = ListItemProfileBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ProfileViewHolder(binding)
}
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
holder.bind(differ.currentList[position])
}
override fun getItemCount(): Int = differ.currentList.size
inner class ProfileViewHolder(private val binding: ListItemProfileBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(profile: UserProfile) {
binding.textUsername.text = profile.username
binding.textUuid.text = formatUUID(profile.uuid)
val imageFile = File(profile.imagePath)
if (imageFile.exists()) {
val bitmap = BitmapFactory.decodeFile(profile.imagePath)
binding.imageAvatar.setImageBitmap(bitmap)
} else {
val jpegData = NativeLibrary.getDefaultAccountBackupJpeg()
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
binding.imageAvatar.setImageBitmap(bitmap)
}
if (profile.uuid == currentUserUUID) {
binding.checkContainer.visibility = View.VISIBLE
} else {
binding.checkContainer.visibility = View.GONE
}
binding.root.setOnClickListener {
onProfileClick(profile)
}
binding.buttonEdit.setOnClickListener {
onEditClick(profile)
}
binding.buttonDelete.setOnClickListener {
onDeleteClick(profile)
}
}
private fun formatUUID(uuid: String): String {
if (uuid.length != 32) return uuid
return buildString {
append(uuid.substring(0, 8))
append("-")
append(uuid.substring(8, 12))
append("-")
append(uuid.substring(12, 16))
append("-")
append(uuid.substring(16, 20))
append("-")
append(uuid.substring(20, 32))
}
}
}
}
@@ -0,0 +1,459 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.FirmwareAvatarAdapter
import org.yuzu.yuzu_emu.databinding.FragmentEditUserDialogBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.ProfileUtils
import org.yuzu.yuzu_emu.model.UserProfile
import java.io.File
import java.io.FileOutputStream
import androidx.core.graphics.scale
import androidx.core.graphics.createBitmap
class EditUserDialogFragment : Fragment() {
private var _binding: FragmentEditUserDialogBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private var currentUUID: String = ""
private var isEditMode = false
private var selectedImageUri: Uri? = null
private var selectedFirmwareAvatar: Bitmap? = null
private var hasCustomImage = false
private var revertedToDefault = false
companion object {
private const val ARG_UUID = "uuid"
private const val ARG_USERNAME = "username"
fun newInstance(profile: UserProfile?): EditUserDialogFragment {
val fragment = EditUserDialogFragment()
profile?.let {
val args = Bundle()
args.putString(ARG_UUID, it.uuid)
args.putString(ARG_USERNAME, it.username)
fragment.arguments = args
}
return fragment
}
}
private val imagePickerLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let { uri ->
selectedImageUri = uri
loadImage(uri)
hasCustomImage = true
binding.buttonRevertImage.visibility = View.VISIBLE
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentEditUserDialogBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = false)
val existingUUID = arguments?.getString(ARG_UUID)
val existingUsername = arguments?.getString(ARG_USERNAME)
if (existingUUID != null && existingUsername != null) {
isEditMode = true
currentUUID = existingUUID
binding.toolbarNewUser.title = getString(R.string.profile_edit_user)
binding.editUsername.setText(existingUsername)
binding.textUuid.text = formatUUID(existingUUID)
binding.buttonGenerateUuid.visibility = View.GONE
val imagePath = NativeLibrary.getUserImagePath(existingUUID)
val imageFile = File(imagePath)
if (imageFile.exists()) {
val bitmap = BitmapFactory.decodeFile(imagePath)
binding.imageUserAvatar.setImageBitmap(bitmap)
hasCustomImage = true
binding.buttonRevertImage.visibility = View.VISIBLE
} else {
loadDefaultAvatar()
}
} else {
isEditMode = false
currentUUID = ProfileUtils.generateRandomUUID()
binding.toolbarNewUser.title = getString(R.string.profile_new_user)
binding.textUuid.text = formatUUID(currentUUID)
loadDefaultAvatar()
}
binding.toolbarNewUser.setNavigationOnClickListener {
findNavController().popBackStack()
}
binding.editUsername.doAfterTextChanged {
validateInput()
}
binding.buttonGenerateUuid.setOnClickListener {
currentUUID = ProfileUtils.generateRandomUUID()
binding.textUuid.text = formatUUID(currentUUID)
}
binding.buttonSelectImage.setOnClickListener {
selectImage()
}
binding.buttonRevertImage.setOnClickListener {
revertToDefaultImage()
}
if (NativeLibrary.isFirmwareAvailable()) {
binding.buttonFirmwareAvatars.visibility = View.VISIBLE
binding.buttonFirmwareAvatars.setOnClickListener {
showFirmwareAvatarPicker()
}
}
binding.buttonSave.setOnClickListener {
saveUser()
}
binding.buttonCancel.setOnClickListener {
findNavController().popBackStack()
}
validateInput()
setInsets()
}
private fun showFirmwareAvatarPicker() {
val dialogView = LayoutInflater.from(requireContext())
.inflate(R.layout.dialog_firmware_avatar_picker, null)
val gridAvatars = dialogView.findViewById<RecyclerView>(R.id.grid_avatars)
val progressLoading = dialogView.findViewById<View>(R.id.progress_loading)
val textEmpty = dialogView.findViewById<View>(R.id.text_empty)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.profile_firmware_avatars)
.setView(dialogView)
.setNegativeButton(android.R.string.cancel, null)
.create()
dialog.show()
viewLifecycleOwner.lifecycleScope.launch {
val avatars = withContext(Dispatchers.IO) {
loadFirmwareAvatars()
}
if (avatars.isEmpty()) {
progressLoading.visibility = View.GONE
textEmpty.visibility = View.VISIBLE
} else {
progressLoading.visibility = View.GONE
gridAvatars.visibility = View.VISIBLE
val adapter = FirmwareAvatarAdapter(avatars) { selectedAvatar ->
val scaledBitmap = selectedAvatar.scale(256, 256)
binding.imageUserAvatar.setImageBitmap(scaledBitmap)
selectedFirmwareAvatar = scaledBitmap
hasCustomImage = true
binding.buttonRevertImage.visibility = View.VISIBLE
dialog.dismiss()
}
gridAvatars.apply {
layoutManager = GridLayoutManager(requireContext(), 4)
this.adapter = adapter
}
}
}
}
private fun loadFirmwareAvatars(): List<Bitmap> {
val avatars = mutableListOf<Bitmap>()
val count = NativeLibrary.getFirmwareAvatarCount()
for (i in 0 until count) {
try {
val imageData = NativeLibrary.getFirmwareAvatarImage(i) ?: continue
val argbData = IntArray(256 * 256)
for (pixel in 0 until 256 * 256) {
val offset = pixel * 4
val r = imageData[offset].toInt() and 0xFF
val g = imageData[offset + 1].toInt() and 0xFF
val b = imageData[offset + 2].toInt() and 0xFF
val a = imageData[offset + 3].toInt() and 0xFF
argbData[pixel] = (a shl 24) or (r shl 16) or (g shl 8) or b
}
val bitmap = Bitmap.createBitmap(argbData, 256, 256, Bitmap.Config.ARGB_8888)
avatars.add(bitmap)
} catch (e: Exception) {
continue
}
}
return avatars
}
private fun formatUUID(uuid: String): String {
if (uuid.length != 32) return uuid
return buildString {
append(uuid.substring(0, 8))
append("-")
append(uuid.substring(8, 12))
append("-")
append(uuid.substring(12, 16))
append("-")
append(uuid.substring(16, 20))
append("-")
append(uuid.substring(20, 32))
}
}
private fun validateInput() {
val username = binding.editUsername.text.toString()
val isValid = username.isNotEmpty() && username.length <= 32
binding.buttonSave.isEnabled = isValid
}
private fun selectImage() {
val intent = Intent(Intent.ACTION_PICK).apply {
type = "image/*"
}
imagePickerLauncher.launch(intent)
}
private fun loadImage(uri: Uri) {
try {
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source = ImageDecoder.createSource(requireContext().contentResolver, uri)
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
decoder.setTargetSampleSize(1)
}
} else {
@Suppress("DEPRECATION")
MediaStore.Images.Media.getBitmap(requireContext().contentResolver, uri)
}
val croppedBitmap = centerCropBitmap(bitmap, 256, 256)
binding.imageUserAvatar.setImageBitmap(croppedBitmap)
} catch (e: Exception) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.error)
.setMessage(getString(R.string.profile_image_load_error, e.message))
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
private fun loadDefaultAvatar() {
val jpegData = NativeLibrary.getDefaultAccountBackupJpeg()
val bitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.size)
binding.imageUserAvatar.setImageBitmap(bitmap)
hasCustomImage = false
binding.buttonRevertImage.visibility = View.GONE
}
private fun revertToDefaultImage() {
selectedImageUri = null
selectedFirmwareAvatar = null
revertedToDefault = true
loadDefaultAvatar()
}
private fun saveUser() {
val username = binding.editUsername.text.toString()
if (isEditMode) {
if (NativeLibrary.updateUserUsername(currentUUID, username)) {
saveImageIfNeeded()
findNavController().popBackStack()
} else {
showError(getString(R.string.profile_update_failed))
}
} else {
if (NativeLibrary.createUser(currentUUID, username)) {
saveImageIfNeeded()
findNavController().popBackStack()
} else {
showError(getString(R.string.profile_create_failed))
}
}
}
private fun saveImageIfNeeded() {
if (revertedToDefault && isEditMode) {
val imagePath = NativeLibrary.getUserImagePath(currentUUID)
if (imagePath != null) {
val imageFile = File(imagePath)
if (imageFile.exists()) {
imageFile.delete()
}
}
return
}
if (!hasCustomImage) {
return
}
try {
val bitmapToSave: Bitmap? = when {
selectedFirmwareAvatar != null -> selectedFirmwareAvatar
selectedImageUri != null -> {
val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val source = ImageDecoder.createSource(
requireContext().contentResolver,
selectedImageUri!!
)
ImageDecoder.decodeBitmap(source)
} else {
@Suppress("DEPRECATION")
MediaStore.Images.Media.getBitmap(
requireContext().contentResolver,
selectedImageUri
)
}
centerCropBitmap(bitmap, 256, 256)
}
else -> null
}
if (bitmapToSave == null) {
return
}
val tempFile = File(requireContext().cacheDir, "temp_avatar_${currentUUID}.jpg")
FileOutputStream(tempFile).use { out ->
bitmapToSave.compress(Bitmap.CompressFormat.JPEG, 100, out)
}
NativeLibrary.saveUserImage(currentUUID, tempFile.absolutePath)
tempFile.delete()
} catch (e: Exception) {
showError(getString(R.string.profile_image_save_error, e.message))
}
}
private fun centerCropBitmap(source: Bitmap, targetWidth: Int, targetHeight: Int): Bitmap {
val sourceWidth = source.width
val sourceHeight = source.height
val scale = maxOf(
targetWidth.toFloat() / sourceWidth,
targetHeight.toFloat() / sourceHeight
)
val scaledWidth = (sourceWidth * scale).toInt()
val scaledHeight = (sourceHeight * scale).toInt()
val scaledBitmap = source.scale(scaledWidth, scaledHeight)
val x = (scaledWidth - targetWidth) / 2
val y = (scaledHeight - targetHeight) / 2
return Bitmap.createBitmap(scaledBitmap, x, y, targetWidth, targetHeight)
}
private fun showError(message: String) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.error)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show()
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInset = barInsets.left + cutoutInsets.left
val topInset = cutoutInsets.top
val rightInset = barInsets.right + cutoutInsets.right
val bottomInset = barInsets.bottom + cutoutInsets.bottom
binding.appbar.updatePadding(
left = leftInset,
top = topInset,
right = rightInset
)
binding.scrollContent.updatePadding(
left = leftInset,
right = rightInset
)
binding.buttonContainer.updatePadding(
left = leftInset,
right = rightInset,
bottom = bottomInset
)
windowInsets
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
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"
"TU_BREADCRUMBS", "FD_DEV_FEATURES"
)
for (varName in commonVars) {
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -6,11 +9,13 @@ 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
@@ -25,14 +30,18 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
// Restore checkbox state
binding.deepScanSwitch.isChecked =
savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
// 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
// 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())
@@ -41,8 +50,10 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
if (folderIndex != -1) {
gamesViewModel.folders.value[folderIndex].deepScan =
binding.deepScanSwitch.isChecked
if (gameDir.type == DirectoryType.GAME) {
gamesViewModel.folders.value[folderIndex].deepScan =
binding.deepScanSwitch.isChecked
}
gamesViewModel.updateGameDirs()
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -15,11 +15,14 @@ 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
@@ -73,7 +76,25 @@ class GameFoldersFragment : Fragment() {
val mainActivity = requireActivity() as MainActivity
binding.buttonAdd.setOnClickListener {
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
// Show a model to choose between Game and External Content
val options = arrayOf(
getString(R.string.games),
getString(R.string.external_content)
)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.add_folders)
.setItems(options) { _, which ->
when (which) {
0 -> { // Game Folder
mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
1 -> { // External Content Folder
mainActivity.getExternalContentDirectory.launch(null)
}
}
}
.show()
}
setInsets()
@@ -1,6 +1,9 @@
// 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
@@ -114,6 +117,17 @@ class HomeSettingsFragment : Fragment() {
}
)
)
add(
HomeSetting(
R.string.profile_manager,
R.string.profile_manager_description,
R.drawable.ic_account_circle,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_profileManagerFragment)
}
)
)
add(
HomeSetting(
R.string.gpu_driver_manager,
@@ -0,0 +1,190 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.ProfileAdapter
import org.yuzu.yuzu_emu.databinding.FragmentProfileManagerBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.UserProfile
import org.yuzu.yuzu_emu.utils.NativeConfig
class ProfileManagerFragment : Fragment() {
private var _binding: FragmentProfileManagerBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private lateinit var profileAdapter: ProfileAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentProfileManagerBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(visible = false)
binding.toolbarProfiles.setNavigationOnClickListener {
findNavController().popBackStack()
}
setupRecyclerView()
loadProfiles()
binding.buttonAddUser.setOnClickListener {
if (NativeLibrary.canCreateUser()) {
findNavController().navigate(R.id.action_profileManagerFragment_to_newUserDialog)
} else {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.profile_max_users_title)
.setMessage(R.string.profile_max_users_message)
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
setInsets()
}
override fun onResume() {
super.onResume()
loadProfiles()
}
private fun setupRecyclerView() {
profileAdapter = ProfileAdapter(
onProfileClick = { profile -> selectProfile(profile) },
onEditClick = { profile -> editProfile(profile) },
onDeleteClick = { profile -> confirmDeleteProfile(profile) }
)
binding.listProfiles.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = profileAdapter
}
}
private fun loadProfiles() {
val profiles = mutableListOf<UserProfile>()
val userUUIDs = NativeLibrary.getAllUsers() ?: emptyArray()
val currentUserUUID = NativeLibrary.getCurrentUser()
for (uuid in userUUIDs) {
if (uuid.isNotEmpty()) {
val username = NativeLibrary.getUserUsername(uuid)
if (!username.isNullOrEmpty()) {
val imagePath = NativeLibrary.getUserImagePath(uuid) ?: ""
profiles.add(UserProfile(uuid, username, imagePath))
}
}
}
profileAdapter.submitList(profiles)
profileAdapter.setCurrentUser(currentUserUUID ?: "")
binding.buttonAddUser.isEnabled = NativeLibrary.canCreateUser()
}
private fun selectProfile(profile: UserProfile) {
if (NativeLibrary.setCurrentUser(profile.uuid)) {
loadProfiles()
}
}
private fun editProfile(profile: UserProfile) {
val bundle = Bundle().apply {
putString("uuid", profile.uuid)
putString("username", profile.username)
}
findNavController().navigate(R.id.action_profileManagerFragment_to_newUserDialog, bundle)
}
private fun confirmDeleteProfile(profile: UserProfile) {
val currentUser = NativeLibrary.getCurrentUser()
val isCurrentUser = profile.uuid == currentUser
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.profile_delete_confirm_title)
.setMessage(
if (isCurrentUser) {
getString(R.string.profile_delete_current_user_message, profile.username)
} else {
getString(R.string.profile_delete_confirm_message, profile.username)
}
)
.setPositiveButton(R.string.profile_delete) { _, _ ->
deleteProfile(profile)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun deleteProfile(profile: UserProfile) {
val currentUser = NativeLibrary.getCurrentUser()
if (!currentUser.isNullOrEmpty() && profile.uuid == currentUser) {
val users = NativeLibrary.getAllUsers() ?: emptyArray()
for (uuid in users) {
if (uuid.isNotEmpty() && uuid != profile.uuid) {
NativeLibrary.setCurrentUser(uuid)
break
}
}
}
if (NativeLibrary.removeUser(profile.uuid)) {
loadProfiles()
}
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val fabLayoutParams = binding.buttonAddUser.layoutParams as ViewGroup.MarginLayoutParams
fabLayoutParams.leftMargin = leftInsets + 24
fabLayoutParams.rightMargin = rightInsets + 24
fabLayoutParams.bottomMargin = barInsets.bottom + 24
binding.buttonAddUser.layoutParams = fabLayoutParams
windowInsets
}
}
override fun onDestroyView() {
super.onDestroyView()
NativeConfig.saveGlobalConfig()
_binding = null
}
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -48,16 +51,68 @@ 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)
@@ -72,13 +127,27 @@ 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 {
it.name
if (PatchType.from(it.type) == PatchType.Update) {
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
it.name
} else if (hasMultipleUpdates) {
"Update@${it.numericVersion}"
} else {
it.name
}
} else {
it.name
}
}
}.toTypedArray()
)
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -9,5 +12,14 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class GameDir(
val uriString: String,
var deepScan: Boolean
) : Parcelable
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
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
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()
getGameDirs()
getGameDirsAndExternalContent()
reloadGames(directoriesChanged = false, firstStartup = true)
}
@@ -144,11 +144,19 @@ class GamesViewModel : ViewModel() {
fun addFolder(gameDir: GameDir, savedFromGameFragment: Boolean) =
viewModelScope.launch {
withContext(Dispatchers.IO) {
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)
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()
}
}
}
if (savedFromGameFragment) {
@@ -168,8 +176,15 @@ class GamesViewModel : ViewModel() {
val removedDirIndex = gameDirs.indexOf(gameDir)
if (removedDirIndex != -1) {
gameDirs.removeAt(removedDirIndex)
NativeConfig.setGameDirs(gameDirs.toTypedArray())
getGameDirs()
when (gameDir.type) {
DirectoryType.GAME -> {
NativeConfig.setGameDirs(gameDirs.filter { it.type == DirectoryType.GAME }.toTypedArray())
}
DirectoryType.EXTERNAL_CONTENT -> {
removeExternalContentDir(gameDir.uriString)
}
}
getGameDirsAndExternalContent()
}
}
}
@@ -177,15 +192,16 @@ class GamesViewModel : ViewModel() {
fun updateGameDirs() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
NativeConfig.setGameDirs(_folders.value.toTypedArray())
getGameDirs()
val gameDirs = _folders.value.filter { it.type == DirectoryType.GAME }
NativeConfig.setGameDirs(gameDirs.toTypedArray())
getGameDirsAndExternalContent()
}
}
fun onOpenGameFoldersFragment() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirs()
getGameDirsAndExternalContent()
}
}
@@ -193,16 +209,36 @@ class GamesViewModel : ViewModel() {
NativeConfig.saveGlobalConfig()
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirs(true)
getGameDirsAndExternalContent(true)
}
}
}
private fun getGameDirs(reloadList: Boolean = false) {
val gameDirs = NativeConfig.getGameDirs()
_folders.value = gameDirs.toMutableList()
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
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,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -12,5 +15,6 @@ data class Patch(
val version: String,
val type: Int,
val programId: String,
val titleId: String
val titleId: String,
val numericVersion: Long = 0
)
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.model
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class UserProfile(
val uuid: String,
val username: String,
val imagePath: String = ""
) : Parcelable
object ProfileUtils {
fun generateRandomUUID(): String {
val uuid = java.util.UUID.randomUUID()
return uuid.toString().replace("-", "")
}
}
@@ -63,6 +63,7 @@ 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
@@ -199,8 +200,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
var artifact = tag
// Nightly builds have a slightly different format
if (NativeLibrary.isNightlyBuild()) {
val splitTag = tag.split('.')
artifact = splitTag.subList(1, splitTag.size - 1).joinToString(".")
artifact = tag.substringAfter('.', tag)
}
downloadAndInstallUpdate(tag, artifact)
}
@@ -389,6 +389,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
val getExternalContentDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
processExternalContentDir(result)
}
}
fun processGamesDir(result: Uri, calledFromGameFragment: Boolean = false) {
contentResolver.takePersistableUriPermission(
result,
@@ -410,6 +417,27 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -25,6 +25,7 @@ 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,6 +49,17 @@ 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()
@@ -88,6 +99,33 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -204,4 +204,12 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
@@ -144,6 +144,15 @@ object FreedrenoPresets {
)
)
val DEV_FEATURES_UBWC_HINT = FreedrenoPreset(
name = "Dev - UBWC Flag Hint",
description = "Enable TP UBWC flag hint for development",
icon = "ic_dev_features",
variables = mapOf(
"FD_DEV_FEATURES" to "enable_tp_ubwc_flag_hint=1"
)
)
val PERFORMANCE_DEFAULT = FreedrenoPreset(
name = "Performance - Default",
description = "Clear all debug options for performance",
@@ -159,6 +168,7 @@ object FreedrenoPresets {
CAPTURE_FRAMES,
SHADER_DEBUG,
GPU_HANG_TRACE,
DEV_FEATURES_UBWC_HINT,
PERFORMANCE_DEFAULT
)
}
@@ -1,8 +1,9 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#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"
@@ -69,6 +70,18 @@ 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);
@@ -241,6 +254,14 @@ 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,
+496 -13
View File
@@ -49,12 +49,17 @@
#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"
@@ -210,6 +215,109 @@ 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;
@@ -226,17 +334,6 @@ 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());
}
}
}
}
@@ -1331,7 +1428,8 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env
Common::Android::ToJString(env, patch.name),
Common::Android::ToJString(env, patch.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)));
Common::Android::ToJString(env, std::to_string(patch.title_id)),
static_cast<jlong>(patch.numeric_version));
env->SetObjectArrayElement(jpatchArray, i, jpatch);
++i;
}
@@ -1678,7 +1776,7 @@ JNIEXPORT jstring JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_getUpdateApkUrl(
#endif
}
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", version_str, variant);
const std::string apk_filename = fmt::format("Eden-Android-{}-{}.apk", artifact_str, variant);
const std::string url = fmt::format("{}/{}/releases/download/{}/{}",
std::string{Common::g_build_auto_update_website},
std::string{Common::g_build_auto_update_repo},
@@ -1686,6 +1784,7 @@ 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());
}
@@ -1697,4 +1796,388 @@ 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"
@@ -583,4 +583,26 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/**
@@ -258,6 +258,11 @@ Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
}
LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
if (var_name == "FD_DEV_FEATURES") {
LOG_INFO(Frontend, "[Freedreno] FD_DEV_FEATURES enabled: {}", value);
}
return JNI_TRUE;
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorControlNormal"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,5c1.66,0 3,1.34 3,3s-1.34,3 -3,3s-3,-1.34 -3,-3S10.34,5 12,5zM12,19.2c-2.5,0 -4.71,-1.28 -6,-3.22c0.03,-1.99 4,-3.08 6,-3.08c1.99,0 5.97,1.09 6,3.08C16.71,17.92 14.5,19.2 12,19.2z" />
</vector>
@@ -11,7 +11,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:orientation="vertical"
android:padding="16dp"
android:layout_gravity="center_vertical">
@@ -23,12 +23,25 @@
android:layout_gravity="center_vertical|start"
android:requiresFadingEdge="horizontal"
android:textAlignment="viewStart"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/type_indicator"
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"
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="300dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/grid_avatars"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
<ProgressBar
android:id="@+id/progress_loading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<TextView
android:id="@+id/text_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/profile_firmware_avatars_unavailable"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center"
android:padding="16dp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,226 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_new_user"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/profile_new_user"
app:navigationIcon="@drawable/ic_back"
app:titleCentered="false" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="88dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingVertical="16dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="128dp"
android:layout_height="128dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp"
style="@style/Widget.Material3.CardView.Elevated"
app:cardCornerRadius="64dp"
app:cardElevation="4dp">
<ImageView
android:id="@+id/image_user_avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:contentDescription="@string/profile_avatar"
tools:src="@drawable/ic_account_circle" />
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_select_image"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/profile_select_image"
style="@style/Widget.Material3.Button.TonalButton"
app:icon="@drawable/ic_add"
android:layout_marginEnd="4dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_firmware_avatars"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/profile_firmware_avatars"
style="@style/Widget.Material3.Button.TonalButton"
app:icon="@drawable/ic_account_circle"
android:layout_marginStart="4dp"
android:visibility="gone" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_revert_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_revert_image"
style="@style/Widget.Material3.Button.TextButton"
android:visibility="gone" />
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:hint="@string/profile_username">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLength="32"
android:maxLines="1"
android:minHeight="48dp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.Material3.CardView.Filled"
app:cardCornerRadius="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_uuid"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/text_uuid"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:fontFamily="monospace"
android:textIsSelectable="true"
tools:text="12345678-1234-1234-1234-123456789012" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_generate_uuid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/profile_generate"
style="@style/Widget.Material3.Button.TextButton"
app:icon="@drawable/ic_refresh"
app:iconGravity="textStart" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/profile_uuid_description"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginTop="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/button_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
style="@style/Widget.Material3.CardView.Elevated"
app:cardCornerRadius="0dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/button_container"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:paddingBottom="24dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/button_cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="@android:string/cancel"
style="@style/Widget.Material3.Button.TonalButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_save"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="@string/save"
style="@style/Widget.Material3.Button" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSurface"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar_profiles"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/profile_manager"
app:navigationIcon="@drawable/ic_back"
app:titleCentered="false" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_profiles"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="96dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/list_item_profile" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/button_add_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="24dp"
android:text="@string/profile_add_user"
android:contentDescription="@string/profile_add_user"
app:icon="@drawable/ic_add"
app:iconGravity="start"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
style="@style/Widget.Material3.CardView.Elevated"
app:cardCornerRadius="12dp"
android:clickable="true"
android:focusable="true"
android:checkable="true">
<ImageView
android:id="@+id/image_avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:scaleType="centerCrop"
android:contentDescription="@string/profile_avatar" />
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="6dp"
style="@style/Widget.Material3.CardView.Filled"
app:cardCornerRadius="16dp"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<com.google.android.material.card.MaterialCardView
android:id="@+id/avatar_container"
android:layout_width="56dp"
android:layout_height="56dp"
style="@style/Widget.Material3.CardView.Elevated"
app:cardCornerRadius="28dp"
app:cardElevation="1dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/image_avatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:src="@drawable/ic_account_circle"
android:contentDescription="@string/profile_avatar" />
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/check_container"
android:layout_width="20dp"
android:layout_height="20dp"
android:visibility="gone"
style="@style/Widget.Material3.CardView.Filled"
app:cardBackgroundColor="?attr/colorPrimary"
app:cardCornerRadius="10dp"
app:cardElevation="2dp"
app:layout_constraintEnd_toEndOf="@id/avatar_container"
app:layout_constraintBottom_toBottomOf="@id/avatar_container"
tools:visibility="visible">
<ImageView
android:id="@+id/icon_check"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="center"
android:src="@drawable/ic_check"
app:tint="?attr/colorOnPrimary"
android:contentDescription="@string/profile_current_user" />
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/text_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp"
android:layout_marginEnd="8dp"
app:layout_constraintStart_toEndOf="@id/avatar_container"
app:layout_constraintEnd_toStartOf="@id/button_edit"
app:layout_constraintTop_toTopOf="@id/avatar_container"
app:layout_constraintBottom_toBottomOf="@id/avatar_container">
<TextView
android:id="@+id/text_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:maxLines="1"
android:ellipsize="end"
tools:text="User Name" />
<TextView
android:id="@+id/text_uuid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodySmall"
android:maxLines="1"
android:ellipsize="middle"
android:textColor="?attr/colorOnSurfaceVariant"
tools:text="12345678-1234-1234-1234-123456789012" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_edit"
android:layout_width="40dp"
android:layout_height="40dp"
style="@style/Widget.Material3.Button.IconButton"
app:icon="@drawable/ic_edit"
app:iconTint="?attr/colorOnSurfaceVariant"
app:layout_constraintEnd_toStartOf="@id/button_delete"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/profile_edit" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button_delete"
android:layout_width="40dp"
android:layout_height="40dp"
style="@style/Widget.Material3.Button.IconButton"
app:icon="@drawable/ic_delete"
app:iconTint="?attr/colorError"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/profile_delete" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -36,6 +36,9 @@
<action
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
@@ -186,5 +189,17 @@
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 or invalid version</string>
<string name="applets_error_firmware">Firmware not installed</string>
<string name="applets_error_applet">Applet not available</string>
<string name="applets_error_description"><![CDATA[Please ensure your <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> file and <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-system-firmware\">firmware</a> are installed and try again.<br>Additionally, ensure your firmware is of version 19.0.1 or older.]]></string>
<string name="applets_error_description"><![CDATA[Please ensure your <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-prodkeys-and-titlekeys\">prod.keys</a> file and <a href=\"https://yuzu-mirror.github.io/help/quickstart/#dumping-system-firmware\">firmware</a> are installed and try again.]]></string>
<string name="album_applet">Album</string>
<string name="album_applet_description">See images stored in the user screenshots folder with the system photo viewer</string>
<string name="mii_edit_applet">Mii edit</string>
@@ -1239,6 +1239,35 @@
<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>
@@ -1745,4 +1774,8 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
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>
+1 -3
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-License-Identifier:
@@ -186,8 +186,6 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 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;)V");
"(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;J)V");
s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
+60 -54
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -7,6 +7,7 @@
#include <atomic>
#include <chrono>
#include <climits>
#include <cstdlib>
#include <regex>
#include <thread>
@@ -41,7 +42,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) {
constexpr const char* TrimSourcePath(std::string_view source) noexcept {
const auto rfind = [source](const std::string_view match) {
return source.rfind(match) == source.npos ? 0 : (source.rfind(match) + match.size());
};
@@ -51,31 +52,31 @@ constexpr const char* TrimSourcePath(std::string_view source) {
/// @brief Interface for logging backends.
struct Backend {
virtual ~Backend() = default;
virtual void Write(const Entry& entry) = 0;
virtual void EnableForStacktrace() = 0;
virtual void Flush() = 0;
virtual ~Backend() noexcept = default;
virtual void Write(const Entry& entry) noexcept = 0;
virtual void EnableForStacktrace() noexcept= 0;
virtual void Flush() noexcept = 0;
};
/// @brief Backend that writes to stderr and with color
struct ColorConsoleBackend final : public Backend {
explicit ColorConsoleBackend() = default;
~ColorConsoleBackend() override = default;
explicit ColorConsoleBackend() noexcept = default;
~ColorConsoleBackend() noexcept override = default;
void Write(const Entry& entry) override {
void Write(const Entry& entry) noexcept override {
if (enabled.load(std::memory_order_relaxed))
PrintColoredMessage(entry);
}
void Flush() override {
void Flush() noexcept override {
// stderr shouldn't be buffered
}
void EnableForStacktrace() override {
void EnableForStacktrace() noexcept override {
enabled = true;
}
void SetEnabled(bool enabled_) {
void SetEnabled(bool enabled_) noexcept {
enabled = enabled_;
}
@@ -83,9 +84,10 @@ 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) {
explicit FileBackend(const std::filesystem::path& filename) noexcept {
auto old_filename = filename;
old_filename += ".old.txt";
@@ -97,9 +99,9 @@ struct FileBackend final : public Backend {
file = std::make_unique<FS::IOFile>(filename, FS::FileAccessMode::Write, FS::FileType::TextFile);
}
~FileBackend() override = default;
~FileBackend() noexcept override = default;
void Write(const Entry& entry) override {
void Write(const Entry& entry) noexcept override {
if (!enabled)
return;
@@ -147,11 +149,11 @@ struct FileBackend final : public Backend {
}
}
void Flush() override {
void Flush() noexcept override {
file->Flush();
}
void EnableForStacktrace() override {
void EnableForStacktrace() noexcept override {
enabled = true;
bytes_written = 0;
}
@@ -161,29 +163,30 @@ 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() = default;
~DebuggerBackend() override = default;
void Write(const Entry& entry) override {
explicit DebuggerBackend() noexcept = default;
~DebuggerBackend() noexcept override = default;
void Write(const Entry& entry) noexcept override {
::OutputDebugStringW(UTF8ToUTF16W(FormatLogMessage(entry).append(1, '\n')).c_str());
}
void Flush() override {}
void EnableForStacktrace() override {}
void Flush() noexcept override {}
void EnableForStacktrace() noexcept override {}
};
#endif
#ifdef ANDROID
/// @brief Backend that writes to the Android logcat
struct LogcatBackend : public Backend {
explicit LogcatBackend() = default;
~LogcatBackend() override = default;
void Write(const Entry& entry) override {
explicit LogcatBackend() noexcept = default;
~LogcatBackend() noexcept override = default;
void Write(const Entry& entry) noexcept override {
PrintMessageToLogcat(entry);
}
void Flush() override {}
void EnableForStacktrace() override {}
void Flush() noexcept override {}
void EnableForStacktrace() noexcept override {}
};
#endif
@@ -192,13 +195,13 @@ bool initialization_in_progress_suppress_logging = true;
/// @brief Static state as a singleton.
class Impl {
public:
static Impl& Instance() {
static Impl& Instance() noexcept {
if (!instance)
throw std::runtime_error("Using Logging instance before its initialization");
std::abort();
return *instance;
}
static void Initialize() {
static void Initialize() noexcept {
if (instance) {
LOG_WARNING(Log, "Reinitializing logging backend");
return;
@@ -212,25 +215,25 @@ public:
initialization_in_progress_suppress_logging = false;
}
static void Start() {
static void Start() noexcept {
instance->StartBackendThread();
}
static void Stop() {
static void Stop() noexcept {
instance->StopBackendThread();
}
Impl(const Impl&) = delete;
Impl& operator=(const Impl&) = delete;
Impl(const Impl&) noexcept = delete;
Impl& operator=(const Impl&) noexcept = delete;
Impl(Impl&&) = delete;
Impl& operator=(Impl&&) = delete;
Impl(Impl&&) noexcept = delete;
Impl& operator=(Impl&&) noexcept = delete;
void SetGlobalFilter(const Filter& f) {
void SetGlobalFilter(const Filter& f) noexcept {
filter = f;
}
void SetColorConsoleBackendEnabled(bool enabled) {
void SetColorConsoleBackendEnabled(bool enabled) noexcept {
color_console_backend.SetEnabled(enabled);
}
@@ -238,19 +241,21 @@ 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_)
: filter{filter_}, file_backend{file_backend_filename} {}
Impl(const std::filesystem::path& file_backend_filename, const Filter& filter_) noexcept :
filter{filter_}
#ifndef __OPENORBIS__
, file_backend{file_backend_filename}
#endif
{}
~Impl() = default;
~Impl() noexcept = default;
void StartBackendThread() {
void StartBackendThread() noexcept {
backend_thread = std::jthread([this](std::stop_token stop_token) {
Common::SetCurrentThreadName("Logger");
Entry entry;
@@ -271,15 +276,14 @@ private:
});
}
void StopBackendThread() {
void StopBackendThread() noexcept {
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 {
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr, const char* function, std::string&& message) const noexcept {
using std::chrono::duration_cast;
using std::chrono::microseconds;
using std::chrono::steady_clock;
@@ -294,9 +298,11 @@ private:
};
}
void ForEachBackend(auto lambda) {
void ForEachBackend(auto lambda) noexcept {
lambda(static_cast<Backend&>(color_console_backend));
#ifndef __OPENORBIS__
lambda(static_cast<Backend&>(file_backend));
#endif
#ifdef _WIN32
lambda(static_cast<Backend&>(debugger_backend));
#endif
@@ -305,7 +311,7 @@ private:
#endif
}
static void Deleter(Impl* ptr) {
static void Deleter(Impl* ptr) noexcept {
delete ptr;
}
@@ -313,7 +319,9 @@ private:
Filter filter;
ColorConsoleBackend color_console_backend{};
#ifndef __OPENORBIS__
FileBackend file_backend;
#endif
#ifdef _WIN32
DebuggerBackend debugger_backend{};
#endif
@@ -351,9 +359,7 @@ 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))
+6 -5
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -9,17 +12,15 @@
namespace Common::Log {
/**
* 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.
*/
/// @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.
struct Entry {
std::chrono::microseconds timestamp;
Class log_class{};
Level log_level{};
const char* filename = nullptr;
unsigned int line_num = 0;
std::string function;
const char* function = nullptr;
std::string message;
};
+17 -11
View File
@@ -24,24 +24,26 @@
namespace Common::Log {
// Some IDEs prefer <file>:<line> instead, so let's just do that :)
std::string FormatLogMessage(const Entry& entry) {
std::string FormatLogMessage(const Entry& entry) noexcept {
if (!entry.filename) return "";
auto const time_seconds = uint32_t(entry.timestamp.count() / 1000000);
auto const time_fractional = uint32_t(entry.timestamp.count() % 1000000);
char const* class_name = GetLogClassName(entry.log_class);
char const* level_name = GetLevelName(entry.log_level);
auto const class_name = GetLogClassName(entry.log_class);
auto 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);
}
void PrintMessage(const Entry& entry) {
/// @brief Formats and prints a log entry to stderr.
static void PrintMessage(const Entry& entry) noexcept {
#ifdef _WIN32
auto const str = FormatLogMessage(entry).append(1, '\n');
fwrite(str.c_str(), 1, str.size(), stderr);
#else
#define ESC "\x1b"
auto str = std::string{[&entry]() -> const char* {
auto const color_str = [&entry]() -> const char* {
switch (entry.log_level) {
case Level::Debug: return ESC "[0;36m"; // Cyan
case Level::Info: return ESC "[0;37m"; // Bright gray
@@ -50,15 +52,19 @@ void PrintMessage(const Entry& entry) {
case Level::Critical: return ESC "[1;35m"; // Bright magenta
default: return ESC "[1;30m"; // Grey
}
}()};
str.append(FormatLogMessage(entry));
str.append(ESC "[0m\n");
}();
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());
#undef ESC
#endif
fwrite(str.c_str(), 1, str.size(), stderr);
}
void PrintColoredMessage(const Entry& entry) {
void PrintColoredMessage(const Entry& entry) noexcept {
#ifdef _WIN32
HANDLE console_handle = GetStdHandle(STD_ERROR_HANDLE);
if (console_handle == INVALID_HANDLE_VALUE)
@@ -84,7 +90,7 @@ void PrintColoredMessage(const Entry& entry) {
#endif
}
void PrintMessageToLogcat(const Entry& entry) {
void PrintMessageToLogcat(const Entry& entry) noexcept {
#ifdef ANDROID
android_LogPriority android_log_priority = [&]() {
switch (entry.log_level) {
+6 -5
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -10,11 +13,9 @@ namespace Common::Log {
struct Entry;
/// Formats a log entry into the provided text buffer.
std::string FormatLogMessage(const Entry& entry);
/// Formats and prints a log entry to stderr.
void PrintMessage(const Entry& entry);
std::string FormatLogMessage(const Entry& entry) noexcept;
/// Prints the same message as `PrintMessage`, but colored according to the severity level.
void PrintColoredMessage(const Entry& entry);
void PrintColoredMessage(const Entry& entry) noexcept;
/// Formats and prints a log entry to the android logcat.
void PrintMessageToLogcat(const Entry& entry);
void PrintMessageToLogcat(const Entry& entry) noexcept;
} // namespace Common::Log
+32 -34
View File
@@ -5,7 +5,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <version>
#include "common/settings_enums.h"
#if __cpp_lib_chrono >= 201907L
#include <chrono>
#include <exception>
@@ -17,8 +16,10 @@
#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"
@@ -110,36 +111,38 @@ std::string GetTimeZoneString(TimeZone time_zone) {
}
void LogSettings() {
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:");
std::deque<std::string> settings_list;
for (auto& [category, settings] : values.linkage.by_category) {
for (const auto& setting : settings) {
if (setting->Id() == values.eden_token.Id()) {
// Hide the token secret, for security reasons.
continue;
// 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()));
}
const auto name = fmt::format(
"{:c}{:c} {}.{}", setting->ToString() == setting->DefaultToString() ? '-' : 'M',
setting->UsingGlobal() ? '-' : 'C', TranslateCategory(category),
setting->GetLabel());
log_setting(name, setting->Canonicalize());
}
}
log_path("DataStorage_CacheDir", Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir));
log_path("DataStorage_ConfigDir", Common::FS::GetEdenPath(Common::FS::EdenPath::ConfigDir));
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));
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
}
bool getDebugKnobAt(u8 i) {
@@ -171,12 +174,10 @@ 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);
}
if (values.cpu_accuracy.GetValue() == CpuAccuracy::Unsafe) {
else 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
@@ -341,10 +342,7 @@ void TranslateResolutionInfo(ResolutionSetup setup, ResolutionScalingInfo& info)
info.down_shift = 0;
break;
default:
ASSERT(false);
info.up_scale = 1;
info.down_shift = 0;
break;
UNREACHABLE();
}
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
View File
@@ -715,6 +715,7 @@ 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;
+1
View File
@@ -155,6 +155,7 @@ 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
@@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#ifdef _MSC_VER
#include <intrin.h>
#endif
#include "common/x64/cpu_detect.h"
#include "common/x64/cpu_wait.h"
#include "common/x64/rdtsc.h"
namespace Common::X64 {
namespace {
// 100,000 cycles is a reasonable amount of time to wait to save on CPU resources.
// For reference:
// At 1 GHz, 100K cycles is 100us
// At 2 GHz, 100K cycles is 50us
// At 4 GHz, 100K cycles is 25us
constexpr auto PauseCycles = 100'000U;
} // Anonymous namespace
#if defined(_MSC_VER) && !defined(__clang__)
__forceinline static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
_tpause(RequestC02State, FencedRDTSC() + PauseCycles);
}
__forceinline static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
_mm_monitorx(&monitor_var, 0, 0);
_mm_mwaitx(EnableWaitTimeFlag, RequestC1State, PauseCycles);
}
#else
static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
const auto tsc = FencedRDTSC() + PauseCycles;
const auto eax = static_cast<u32>(tsc & 0xFFFFFFFF);
const auto edx = static_cast<u32>(tsc >> 32);
asm volatile("tpause %0" : : "r"(RequestC02State), "d"(edx), "a"(eax));
}
static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
asm volatile("monitorx" : : "a"(&monitor_var), "c"(0), "d"(0));
asm volatile("mwaitx" : : "a"(RequestC1State), "b"(PauseCycles), "c"(EnableWaitTimeFlag));
}
#endif
void MicroSleep() {
static const bool has_waitpkg = GetCPUCaps().waitpkg;
static const bool has_monitorx = GetCPUCaps().monitorx;
if (has_waitpkg) {
TPAUSE();
} else if (has_monitorx) {
MWAITX();
} else {
std::this_thread::yield();
}
}
} // namespace Common::X64
-10
View File
@@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Common::X64 {
void MicroSleep();
} // namespace Common::X64
+326 -153
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <numeric>
@@ -20,9 +20,13 @@ 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),
@@ -42,20 +46,25 @@ using NativeExecutionParameters = Kernel::KThread::NativeExecutionParameters;
constexpr size_t MaxRelativeBranch = 128_MiB;
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
Patcher::Patcher() : c(m_patch_instructions) {
Patcher::Patcher() : c(m_patch_instructions), c_pre(m_patch_instructions_pre) {
LOG_WARNING(Core_ARM, "Patcher initialized with LRU cache {}",
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;
@@ -64,7 +73,16 @@ 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();
if (total_program_size + image_size > MaxRelativeBranch && total_program_size > 0) {
// Check if we need split mode for large modules. A64 max takes 128MB
// tests showed that, with update, some are larger. (In this case 208MB)
bool use_split = false;
if (image_size > MaxRelativeBranch) {
if (total_program_size > 0) {
return false;
}
use_split = true;
} else if (total_program_size + image_size > MaxRelativeBranch && total_program_size > 0) {
return false;
}
@@ -74,7 +92,12 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// The first word of the patch section is always a branch to the first instruction of the
// module.
curr_patch->m_branch_to_module_relocations.push_back({0, 0});
if (use_split) {
// curr_patch->m_branch_to_module_relocations.push_back({0, 0});
curr_patch->m_branch_to_module_relocations_pre.push_back({0, 0});
} else {
curr_patch->m_branch_to_module_relocations.push_back({0, 0});
}
// Retrieve text segment data.
const auto text = std::span{program_image}.subspan(code.offset, code.size);
@@ -85,12 +108,18 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
for (u32 i = ModuleCodeIndex; i < static_cast<u32>(text_words.size()); i++) {
const u32 inst = text_words[i];
const auto AddRelocations = [&] {
const auto AddRelocations = [&](bool& pre_buffer) {
const uintptr_t this_offset = i * sizeof(u32);
const uintptr_t next_offset = this_offset + sizeof(u32);
// Relocate from here to patch.
this->BranchToPatch(this_offset);
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 patch to next instruction.
return next_offset;
@@ -98,7 +127,13 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// SVC
if (auto svc = SVC{inst}; svc.Verify()) {
WriteSvcTrampoline(AddRelocations(), svc.GetValue());
bool pre_buffer = false;
auto ret = AddRelocations(pre_buffer);
if (pre_buffer) {
WriteSvcTrampoline(ret, svc.GetValue(), c_pre, m_save_context_pre, m_load_context_pre);
} else {
WriteSvcTrampoline(ret, svc.GetValue(), c, m_save_context, m_load_context);
}
continue;
}
@@ -109,13 +144,25 @@ 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())};
WriteMrsHandler(AddRelocations(), dest_reg, src_reg);
bool pre_buffer = false;
auto ret = AddRelocations(pre_buffer);
if (pre_buffer) {
WriteMrsHandler(ret, dest_reg, src_reg, c_pre);
} else {
WriteMrsHandler(ret, dest_reg, src_reg, c);
}
continue;
}
// MRS Xn, CNTPCT_EL0
if (auto mrs = MRS{inst}; mrs.Verify() && mrs.GetSystemReg() == CntpctEl0) {
WriteCntpctHandler(AddRelocations(), oaknut::XReg{static_cast<int>(mrs.GetRt())});
bool pre_buffer = false;
auto ret = AddRelocations(pre_buffer);
if (pre_buffer) {
WriteCntpctHandler(ret, oaknut::XReg{static_cast<int>(mrs.GetRt())}, c_pre);
} else {
WriteCntpctHandler(ret, oaknut::XReg{static_cast<int>(mrs.GetRt())}, c);
}
continue;
}
@@ -126,7 +173,13 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// MSR TPIDR_EL0, Xn
if (auto msr = MSR{inst}; msr.Verify() && msr.GetSystemReg() == TpidrEl0) {
WriteMsrHandler(AddRelocations(), oaknut::XReg{static_cast<int>(msr.GetRt())});
bool pre_buffer = false;
auto ret = AddRelocations(pre_buffer);
if (pre_buffer) {
WriteMsrHandler(ret, oaknut::XReg{static_cast<int>(msr.GetRt())}, c_pre);
} else {
WriteMsrHandler(ret, oaknut::XReg{static_cast<int>(msr.GetRt())}, c);
}
continue;
}
@@ -137,7 +190,11 @@ bool Patcher::PatchText(const Kernel::PhysicalMemory& program_image,
// Determine patching mode for the final relocation step
total_program_size += image_size;
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
if (use_split) {
this->mode = PatchMode::Split;
} else {
this->mode = image_size > MaxRelativeBranch ? PatchMode::PreText : PatchMode::PostData;
}
return true;
}
@@ -146,7 +203,9 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
Kernel::PhysicalMemory& program_image,
EntryTrampolines* out_trampolines) {
const size_t patch_size = GetSectionSize();
const size_t image_size = program_image.size();
const size_t pre_patch_size = GetPreSectionSize();
const size_t image_size = (mode == PatchMode::Split) ? program_image.size() - pre_patch_size : program_image.size();
// Retrieve text segment data.
const auto text = std::span{program_image}.subspan(code.offset, code.size);
@@ -162,6 +221,16 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
}
};
const auto ApplyBranchToPatchRelocationPre = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
rc.B(static_cast<ptrdiff_t>(rel.patch_offset) - static_cast<ptrdiff_t>(pre_patch_size) - static_cast<ptrdiff_t>(rel.module_offset));
};
const auto ApplyBranchToPatchRelocationPostSplit = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
rc.B(static_cast<ptrdiff_t>(image_size) + static_cast<ptrdiff_t>(rel.patch_offset) - static_cast<ptrdiff_t>(rel.module_offset));
};
const auto ApplyBranchToModuleRelocation = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
if (mode == PatchMode::PreText) {
@@ -171,6 +240,16 @@ bool Patcher::RelocateAndCopy(Common::ProcessAddress load_base,
}
};
const auto ApplyBranchToModuleRelocationPre = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
rc.B(static_cast<ptrdiff_t>(pre_patch_size) + static_cast<ptrdiff_t>(rel.module_offset) - static_cast<ptrdiff_t>(rel.patch_offset));
};
const auto ApplyBranchToModuleRelocationPostSplit = [&](u32* target, const Relocation& rel) {
oaknut::CodeGenerator rc{target};
rc.B(static_cast<ptrdiff_t>(rel.module_offset) - static_cast<ptrdiff_t>(image_size) - static_cast<ptrdiff_t>(rel.patch_offset));
};
const auto RebasePatch = [&](ptrdiff_t patch_offset) {
if (mode == PatchMode::PreText) {
return GetInteger(load_base) + patch_offset;
@@ -182,28 +261,87 @@ 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++];
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 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));
}
}
// 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)});
if (mode != PatchMode::Split) {
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.
@@ -223,6 +361,15 @@ 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(),
@@ -238,202 +385,225 @@ size_t Patcher::GetSectionSize() const noexcept {
return Common::AlignUp(m_patch_instructions.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
}
void Patcher::WriteLoadContext() {
size_t Patcher::GetPreSectionSize() const noexcept {
return Common::AlignUp(m_patch_instructions_pre.size() * sizeof(u32), Core::Memory::YUZU_PAGESIZE);
}
void Patcher::WriteLoadContext(oaknut::VectorCodeGenerator& cg) {
// This function was called, which modifies X30, so use that as a scratch register.
// SP contains the guest X30, so save our return X30 to SP + 8, since we have allocated 16 bytes
// of stack.
c.STR(X30, SP, 8);
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
cg.STR(X30, SP, 8);
cg.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
// Load system registers.
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);
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);
// Load all vector registers.
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
for (int i = 0; i <= 30; i += 2) {
c.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
cg.LDP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
}
// Load all general-purpose registers except X30.
for (int i = 0; i <= 28; i += 2) {
c.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
cg.LDP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
}
// Reload our return X30 from the stack and return.
// The patch code will reload the guest X30 for us.
c.LDR(X30, SP, 8);
c.RET();
cg.LDR(X30, SP, 8);
cg.RET();
}
void Patcher::WriteSaveContext() {
void Patcher::WriteSaveContext(oaknut::VectorCodeGenerator& cg) {
// This function was called, which modifies X30, so use that as a scratch register.
// SP contains the guest X30, so save our X30 to SP + 8, since we have allocated 16 bytes of
// stack.
c.STR(X30, SP, 8);
c.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
c.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
cg.STR(X30, SP, 8);
cg.MRS(X30, oaknut::SystemReg::TPIDR_EL0);
cg.LDR(X30, X30, offsetof(NativeExecutionParameters, native_context));
// Store all general-purpose registers except X30.
for (int i = 0; i <= 28; i += 2) {
c.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
cg.STP(oaknut::XReg{i}, oaknut::XReg{i + 1}, X30, 8 * i);
}
// Store all vector registers.
static constexpr size_t VEC_OFF = offsetof(GuestContext, vector_registers);
for (int i = 0; i <= 30; i += 2) {
c.STP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
cg.STP(oaknut::QReg{i}, oaknut::QReg{i + 1}, X30, VEC_OFF + 16 * i);
}
// Store guest system registers, X30 and SP, using X0 as a scratch register.
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);
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);
// Reload our return X30 from the stack, and return.
c.LDR(X30, SP, 8);
c.RET();
cg.LDR(X30, SP, 8);
cg.RET();
}
void Patcher::WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id) {
void Patcher::WriteSvcTrampoline(ModuleDestLabel module_dest, u32 svc_id, oaknut::VectorCodeGenerator& cg, oaknut::Label& save_ctx, oaknut::Label& load_ctx) {
// Determine if we're writing to the pre-patch buffer
const bool is_pre = (&cg == &c_pre);
// We are about to start saving state, so we need to lock the context.
this->LockContext();
this->LockContext(cg);
// 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.
c.STR(X30, SP, PRE_INDEXED, -16);
c.BL(m_save_context);
c.LDR(X30, SP, POST_INDEXED, 16);
cg.STR(X30, SP, PRE_INDEXED, -16);
cg.BL(save_ctx);
cg.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;
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));
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));
// Store SVC number to execute when we return
c.MOV(X2, svc_id);
c.STR(W2, X1, offsetof(GuestContext, svc));
cg.MOV(X2, svc_id);
cg.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;
c.ADD(X2, X1, offsetof(GuestContext, esr_el1));
c.l(retry);
c.LDAXR(X0, X2);
c.STLXR(W3, XZR, X2);
c.CBNZ(W3, 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);
// Add "calling SVC" flag. Since this is X0, this is now our return value.
c.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
cg.ORR(X0, X0, static_cast<u64>(HaltReason::SupervisorCall));
// Offset the GuestContext pointer to the HostContext member.
// 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.
c.ADD(X1, X1, offsetof(GuestContext, host_ctx));
cg.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));
c.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
c.MOV(SP, X2);
c.MSR(oaknut::SystemReg::TPIDR_EL0, X3);
cg.LDP(X2, X3, X1, offsetof(HostContext, host_sp));
cg.MOV(SP, X2);
cg.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);
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();
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();
// Write the post-SVC trampoline address, which will jump back to the guest after restoring its
// state.
curr_patch->m_trampolines.push_back({c.offset(), module_dest});
if (is_pre) {
curr_patch->m_trampolines_pre.push_back({cg.offset(), module_dest});
} else {
curr_patch->m_trampolines.push_back({cg.offset(), module_dest});
}
// Host called this location. Save the return address so we can
// unwind the stack properly when jumping back.
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));
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));
// Reload all guest registers except X30 and PC.
// The function also expects 16 bytes of stack already allocated.
c.STR(X30, SP, PRE_INDEXED, -16);
c.BL(m_load_context);
c.LDR(X30, SP, POST_INDEXED, 16);
cg.STR(X30, SP, PRE_INDEXED, -16);
cg.BL(load_ctx);
cg.LDR(X30, SP, POST_INDEXED, 16);
// Use X1 as a scratch register to restore X30.
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);
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);
// Unlock the context.
this->UnlockContext();
this->UnlockContext(cg);
// Jump back to the instruction after the emulated SVC.
this->BranchToModule(module_dest);
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
// Store PC after call.
c.l(pc_after_svc);
this->WriteModulePc(module_dest);
cg.l(pc_after_svc);
if (&cg == &c_pre)
this->WriteModulePcPre(module_dest);
else
this->WriteModulePc(module_dest);
}
void Patcher::WriteMrsHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg,
oaknut::SystemReg src_reg) {
oaknut::SystemReg src_reg, oaknut::VectorCodeGenerator& cg) {
// Retrieve emulated TLS register from GuestContext.
c.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
cg.MRS(dest_reg, oaknut::SystemReg::TPIDR_EL0);
if (src_reg == oaknut::SystemReg::TPIDRRO_EL0) {
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
cg.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidrro_el0));
} else {
c.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
cg.LDR(dest_reg, dest_reg, offsetof(NativeExecutionParameters, tpidr_el0));
}
// Jump back to the instruction after the emulated MRS.
this->BranchToModule(module_dest);
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
}
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg) {
void Patcher::WriteMsrHandler(ModuleDestLabel module_dest, oaknut::XReg src_reg, oaknut::VectorCodeGenerator& cg) {
const auto scratch_reg = src_reg.index() == 0 ? X1 : X0;
c.STR(scratch_reg, SP, PRE_INDEXED, -16);
cg.STR(scratch_reg, SP, PRE_INDEXED, -16);
// Save guest value to NativeExecutionParameters::tpidr_el0.
c.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
c.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
cg.MRS(scratch_reg, oaknut::SystemReg::TPIDR_EL0);
cg.STR(src_reg, scratch_reg, offsetof(NativeExecutionParameters, tpidr_el0));
// Restore scratch register.
c.LDR(scratch_reg, SP, POST_INDEXED, 16);
cg.LDR(scratch_reg, SP, POST_INDEXED, 16);
// Jump back to the instruction after the emulated MSR.
this->BranchToModule(module_dest);
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
}
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg) {
void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg, oaknut::VectorCodeGenerator& cg) {
static Common::Arm64::NativeClock clock{};
const auto factor = clock.GetGuestCNTFRQFactor();
const auto raw_factor = std::bit_cast<std::array<u64, 2>>(factor);
@@ -446,80 +616,83 @@ void Patcher::WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_
oaknut::Label factorhi;
// Save scratches.
c.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
cg.STP(scratch0, scratch1, SP, PRE_INDEXED, -16);
// Load counter value.
c.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
cg.MRS(dest_reg, oaknut::SystemReg::CNTVCT_EL0);
// Load scaling factor.
c.LDR(scratch0, factorlo);
c.LDR(scratch1, factorhi);
cg.LDR(scratch0, factorlo);
cg.LDR(scratch1, factorhi);
// Multiply low bits and get result.
c.UMULH(scratch0, dest_reg, scratch0);
cg.UMULH(scratch0, dest_reg, scratch0);
// Multiply high bits and add low bit result.
c.MADD(dest_reg, dest_reg, scratch1, scratch0);
cg.MADD(dest_reg, dest_reg, scratch1, scratch0);
// Reload scratches.
c.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
cg.LDP(scratch0, scratch1, SP, POST_INDEXED, 16);
// Jump back to the instruction after the emulated MRS.
this->BranchToModule(module_dest);
if (&cg == &c_pre)
this->BranchToModulePre(module_dest);
else
this->BranchToModule(module_dest);
// Scaling factor constant values.
c.l(factorlo);
c.dx(raw_factor[0]);
c.l(factorhi);
c.dx(raw_factor[1]);
cg.l(factorlo);
cg.dx(raw_factor[0]);
cg.l(factorhi);
cg.dx(raw_factor[1]);
}
void Patcher::LockContext() {
void Patcher::LockContext(oaknut::VectorCodeGenerator& cg) {
oaknut::Label retry;
// Save scratches.
c.STP(X0, X1, SP, PRE_INDEXED, -16);
cg.STP(X0, X1, SP, PRE_INDEXED, -16);
// Reload lock pointer.
c.l(retry);
c.CLREX();
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
cg.l(retry);
cg.CLREX();
cg.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
cg.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
static_assert(SpinLockLocked == 0);
// Load-linked with acquire ordering.
c.LDAXR(W1, X0);
cg.LDAXR(W1, X0);
// If the value was SpinLockLocked, clear monitor and retry.
c.CBZ(W1, retry);
cg.CBZ(W1, retry);
// Store-conditional SpinLockLocked with relaxed ordering.
c.STXR(W1, WZR, X0);
cg.STXR(W1, WZR, X0);
// If we failed to store, retry.
c.CBNZ(W1, retry);
cg.CBNZ(W1, retry);
// We succeeded! Reload scratches.
c.LDP(X0, X1, SP, POST_INDEXED, 16);
cg.LDP(X0, X1, SP, POST_INDEXED, 16);
}
void Patcher::UnlockContext() {
void Patcher::UnlockContext(oaknut::VectorCodeGenerator& cg) {
// Save scratches.
c.STP(X0, X1, SP, PRE_INDEXED, -16);
cg.STP(X0, X1, SP, PRE_INDEXED, -16);
// Load lock pointer.
c.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
c.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
cg.MRS(X0, oaknut::SystemReg::TPIDR_EL0);
cg.ADD(X0, X0, offsetof(NativeExecutionParameters, lock));
// Load SpinLockUnlocked.
c.MOV(W1, SpinLockUnlocked);
cg.MOV(W1, SpinLockUnlocked);
// Store value with release ordering.
c.STLR(W1, X0);
cg.STLR(W1, X0);
// Load scratches.
c.LDP(X0, X1, SP, POST_INDEXED, 16);
cg.LDP(X0, X1, SP, POST_INDEXED, 16);
}
} // namespace Core::NCE
+44 -10
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -41,6 +41,7 @@ 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;
@@ -63,6 +64,7 @@ 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;
@@ -76,15 +78,25 @@ private:
uintptr_t module_offset;
};
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);
// 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); }
private:
static constexpr size_t CACHE_SIZE = 16384; // Cache size for patch entries
@@ -111,19 +123,34 @@ private:
}
}
void BranchToPatchPre(uintptr_t module_dest) {
curr_patch->m_branch_to_pre_patch_relocations.push_back({c_pre.offset(), module_dest});
}
void BranchToModule(uintptr_t module_dest) {
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 {
@@ -133,15 +160,22 @@ 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{};
+3 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <array>
@@ -142,6 +142,7 @@ 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);
@@ -414,6 +415,7 @@ struct System::Impl {
cpu_manager.Shutdown();
debugger.reset();
kernel.Shutdown();
device_memory.reset();
stop_event = {};
Network::RestartSocketOperations();
+2 -27
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -13,10 +13,6 @@
#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"
@@ -287,28 +283,7 @@ 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();
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
}
event.WaitFor(std::chrono::nanoseconds(wait_time));
} else {
// Queue is empty, wait until another event is scheduled and signals us to
// continue.
+411 -43
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -137,12 +137,127 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
return exefs;
const auto& disabled = Settings::values.disabled_addons[title_id];
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
bool update_disabled = true;
std::optional<u32> enabled_version;
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
const auto update_tid = GetUpdateTitleID(title_id);
if (content_union) {
// First, check ExternalContentProvider
const auto* external_provider = content_union->GetExternalProvider();
if (external_provider) {
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
if (!update_versions.empty()) {
checked_external = true;
for (const auto& update_entry : update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false;
enabled_version = update_entry.version;
break;
}
}
}
}
// Also check ManualContentProvider (for Android)
if (!checked_external) {
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
if (!manual_update_versions.empty()) {
checked_manual = true;
for (const auto& update_entry : manual_update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false;
enabled_version = update_entry.version;
break;
}
}
}
}
}
}
// check for original NAND style
// Check NAND if: no external updates exist, OR all external updates are disabled
if (!checked_external && !checked_manual) {
// Only enable NAND update if it exists AND is not disabled
// We need to check if an update actually exists in the content provider
const bool has_nand_update = content_provider.HasEntry(update_tid, ContentRecordType::Program);
if (has_nand_update) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
update_disabled = false;
}
}
} else if (update_disabled && content_union) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
if (!nand_disabled || !sdmc_disabled) {
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
std::nullopt, TitleType::Update, ContentRecordType::Program, update_tid);
for (const auto& [slot, entry] : nand_sdmc_entries) {
if (slot == ContentProviderUnionSlot::UserNAND ||
slot == ContentProviderUnionSlot::SysNAND) {
if (!nand_disabled) {
update_disabled = false;
break;
}
} else if (slot == ContentProviderUnionSlot::SDMC) {
if (!sdmc_disabled) {
update_disabled = false;
break;
}
}
}
}
}
// Game Updates
const auto update_tid = GetUpdateTitleID(title_id);
const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
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);
}
if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
@@ -447,21 +562,103 @@ 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];
const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
bool update_disabled = true;
std::optional<u32> enabled_version;
VirtualFile update_raw = nullptr;
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider
const auto* external_provider = content_union->GetExternalProvider();
if (external_provider) {
const auto update_versions = external_provider->ListUpdateVersions(update_tid);
if (!update_versions.empty()) {
checked_external = true;
for (const auto& update_entry : update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false;
enabled_version = update_entry.version;
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
break;
}
}
}
}
if (!checked_external) {
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
if (!manual_update_versions.empty()) {
checked_manual = true;
for (const auto& update_entry : manual_update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false;
enabled_version = update_entry.version;
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
break;
}
}
}
}
}
}
if (!checked_external && !checked_manual) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
const bool generic_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
if (!nand_disabled && !sdmc_disabled && !generic_disabled) {
update_disabled = false;
}
if (!update_disabled) {
update_raw = content_provider.GetEntryRaw(update_tid, type);
}
} else if (update_disabled && content_union) {
const bool nand_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (NAND)") != disabled.cend();
const bool sdmc_disabled = std::find(disabled.cbegin(), disabled.cend(), "Update (SDMC)") != disabled.cend();
if (!nand_disabled || !sdmc_disabled) {
const auto nand_sdmc_entries = content_union->ListEntriesFilterOrigin(
std::nullopt, TitleType::Update, type, update_tid);
for (const auto& [slot, entry] : nand_sdmc_entries) {
if (slot == ContentProviderUnionSlot::UserNAND ||
slot == ContentProviderUnionSlot::SysNAND) {
if (!nand_disabled) {
update_disabled = false;
update_raw = content_provider.GetEntryRaw(update_tid, type);
break;
}
} else if (slot == ContentProviderUnionSlot::SDMC) {
if (!sdmc_disabled) {
update_disabled = false;
update_raw = content_provider.GetEntryRaw(update_tid, type);
break;
}
}
}
}
}
if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
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);
@@ -490,34 +687,179 @@ 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;
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};
std::vector<Patch> external_update_patches;
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();
}
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);
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);
}
} else {
PatchManager update{update_tid, fs_controller, content_provider};
const auto metadata = update.GetControlMetadata();
const auto& nacp = metadata.first;
bool update_disabled =
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
Patch update_patch = {.enabled = !update_disabled,
.name = "Update",
.version = "",
.type = PatchType::Update,
.program_id = title_id,
.title_id = title_id,
.source = PatchSource::Unknown,
.numeric_version = 0};
if (nacp != nullptr) {
update_patch.version = nacp->GetVersionString();
out.push_back(update_patch);
} else {
if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
const auto meta_ver = content_provider.GetEntryVersion(update_tid);
if (meta_ver.value_or(0) == 0) {
out.push_back(update_patch);
} else {
update_patch.version = FormatTitleVersion(*meta_ver);
update_patch.numeric_version = *meta_ver;
out.push_back(update_patch);
}
} else if (update_raw != nullptr) {
update_patch.version = "PACKED";
update_patch.source = PatchSource::Packed;
out.push_back(update_patch);
}
} else if (update_raw != nullptr) {
update_patch.version = "PACKED";
out.push_back(update_patch);
}
}
@@ -533,7 +875,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = "Cheats",
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id
.title_id = title_id,
.source = PatchSource::Unknown
});
}
@@ -554,7 +897,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
} else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
file->GetName()) != EXEFS_FILE_NAMES.end()) {
layeredfs = true;
}
}
}
if (ips)
@@ -579,7 +922,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id});
.title_id = title_id,
.source = PatchSource::Unknown});
}
}
@@ -593,7 +937,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs")) ||
IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfslite"))) {
AppendCommaIfNotEmpty(types, "LayeredFS");
}
}
if (!types.empty()) {
const auto mod_disabled =
@@ -603,21 +947,44 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = types,
.type = PatchType::Mod,
.program_id = title_id,
.title_id = title_id});
.title_id = title_id,
.source = PatchSource::Unknown});
}
}
// 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) {
return GetBaseTitleID(entry.title_id) == title_id &&
content_provider.GetEntry(entry)->GetStatus() ==
Loader::ResultStatus::Success;
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;
});
if (!dlc_match.empty()) {
// Ensure sorted so DLC IDs show in order.
std::sort(dlc_match.begin(), dlc_match.end());
@@ -635,7 +1002,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
.version = std::move(list),
.type = PatchType::DLC,
.program_id = title_id,
.title_id = dlc_match.back().title_id});
.title_id = dlc_match.back().title_id,
.source = PatchSource::Unknown});
}
return out;
+12
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -28,6 +31,13 @@ class NACP;
enum class PatchType { Update, DLC, Mod };
enum class PatchSource {
Unknown,
NAND,
External,
Packed,
};
struct Patch {
bool enabled;
std::string name;
@@ -35,6 +45,8 @@ struct Patch {
PatchType type;
u64 program_id;
u64 title_id;
PatchSource source;
u32 numeric_version{0};
};
// A centralized class to manage patches to games.
+518 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -13,12 +13,15 @@
#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"
@@ -974,6 +977,22 @@ 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,
@@ -981,8 +1000,51 @@ 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() {}
@@ -1036,4 +1098,459 @@ 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
+56 -1
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -14,7 +17,8 @@
#include "core/file_sys/vfs/vfs.h"
namespace FileSys {
class CNMT;
class ExternalContentProvider;
class CNMT;
class NCA;
class NSP;
class XCI;
@@ -48,6 +52,13 @@ 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;
}
@@ -208,6 +219,7 @@ 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.
@@ -228,6 +240,9 @@ 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 = {},
@@ -246,6 +261,8 @@ 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;
@@ -258,8 +275,46 @@ 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,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -275,6 +278,14 @@ void NSP::ReadNCAs(const std::vector<VirtualFile>& files) {
ncas[next_nca->GetTitleId()][{cnmt.GetType(), rec.type}] =
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,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -83,6 +86,14 @@ 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.
@@ -93,6 +104,7 @@ struct CodeSet final {
#ifdef HAS_NCE
Segment patch_segment;
Segment post_patch_segment;
#endif
/// The entry point address for this code set.
+10 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -1258,6 +1258,7 @@ 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();
@@ -1265,7 +1266,15 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 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, nullptr, "SetGpuTimeSliceBoost"}, //19.0.0+
{320, D<&IAppletCommonFunctions::SetGpuTimeSliceBoost>, "SetGpuTimeSliceBoost"}, //19.0.0+
{321, nullptr, "SetGpuTimeSliceBoostDueToApplication"}, //19.0.0+
{350, D<&IAppletCommonFunctions::Unknown350>, "Unknown350"} //20.0.0+
};
@@ -82,6 +82,11 @@ 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 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
@@ -24,6 +24,7 @@ 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,7 +179,14 @@ Result ILibraryAppletSelfAccessor::GetMainAppletStorageId(Out<FileSys::StorageId
Result ILibraryAppletSelfAccessor::ExitProcessAndReturn() {
LOG_INFO(Service_AM, "called");
m_applet->process->Terminate();
if (const auto caller_applet = m_applet->caller_applet.lock(); caller_applet) {
m_applet->process->Terminate();
} else {
system.GetUserChannel() = m_applet->user_channel_launch_parameter;
system.ExecuteProgram(0);
}
R_SUCCEED();
}
+37 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -9,6 +9,7 @@
#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"
@@ -507,6 +508,10 @@ 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");
@@ -684,6 +689,7 @@ void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool ove
if (overwrite) {
bis_factory = nullptr;
sdmc_factory = nullptr;
external_provider = nullptr;
}
using EdenPath = Common::FS::EdenPath;
@@ -716,6 +722,36 @@ 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,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -17,6 +20,7 @@ class System;
namespace FileSys {
class BISFactory;
class ExternalContentProvider;
class NCA;
class RegisteredCache;
class RegisteredCacheUnion;
@@ -117,6 +121,8 @@ 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);
@@ -138,6 +144,8 @@ 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;
+32 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -102,6 +102,8 @@ 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
@@ -178,12 +180,26 @@ 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();
patch_segment.addr =
patch->GetPatchMode() == Core::NCE::PatchMode::PreText ? 0 : image_size;
patch_segment.size = static_cast<u32>(patch->GetSectionSize());
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());
}
}
// Refresh image_size to take account the patch section if it was added by RelocateAndCopy
@@ -193,6 +209,18 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::KProcess& process, Core::
// If we aren't actually loading (i.e. just computing the process code layout), we are done
if (!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,9 +433,10 @@ 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: LRU
std::uniform_int_distribution<size_t> dis{0, candidates.size() - 1};
return candidates[dis(rand_gen)];
// 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()];
}
void RegAlloc::SpillGpr(int index) {
@@ -160,8 +160,12 @@ 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)
: code{code}, fpsr_manager{fpsr_manager}, gpr_order{gpr_order}, fpr_order{fpr_order}, rand_gen{std::random_device{}()} {}
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}
{}
ArgumentInfo GetArgumentInfo(IR::Inst* inst);
bool WasValueDefined(IR::Inst* inst) const;
@@ -331,7 +335,7 @@ private:
HostLocInfo flags;
std::array<HostLocInfo, SpillCount> spills;
mutable std::mt19937 rand_gen;
mutable size_t alloc_candidate_index = 0;
ankerl::unordered_dense::set<const IR::Inst*> defined_insts;
};
+2 -1
View File
@@ -13,7 +13,8 @@ add_library(frontend_common STATIC
data_manager.h data_manager.cpp
play_time_manager.cpp
play_time_manager.h
settings_generator.h settings_generator.cpp)
settings_generator.h settings_generator.cpp
mod_manager.h mod_manager.cpp)
if (ENABLE_UPDATE_CHECKER)
target_link_libraries(frontend_common PRIVATE httplib::httplib)
+62
View File
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include <filesystem>
#include <fmt/format.h>
#include "common/fs/fs.h"
#include "common/fs/fs_types.h"
#include "common/logging/backend.h"
#include "frontend_common/data_manager.h"
#include "mod_manager.h"
namespace FrontendCommon {
// TODO: Handle cases where the folder appears to contain multiple mods.
std::vector<std::filesystem::path> GetModFolder(const std::string& root) {
std::vector<std::filesystem::path> paths;
auto callback = [&paths](const std::filesystem::directory_entry& entry) -> bool {
const auto name = entry.path().filename().string();
static const std::array<std::string, 5> valid_names = {"exefs",
"romfs"
"romfs_ext",
"cheats", "romfslite"};
if (std::ranges::find(valid_names, name) != valid_names.end()) {
paths.emplace_back(entry.path().parent_path());
}
return true;
};
Common::FS::IterateDirEntriesRecursively(root, callback, Common::FS::DirEntryFilter::Directory);
return paths;
}
ModInstallResult InstallMod(const std::filesystem::path& path, const u64 program_id, const bool copy) {
const auto program_id_string = fmt::format("{:016X}", program_id);
const auto mod_name = path.filename();
const auto mod_dir =
DataManager::GetDataDir(DataManager::DataDir::Mods) / program_id_string / mod_name;
// pre-emptively remove any existing mod here
std::filesystem::remove_all(mod_dir);
// now copy
try {
std::filesystem::copy(path, mod_dir, std::filesystem::copy_options::recursive);
if (!copy)
std::filesystem::remove_all(path);
} catch (std::exception& e) {
LOG_ERROR(Frontend, "Mod install failed with message {}", e.what());
return Failed;
}
LOG_INFO(Frontend, "Copied mod from {} to {}", path.string(), mod_dir.string());
return Success;
}
} // namespace FrontendCommon
+21
View File
@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <filesystem>
#include <optional>
#include "common/common_types.h"
namespace FrontendCommon {
enum ModInstallResult {
Cancelled,
Failed,
Success,
};
std::vector<std::filesystem::path> GetModFolder(const std::string& root);
ModInstallResult InstallMod(const std::filesystem::path &path, const u64 program_id, const bool copy = true);
}
+3 -3
View File
@@ -22,6 +22,8 @@ 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
@@ -29,9 +31,7 @@ add_library(qt_common STATIC
qt_string_lookup.h
qt_compat.h
discord/discord.h
util/fs.h util/fs.cpp
)
discord/discord.h)
if (UNIX)
target_sources(qt_common PRIVATE gui_settings.cpp gui_settings.h)
+26 -1
View File
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QLineEdit>
#include "frontend.h"
#include "qt_common/qt_common.h"
@@ -8,6 +9,9 @@
#include <QFileDialog>
#endif
#include <QAbstractButton>
#include <QInputDialog>
namespace QtCommon::Frontend {
StandardButton ShowMessage(
@@ -50,4 +54,25 @@ 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
+7 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef FRONTEND_H
@@ -139,5 +139,11 @@ 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
+18 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -231,6 +231,16 @@ 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();
@@ -446,6 +456,13 @@ 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();
}
@@ -766,6 +766,12 @@ 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
+4 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
@@ -210,6 +210,9 @@ 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};
+2 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -98,8 +98,7 @@ 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. "
"Versions 19.0.1 or earlier are recommended, as 20.0.0+ is currently experimental.")},
"Firmware missing. Firmware is required to run certain games and use the Home Menu.")},
{FwCheckErrorFirmwareCorrupted,
QT_TR_NOOP(
"Firmware reported as present, but was unable to be read. Check for decryption keys and "
+6 -10
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "qt_common/util/game.h"
@@ -373,27 +373,23 @@ 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
@@ -0,0 +1,139 @@
// 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
@@ -0,0 +1,16 @@
// 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);
}
@@ -142,15 +142,20 @@ Id GetCbuf(EmitContext& ctx, Id result_type, Id UniformDefinitions::*member_ptr,
const auto is_float = UniformDefinitions::IsFloat(member_ptr);
const auto num_elements = UniformDefinitions::NumElements(member_ptr);
const std::array zero_vec{
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
};
const Id cond = ctx.OpULessThanEqual(ctx.TypeBool(), buffer_offset, ctx.Const(0xFFFFu));
const Id zero = ctx.OpCompositeConstruct(result_type, std::span(zero_vec.data(), num_elements));
return ctx.OpSelect(result_type, cond, val, zero);
if (num_elements > 1) {
const std::array zero_vec{
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
is_float ? ctx.Const(0.0f) : ctx.Const(0u),
};
const Id zero = ctx.OpCompositeConstruct(result_type, std::span(zero_vec.data(), num_elements));
return ctx.OpSelect(result_type, cond, val, zero);
} else {
const Id zero = is_float ? ctx.Const(0.0f) : ctx.Const(0u);
return ctx.OpSelect(result_type, cond, val, zero);
}
}
Id GetCbufU32(EmitContext& ctx, const IR::Value& binding, const IR::Value& offset) {
@@ -11,6 +11,7 @@
#include "shader_recompiler/backend/spirv/emit_spirv_instructions.h"
#include "shader_recompiler/backend/spirv/spirv_emit_context.h"
#include "shader_recompiler/frontend/ir/modifiers.h"
#include "shader_recompiler/shader_info.h"
namespace Shader::Backend::SPIRV {
namespace {
@@ -70,8 +71,7 @@ public:
}
}
explicit ImageOperands(EmitContext& ctx, bool has_lod_clamp, Id derivatives,
u32 num_derivatives, const IR::Value& offset, Id lod_clamp) {
explicit ImageOperands(EmitContext& ctx, bool has_lod_clamp, Id derivatives, u32 num_derivatives, const IR::Value& offset, Id lod_clamp) {
if (!Sirit::ValidId(derivatives)) {
throw LogicError("Derivatives must be present");
}
@@ -81,10 +81,8 @@ public:
deriv_x_accum.push_back(ctx.OpCompositeExtract(ctx.F32[1], derivatives, i * 2));
deriv_y_accum.push_back(ctx.OpCompositeExtract(ctx.F32[1], derivatives, i * 2 + 1));
}
const Id derivatives_X{ctx.OpCompositeConstruct(
ctx.F32[num_derivatives], std::span{deriv_x_accum.data(), deriv_x_accum.size()})};
const Id derivatives_Y{ctx.OpCompositeConstruct(
ctx.F32[num_derivatives], std::span{deriv_y_accum.data(), deriv_y_accum.size()})};
const Id derivatives_X{ctx.OpCompositeConstruct(ctx.F32[num_derivatives], std::span{deriv_x_accum.data(), deriv_x_accum.size()})};
const Id derivatives_Y{ctx.OpCompositeConstruct(ctx.F32[num_derivatives], std::span{deriv_y_accum.data(), deriv_y_accum.size()})};
Add(spv::ImageOperandsMask::Grad, derivatives_X, derivatives_Y);
AddOffset(ctx, offset, ImageGradientOffsetAllowed);
if (has_lod_clamp) {
@@ -197,20 +195,14 @@ Id Texture(EmitContext& ctx, IR::TextureInstInfo info, [[maybe_unused]] const IR
}
Id TextureImage(EmitContext& ctx, IR::TextureInstInfo info, const IR::Value& index) {
if (!index.IsImmediate() || index.U32() != 0) {
throw NotImplementedException("Indirect image indexing");
}
UNIMPLEMENTED_IF_MSG(!index.IsImmediate() || index.U32() != 0, "Indirect image indexing");
if (info.type == TextureType::Buffer) {
const TextureBufferDefinition& def{ctx.texture_buffers.at(info.descriptor_index)};
if (def.count > 1) {
throw NotImplementedException("Indirect texture sample");
}
return ctx.OpLoad(ctx.image_buffer_type, def.id);
UNIMPLEMENTED_IF(def.count > 1);
return ctx.OpLoad(ctx.image_buffer_types[info.descriptor_index], def.id);
} else {
const TextureDefinition& def{ctx.textures.at(info.descriptor_index)};
if (def.count > 1) {
throw NotImplementedException("Indirect texture sample");
}
UNIMPLEMENTED_IF(def.count > 1);
return ctx.OpImage(def.image_type, ctx.OpLoad(def.sampled_type, def.id));
}
}
@@ -533,13 +525,10 @@ Id EmitImageGatherDref(EmitContext& ctx, IR::Inst* inst, const IR::Value& index,
if (ctx.profile.need_gather_subpixel_offset) {
coords = ImageGatherSubpixelOffset(ctx, info, TextureImage(ctx, info, index), coords);
}
return Emit(&EmitContext::OpImageSparseDrefGather, &EmitContext::OpImageDrefGather, ctx, inst,
ctx.F32[4], Texture(ctx, info, index), coords, dref, operands.MaskOptional(),
operands.Span());
return Emit(&EmitContext::OpImageSparseDrefGather, &EmitContext::OpImageDrefGather, ctx, inst, ctx.F32[4], Texture(ctx, info, index), coords, dref, operands.MaskOptional(), operands.Span());
}
Id EmitImageFetch(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords, Id offset,
Id lod, Id ms) {
Id EmitImageFetch(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords, Id offset, Id lod, Id ms) {
const auto info{inst->Flags<IR::TextureInstInfo>()};
AddOffsetToCoordinates(ctx, info, coords, offset);
if (info.type == TextureType::Buffer) {
@@ -549,9 +538,14 @@ Id EmitImageFetch(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id c
// This image is multisampled, lod must be implicit
lod = Id{};
}
const auto is_integer = info.type == TextureType::Buffer
? ctx.texture_buffers.at(info.descriptor_index).is_integer
: ctx.textures.at(info.descriptor_index).is_integer;
const ImageOperands operands(lod, ms);
return Emit(&EmitContext::OpImageSparseFetch, &EmitContext::OpImageFetch, ctx, inst, ctx.F32[4],
TextureImage(ctx, info, index), coords, operands.MaskOptional(), operands.Span());
auto const result = Emit(&EmitContext::OpImageSparseFetch, &EmitContext::OpImageFetch, ctx, inst,
is_integer ? ctx.U32[4] : ctx.F32[4], TextureImage(ctx, info, index), coords, operands.MaskOptional(), operands.Span());
// TODO: Is this even correct?????
return is_integer ? ctx.OpConvertUToF(ctx.F32[4], result) : result;
}
Id EmitImageQueryDimensions(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id lod,
@@ -593,14 +587,11 @@ Id EmitImageQueryLod(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, I
zero, zero);
}
Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords,
Id derivatives, const IR::Value& offset, Id lod_clamp) {
Id EmitImageGradient(EmitContext& ctx, IR::Inst* inst, const IR::Value& index, Id coords, Id derivatives, const IR::Value& offset, Id lod_clamp) {
const auto info{inst->Flags<IR::TextureInstInfo>()};
const auto operands = info.num_derivatives == 3
? ImageOperands(ctx, info.has_lod_clamp != 0, derivatives,
ctx.Def(offset), {}, lod_clamp)
: ImageOperands(ctx, info.has_lod_clamp != 0, derivatives,
info.num_derivatives, offset, lod_clamp);
? ImageOperands(ctx, info.has_lod_clamp != 0, derivatives, ctx.Def(offset), {}, lod_clamp)
: ImageOperands(ctx, info.has_lod_clamp != 0, derivatives, info.num_derivatives, offset, lod_clamp);
return Emit(&EmitContext::OpImageSparseSampleExplicitLod,
&EmitContext::OpImageSampleExplicitLod, ctx, inst, ctx.F32[4],
Texture(ctx, info, index), coords, operands.Mask(), operands.Span());
@@ -30,9 +30,9 @@ enum class Operation {
Id ImageType(EmitContext& ctx, const TextureDescriptor& desc) {
const spv::ImageFormat format{spv::ImageFormat::Unknown};
const Id type{ctx.F32[1]};
const bool depth{desc.is_depth};
const bool ms{desc.is_multisample};
const Id type = desc.is_integer ? ctx.U32[1] : ctx.F32[1];
const bool depth = desc.is_depth;
const bool ms = desc.is_multisample;
switch (desc.type) {
case TextureType::Color1D:
return ctx.TypeImage(type, spv::Dim::Dim1D, depth, false, false, 1, format);
@@ -1304,22 +1304,23 @@ void EmitContext::DefineTextureBuffers(const Info& info, u32& binding) {
if (info.texture_buffer_descriptors.empty()) {
return;
}
const spv::ImageFormat format{spv::ImageFormat::Unknown};
image_buffer_type = TypeImage(F32[1], spv::Dim::Buffer, 0U, false, false, 1, format);
const Id type{TypePointer(spv::StorageClass::UniformConstant, image_buffer_type)};
const spv::ImageFormat format = spv::ImageFormat::Unknown;
texture_buffers.reserve(info.texture_buffer_descriptors.size());
for (const TextureBufferDescriptor& desc : info.texture_buffer_descriptors) {
if (desc.count != 1) {
throw NotImplementedException("Array of texture buffers");
}
const Id id{AddGlobalVariable(type, spv::StorageClass::UniformConstant)};
for (size_t i = 0; i < info.texture_buffer_descriptors.size(); ++i) {
auto const& desc = info.texture_buffer_descriptors[i];
UNIMPLEMENTED_IF(desc.count != 1);
image_buffer_types[i] = TypeImage(desc.is_integer ? U32[1] : F32[1], spv::Dim::Buffer, 0U, false, false, 1, format);
const Id type = TypePointer(spv::StorageClass::UniformConstant, image_buffer_types[i]);
const Id id = AddGlobalVariable(type, spv::StorageClass::UniformConstant);
Decorate(id, spv::Decoration::Binding, binding);
Decorate(id, spv::Decoration::DescriptorSet, 0U);
Name(id, NameOf(stage, desc, "texbuf"));
texture_buffers.push_back({
.id = id,
.count = desc.count,
.is_integer = desc.is_integer
});
if (profile.supported_spirv >= 0x00010400) {
interfaces.push_back(id);
@@ -1374,6 +1375,7 @@ void EmitContext::DefineTextures(const Info& info, u32& binding, u32& scaling_in
.image_type = image_type,
.count = desc.count,
.is_multisample = desc.is_multisample,
.is_integer = desc.is_integer
});
if (profile.supported_spirv >= 0x00010400) {
interfaces.push_back(id);
@@ -41,11 +41,13 @@ struct TextureDefinition {
Id image_type;
u32 count;
bool is_multisample;
bool is_integer;
};
struct TextureBufferDefinition {
Id id;
u32 count;
bool is_integer;
};
struct ImageBufferDefinition {
@@ -248,7 +250,8 @@ public:
Id output_f32{};
Id output_u32{};
Id image_buffer_type{};
// Max TextureBuffers?
std::array<Id, Info::MAX_CBUFS> image_buffer_types{};
Id image_u32{};
std::array<UniformDefinitions, Info::MAX_CBUFS> cbufs{};
+10 -5
View File
@@ -429,7 +429,7 @@ public:
texture_descriptors{texture_descriptors_}, image_descriptors{image_descriptors_} {}
u32 Add(const TextureBufferDescriptor& desc) {
return Add(texture_buffer_descriptors, desc, [&desc](const auto& existing) {
const u32 index = Add(texture_buffer_descriptors, desc, [&desc](const auto& existing) {
return desc.cbuf_index == existing.cbuf_index &&
desc.cbuf_offset == existing.cbuf_offset &&
desc.shift_left == existing.shift_left &&
@@ -439,6 +439,8 @@ public:
desc.count == existing.count && desc.size_shift == existing.size_shift &&
desc.has_secondary == existing.has_secondary;
});
texture_buffer_descriptors[index].is_integer |= desc.is_integer;
return index;
}
u32 Add(const ImageBufferDescriptor& desc) {
@@ -465,7 +467,8 @@ public:
desc.secondary_shift_left == existing.secondary_shift_left &&
desc.count == existing.count && desc.size_shift == existing.size_shift;
})};
// TODO: Read this from TIC
texture_descriptors[index].is_integer |= desc.is_integer;
// TODO: Read this from TIC (the MSAA whatever)
texture_descriptors[index].is_multisample |= desc.is_multisample;
return index;
}
@@ -632,6 +635,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
break;
}
u32 index;
const bool is_integer = IsTexturePixelFormatIntegerCached(env, cbuf);
switch (inst->GetOpcode()) {
case IR::Opcode::ImageRead:
case IR::Opcode::ImageAtomicIAdd32:
@@ -649,9 +653,8 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
if (cbuf.has_secondary) {
throw NotImplementedException("Unexpected separate sampler");
}
const bool is_written{inst->GetOpcode() != IR::Opcode::ImageRead};
const bool is_read{inst->GetOpcode() != IR::Opcode::ImageWrite};
const bool is_integer{IsTexturePixelFormatIntegerCached(env, cbuf)};
const bool is_written = inst->GetOpcode() != IR::Opcode::ImageRead;
const bool is_read = inst->GetOpcode() != IR::Opcode::ImageWrite;
if (flags.type == TextureType::Buffer) {
index = descriptors.Add(ImageBufferDescriptor{
.format = flags.image_format,
@@ -682,6 +685,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
if (flags.type == TextureType::Buffer) {
index = descriptors.Add(TextureBufferDescriptor{
.has_secondary = cbuf.has_secondary,
.is_integer = is_integer,
.cbuf_index = cbuf.index,
.cbuf_offset = cbuf.offset,
.shift_left = cbuf.shift_left,
@@ -696,6 +700,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.type = flags.type,
.is_depth = flags.is_depth != 0,
.is_multisample = is_multisample,
.is_integer = is_integer,
.has_secondary = cbuf.has_secondary,
.cbuf_index = cbuf.index,
.cbuf_offset = cbuf.offset,
+2
View File
@@ -178,6 +178,7 @@ struct StorageBufferDescriptor {
struct TextureBufferDescriptor {
bool has_secondary;
bool is_integer;
u32 cbuf_index;
u32 cbuf_offset;
u32 shift_left;
@@ -209,6 +210,7 @@ struct TextureDescriptor {
TextureType type;
bool is_depth;
bool is_multisample;
bool is_integer;
bool has_secondary;
u32 cbuf_index;
u32 cbuf_offset;
+64 -64
View File
@@ -14,68 +14,68 @@ set(GLSL_INCLUDES
)
set(SHADER_FILES
astc_decoder.comp
blit_color_float.frag
block_linear_unswizzle_2d.comp
block_linear_unswizzle_3d.comp
block_linear_unswizzle_3d_bcn.comp
convert_abgr8_to_d24s8.frag
convert_abgr8_to_d32f.frag
convert_d32f_to_abgr8.frag
convert_d24s8_to_abgr8.frag
convert_depth_to_float.frag
convert_float_to_depth.frag
convert_msaa_to_non_msaa.comp
convert_non_msaa_to_msaa.comp
convert_s8d24_to_abgr8.frag
full_screen_triangle.vert
fxaa.frag
fxaa.vert
opengl_convert_s8d24.comp
opengl_copy_bc4.comp
opengl_fidelityfx_fsr.frag
opengl_fidelityfx_fsr_easu.frag
opengl_fidelityfx_fsr_rcas.frag
opengl_lmem_warmup.comp
opengl_present.frag
opengl_present.vert
opengl_present_scaleforce.frag
opengl_smaa.glsl
pitch_unswizzle.comp
present_area.frag
present_bicubic.frag
present_zero_tangent.frag
present_bspline.frag
present_mitchell.frag
present_gaussian.frag
present_lanczos.frag
present_spline1.frag
present_mmpx.frag
queries_prefix_scan_sum.comp
queries_prefix_scan_sum_nosubgroups.comp
resolve_conditional_render.comp
smaa_edge_detection.vert
smaa_edge_detection.frag
smaa_blending_weight_calculation.vert
smaa_blending_weight_calculation.frag
smaa_neighborhood_blending.vert
smaa_neighborhood_blending.frag
vulkan_blit_depth_stencil.frag
vulkan_color_clear.frag
vulkan_color_clear.vert
vulkan_depthstencil_clear.frag
vulkan_fidelityfx_fsr.vert
vulkan_fidelityfx_fsr_easu_fp16.frag
vulkan_fidelityfx_fsr_easu_fp32.frag
vulkan_fidelityfx_fsr_rcas_fp16.frag
vulkan_fidelityfx_fsr_rcas_fp32.frag
vulkan_present.frag
vulkan_present.vert
vulkan_present_scaleforce_fp16.frag
vulkan_present_scaleforce_fp32.frag
vulkan_quad_indexed.comp
vulkan_turbo_mode.comp
vulkan_uint8.comp
${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag
${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp
${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_3d.comp
${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_3d_bcn.comp
${CMAKE_CURRENT_SOURCE_DIR}/convert_abgr8_to_d24s8.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_abgr8_to_d32f.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_d32f_to_abgr8.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_d24s8_to_abgr8.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_depth_to_float.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_float_to_depth.frag
${CMAKE_CURRENT_SOURCE_DIR}/convert_msaa_to_non_msaa.comp
${CMAKE_CURRENT_SOURCE_DIR}/convert_non_msaa_to_msaa.comp
${CMAKE_CURRENT_SOURCE_DIR}/convert_s8d24_to_abgr8.frag
${CMAKE_CURRENT_SOURCE_DIR}/full_screen_triangle.vert
${CMAKE_CURRENT_SOURCE_DIR}/fxaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/fxaa.vert
${CMAKE_CURRENT_SOURCE_DIR}/opengl_convert_s8d24.comp
${CMAKE_CURRENT_SOURCE_DIR}/opengl_copy_bc4.comp
${CMAKE_CURRENT_SOURCE_DIR}/opengl_fidelityfx_fsr.frag
${CMAKE_CURRENT_SOURCE_DIR}/opengl_fidelityfx_fsr_easu.frag
${CMAKE_CURRENT_SOURCE_DIR}/opengl_fidelityfx_fsr_rcas.frag
${CMAKE_CURRENT_SOURCE_DIR}/opengl_lmem_warmup.comp
${CMAKE_CURRENT_SOURCE_DIR}/opengl_present.frag
${CMAKE_CURRENT_SOURCE_DIR}/opengl_present.vert
${CMAKE_CURRENT_SOURCE_DIR}/opengl_present_scaleforce.frag
${CMAKE_CURRENT_SOURCE_DIR}/opengl_smaa.glsl
${CMAKE_CURRENT_SOURCE_DIR}/pitch_unswizzle.comp
${CMAKE_CURRENT_SOURCE_DIR}/present_area.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_bicubic.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_zero_tangent.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_bspline.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_mitchell.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_gaussian.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_lanczos.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_spline1.frag
${CMAKE_CURRENT_SOURCE_DIR}/present_mmpx.frag
${CMAKE_CURRENT_SOURCE_DIR}/queries_prefix_scan_sum.comp
${CMAKE_CURRENT_SOURCE_DIR}/queries_prefix_scan_sum_nosubgroups.comp
${CMAKE_CURRENT_SOURCE_DIR}/resolve_conditional_render.comp
${CMAKE_CURRENT_SOURCE_DIR}/smaa_edge_detection.vert
${CMAKE_CURRENT_SOURCE_DIR}/smaa_edge_detection.frag
${CMAKE_CURRENT_SOURCE_DIR}/smaa_blending_weight_calculation.vert
${CMAKE_CURRENT_SOURCE_DIR}/smaa_blending_weight_calculation.frag
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.vert
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_blit_depth_stencil.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.vert
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_depthstencil_clear.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_fidelityfx_fsr.vert
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_fidelityfx_fsr_easu_fp16.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_fidelityfx_fsr_easu_fp32.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_fidelityfx_fsr_rcas_fp16.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_fidelityfx_fsr_rcas_fp32.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present.vert
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp16.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_present_scaleforce_fp32.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_quad_indexed.comp
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_turbo_mode.comp
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_uint8.comp
)
if (PLATFORM_HAIKU)
@@ -118,9 +118,9 @@ if (NOT GLSLANG_ERROR STREQUAL "")
set(QUIET_FLAG "")
endif()
foreach(FILENAME IN ITEMS ${SHADER_FILES})
foreach(SOURCE_FILE IN ITEMS ${SHADER_FILES})
get_filename_component(FILENAME ${SOURCE_FILE} NAME)
string(REPLACE "." "_" SHADER_NAME ${FILENAME})
set(SOURCE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/${FILENAME})
# Skip generating source headers on Vulkan exclusive files
if (NOT ${FILENAME} MATCHES "vulkan.*")
set(SOURCE_HEADER_FILE ${SHADER_DIR}/${SHADER_NAME}.h)
@@ -623,7 +623,7 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map,
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.pNext = nullptr,
.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_GENERAL,
.newLayout = VK_IMAGE_LAYOUT_GENERAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
@@ -638,7 +638,7 @@ void ASTCDecoderPass::Assemble(Image& image, const StagingBufferRef& map,
},
};
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, image_barrier);
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, image_barrier);
});
scheduler.Finish();
}
+4 -9
View File
@@ -65,9 +65,7 @@ static Shader::TextureType ConvertTextureType(const Tegra::Texture::TICEntry& en
}
static Shader::TexturePixelFormat ConvertTexturePixelFormat(const Tegra::Texture::TICEntry& entry) {
return static_cast<Shader::TexturePixelFormat>(
PixelFormatFromTextureInfo(entry.format, entry.r_type, entry.g_type, entry.b_type,
entry.a_type, entry.srgb_conversion));
return Shader::TexturePixelFormat(PixelFormatFromTextureInfo(entry.format, entry.r_type, entry.g_type, entry.b_type, entry.a_type, entry.srgb_conversion));
}
static std::string_view StageToPrefix(Shader::Stage stage) {
@@ -388,8 +386,7 @@ Shader::TexturePixelFormat GraphicsEnvironment::ReadTexturePixelFormat(u32 handl
}
bool GraphicsEnvironment::IsTexturePixelFormatInteger(u32 handle) {
return VideoCore::Surface::IsPixelFormatInteger(
static_cast<VideoCore::Surface::PixelFormat>(ReadTexturePixelFormat(handle)));
return VideoCore::Surface::IsPixelFormatInteger(VideoCore::Surface::PixelFormat(ReadTexturePixelFormat(handle)));
}
u32 GraphicsEnvironment::ReadViewportTransformState() {
@@ -443,8 +440,7 @@ Shader::TexturePixelFormat ComputeEnvironment::ReadTexturePixelFormat(u32 handle
}
bool ComputeEnvironment::IsTexturePixelFormatInteger(u32 handle) {
return VideoCore::Surface::IsPixelFormatInteger(
static_cast<VideoCore::Surface::PixelFormat>(ReadTexturePixelFormat(handle)));
return VideoCore::Surface::IsPixelFormatInteger(VideoCore::Surface::PixelFormat(ReadTexturePixelFormat(handle)));
}
u32 ComputeEnvironment::ReadViewportTransformState() {
@@ -551,8 +547,7 @@ Shader::TexturePixelFormat FileEnvironment::ReadTexturePixelFormat(u32 handle) {
}
bool FileEnvironment::IsTexturePixelFormatInteger(u32 handle) {
return VideoCore::Surface::IsPixelFormatInteger(
static_cast<VideoCore::Surface::PixelFormat>(ReadTexturePixelFormat(handle)));
return VideoCore::Surface::IsPixelFormatInteger(VideoCore::Surface::PixelFormat(ReadTexturePixelFormat(handle)));
}
u32 FileEnvironment::ReadViewportTransformState() {
+34 -34
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -242,39 +242,39 @@ SurfaceType GetFormatType(PixelFormat pixel_format) {
bool HasAlpha(PixelFormat pixel_format) {
switch (pixel_format) {
case PixelFormat::A8B8G8R8_UNORM:
case PixelFormat::A8B8G8R8_SNORM:
case PixelFormat::A8B8G8R8_SINT:
case PixelFormat::A8B8G8R8_UINT:
case PixelFormat::A1R5G5B5_UNORM:
case PixelFormat::A2B10G10R10_UNORM:
case PixelFormat::A2B10G10R10_UINT:
case PixelFormat::A2R10G10B10_UNORM:
case PixelFormat::A1B5G5R5_UNORM:
case PixelFormat::A5B5G5R1_UNORM:
case PixelFormat::R16G16B16A16_FLOAT:
case PixelFormat::R16G16B16A16_UNORM:
case PixelFormat::R16G16B16A16_SNORM:
case PixelFormat::R16G16B16A16_SINT:
case PixelFormat::R16G16B16A16_UINT:
case PixelFormat::R32G32B32A32_UINT:
case PixelFormat::BC1_RGBA_UNORM:
case PixelFormat::B8G8R8A8_UNORM:
case PixelFormat::R32G32B32A32_FLOAT:
case PixelFormat::R32G32B32A32_SINT:
case PixelFormat::A8B8G8R8_SRGB:
case PixelFormat::B8G8R8A8_SRGB:
case PixelFormat::BC1_RGBA_SRGB:
case PixelFormat::A4B4G4R4_UNORM:
case PixelFormat::BC2_SRGB:
case PixelFormat::BC2_UNORM:
case PixelFormat::BC3_SRGB:
case PixelFormat::BC3_UNORM:
case PixelFormat::BC7_SRGB:
case PixelFormat::BC7_UNORM:
return true;
default:
return false;
case PixelFormat::A8B8G8R8_UNORM:
case PixelFormat::A8B8G8R8_SNORM:
case PixelFormat::A8B8G8R8_SINT:
case PixelFormat::A8B8G8R8_UINT:
case PixelFormat::A1R5G5B5_UNORM:
case PixelFormat::A2B10G10R10_UNORM:
case PixelFormat::A2B10G10R10_UINT:
case PixelFormat::A2R10G10B10_UNORM:
case PixelFormat::A1B5G5R5_UNORM:
case PixelFormat::A5B5G5R1_UNORM:
case PixelFormat::R16G16B16A16_FLOAT:
case PixelFormat::R16G16B16A16_UNORM:
case PixelFormat::R16G16B16A16_SNORM:
case PixelFormat::R16G16B16A16_SINT:
case PixelFormat::R16G16B16A16_UINT:
case PixelFormat::R32G32B32A32_UINT:
case PixelFormat::BC1_RGBA_UNORM:
case PixelFormat::B8G8R8A8_UNORM:
case PixelFormat::R32G32B32A32_FLOAT:
case PixelFormat::R32G32B32A32_SINT:
case PixelFormat::A8B8G8R8_SRGB:
case PixelFormat::B8G8R8A8_SRGB:
case PixelFormat::BC1_RGBA_SRGB:
case PixelFormat::A4B4G4R4_UNORM:
case PixelFormat::BC2_SRGB:
case PixelFormat::BC2_UNORM:
case PixelFormat::BC3_SRGB:
case PixelFormat::BC3_UNORM:
case PixelFormat::BC7_SRGB:
case PixelFormat::BC7_UNORM:
return true;
default:
return false;
}
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -11,243 +14,124 @@ using Tegra::Texture::ComponentType;
using Tegra::Texture::TextureFormat;
using VideoCore::Surface::PixelFormat;
namespace {
constexpr auto SNORM = ComponentType::SNORM;
constexpr auto UNORM = ComponentType::UNORM;
constexpr auto SINT = ComponentType::SINT;
constexpr auto UINT = ComponentType::UINT;
constexpr auto FLOAT = ComponentType::FLOAT;
constexpr bool LINEAR = false;
constexpr bool SRGB = true;
constexpr u32 Hash(TextureFormat format, ComponentType red_component, ComponentType green_component,
ComponentType blue_component, ComponentType alpha_component, bool is_srgb) {
u32 hash = is_srgb ? 1 : 0;
hash |= static_cast<u32>(red_component) << 1;
hash |= static_cast<u32>(green_component) << 4;
hash |= static_cast<u32>(blue_component) << 7;
hash |= static_cast<u32>(alpha_component) << 10;
hash |= static_cast<u32>(format) << 13;
return hash;
}
// (TF2PF_ELEM[A-Z_0-9(]+, )([A-Z]+)(, LINEAR|. SRGB)
// $1$2, $2, $2, $2$3
#define TF2PF_LIST \
TF2PF_ELEM(A8B8G8R8, UNORM, UNORM, UNORM, UNORM, LINEAR, A8B8G8R8_UNORM) \
TF2PF_ELEM(A8B8G8R8, UNORM, UNORM, UNORM, UNORM, SRGB, A8B8G8R8_SRGB) \
TF2PF_ELEM(A8B8G8R8, SNORM, SNORM, SNORM, SNORM, LINEAR, A8B8G8R8_SNORM) \
TF2PF_ELEM(A8B8G8R8, UINT, UINT, UINT, UINT, LINEAR, A8B8G8R8_UINT) \
TF2PF_ELEM(A8B8G8R8, SINT, SINT, SINT, SINT, LINEAR, A8B8G8R8_SINT) \
TF2PF_ELEM(B5G6R5, UNORM, UNORM, UNORM, UNORM, LINEAR, B5G6R5_UNORM) \
TF2PF_ELEM(A2B10G10R10, UNORM, UNORM, UNORM, UNORM, LINEAR, A2B10G10R10_UNORM) \
TF2PF_ELEM(A2B10G10R10, UINT, UINT, UINT, UINT, LINEAR, A2B10G10R10_UINT) \
TF2PF_ELEM(A1B5G5R5, UNORM, UNORM, UNORM, UNORM, LINEAR, A1B5G5R5_UNORM) \
TF2PF_ELEM(A4B4G4R4, UNORM, UNORM, UNORM, UNORM, LINEAR, A4B4G4R4_UNORM) \
TF2PF_ELEM(G4R4, UNORM, UNORM, UNORM, UNORM, LINEAR, G4R4_UNORM) \
TF2PF_ELEM(A5B5G5R1, UNORM, UNORM, UNORM, UNORM, LINEAR, A5B5G5R1_UNORM) \
TF2PF_ELEM(R8, UNORM, UNORM, UNORM, UNORM, LINEAR, R8_UNORM) \
TF2PF_ELEM(R8, SNORM, SNORM, SNORM, SNORM, LINEAR, R8_SNORM) \
TF2PF_ELEM(R8, UINT, UINT, UINT, UINT, LINEAR, R8_UINT) \
TF2PF_ELEM(R8, SINT, SINT, SINT, SINT, LINEAR, R8_SINT) \
TF2PF_ELEM(G8R8, UNORM, UNORM, UNORM, UNORM, LINEAR, R8G8_UNORM) \
TF2PF_ELEM(G8R8, SNORM, SNORM, SNORM, SNORM, LINEAR, R8G8_SNORM) \
TF2PF_ELEM(G8R8, UINT, UINT, UINT, UINT, LINEAR, R8G8_UINT) \
TF2PF_ELEM(G8R8, SINT, SINT, SINT, SINT, LINEAR, R8G8_SINT) \
TF2PF_ELEM(R16G16B16A16, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R16G16B16A16_FLOAT) \
TF2PF_ELEM(R16G16B16A16, UNORM, UNORM, UNORM, UNORM, LINEAR, R16G16B16A16_UNORM) \
TF2PF_ELEM(R16G16B16A16, SNORM, SNORM, SNORM, SNORM, LINEAR, R16G16B16A16_SNORM) \
TF2PF_ELEM(R16G16B16A16, UINT, UINT, UINT, UINT, LINEAR, R16G16B16A16_UINT) \
TF2PF_ELEM(R16G16B16A16, SINT, SINT, SINT, SINT, LINEAR, R16G16B16A16_SINT) \
TF2PF_ELEM(R16G16, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R16G16_FLOAT) \
TF2PF_ELEM(R16G16, UNORM, UNORM, UNORM, UNORM, LINEAR, R16G16_UNORM) \
TF2PF_ELEM(R16G16, SNORM, SNORM, SNORM, SNORM, LINEAR, R16G16_SNORM) \
TF2PF_ELEM(R16G16, UINT, UINT, UINT, UINT, LINEAR, R16G16_UINT) \
TF2PF_ELEM(R16G16, SINT, SINT, SINT, SINT, LINEAR, R16G16_SINT) \
TF2PF_ELEM(R16, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R16_FLOAT) \
TF2PF_ELEM(R16, UNORM, UNORM, UNORM, UNORM, LINEAR, R16_UNORM) \
TF2PF_ELEM(R16, SNORM, SNORM, SNORM, SNORM, LINEAR, R16_SNORM) \
TF2PF_ELEM(R16, UINT, UINT, UINT, UINT, LINEAR, R16_UINT) \
TF2PF_ELEM(R16, SINT, SINT, SINT, SINT, LINEAR, R16_SINT) \
TF2PF_ELEM(B10G11R11, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, B10G11R11_FLOAT) \
TF2PF_ELEM(R32G32B32A32, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R32G32B32A32_FLOAT) \
TF2PF_ELEM(R32G32B32A32, UINT, UINT, UINT, UINT, LINEAR, R32G32B32A32_UINT) \
TF2PF_ELEM(R32G32B32A32, SINT, SINT, SINT, SINT, LINEAR, R32G32B32A32_SINT) \
TF2PF_ELEM(R32G32B32, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R32G32B32_FLOAT) \
TF2PF_ELEM(R32G32, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R32G32_FLOAT) \
TF2PF_ELEM(R32G32, UINT, UINT, UINT, UINT, LINEAR, R32G32_UINT) \
TF2PF_ELEM(R32G32, SINT, SINT, SINT, SINT, LINEAR, R32G32_SINT) \
TF2PF_ELEM(R32, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, R32_FLOAT) \
TF2PF_ELEM(R32, UINT, UINT, UINT, UINT, LINEAR, R32_UINT) \
TF2PF_ELEM(R32, SINT, SINT, SINT, SINT, LINEAR, R32_SINT) \
TF2PF_ELEM(E5B9G9R9, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, E5B9G9R9_FLOAT) \
TF2PF_ELEM(Z32, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, D32_FLOAT) \
TF2PF_ELEM(Z32, FLOAT, UINT, UINT, UINT, LINEAR, D32_FLOAT) \
TF2PF_ELEM(Z16, UNORM, UNORM, UNORM, UNORM, LINEAR, D16_UNORM) \
TF2PF_ELEM(Z16, UNORM, UINT, UINT, UINT, LINEAR, D16_UNORM) \
TF2PF_ELEM(X8Z24, UNORM, UNORM, UNORM, UNORM, LINEAR, X8_D24_UNORM) \
TF2PF_ELEM(X8Z24, UNORM, UINT, UINT, UINT, LINEAR, X8_D24_UNORM) \
TF2PF_ELEM(Z24S8, UINT, UNORM, UNORM, UNORM, LINEAR, S8_UINT_D24_UNORM) \
TF2PF_ELEM(Z24S8, UINT, UNORM, UINT, UINT, LINEAR, S8_UINT_D24_UNORM) \
TF2PF_ELEM(G24R8, UINT, UNORM, UNORM, UNORM, LINEAR, S8_UINT_D24_UNORM) \
TF2PF_ELEM(S8Z24, UNORM, UINT, UINT, UINT, LINEAR, D24_UNORM_S8_UINT) \
TF2PF_ELEM(Z32_X24S8, FLOAT, UINT, UNORM, UNORM, LINEAR, D32_FLOAT_S8_UINT) \
TF2PF_ELEM(R32B24G8, FLOAT, UINT, UNORM, UNORM, LINEAR, D32_FLOAT_S8_UINT) \
TF2PF_ELEM(DXT1, UNORM, UNORM, UNORM, UNORM, LINEAR, BC1_RGBA_UNORM) \
TF2PF_ELEM(DXT1, UNORM, UNORM, UNORM, UNORM, SRGB, BC1_RGBA_SRGB) \
TF2PF_ELEM(DXT23, UNORM, UNORM, UNORM, UNORM, LINEAR, BC2_UNORM) \
TF2PF_ELEM(DXT23, UNORM, UNORM, UNORM, UNORM, SRGB, BC2_SRGB) \
TF2PF_ELEM(DXT45, UNORM, UNORM, UNORM, UNORM, LINEAR, BC3_UNORM) \
TF2PF_ELEM(DXT45, UNORM, UNORM, UNORM, UNORM, SRGB, BC3_SRGB) \
TF2PF_ELEM(DXN1, UNORM, UNORM, UNORM, UNORM, LINEAR, BC4_UNORM) \
TF2PF_ELEM(DXN1, SNORM, SNORM, SNORM, SNORM, LINEAR, BC4_SNORM) \
TF2PF_ELEM(DXN2, UNORM, UNORM, UNORM, UNORM, LINEAR, BC5_UNORM) \
TF2PF_ELEM(DXN2, SNORM, SNORM, SNORM, SNORM, LINEAR, BC5_SNORM) \
TF2PF_ELEM(BC7U, UNORM, UNORM, UNORM, UNORM, LINEAR, BC7_UNORM) \
TF2PF_ELEM(BC7U, UNORM, UNORM, UNORM, UNORM, SRGB, BC7_SRGB) \
TF2PF_ELEM(BC6H_S16, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, BC6H_SFLOAT) \
TF2PF_ELEM(BC6H_U16, FLOAT, FLOAT, FLOAT, FLOAT, LINEAR, BC6H_UFLOAT) \
TF2PF_ELEM(ASTC_2D_4X4, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_4X4_UNORM) \
TF2PF_ELEM(ASTC_2D_4X4, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_4X4_SRGB) \
TF2PF_ELEM(ASTC_2D_5X4, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_5X4_UNORM) \
TF2PF_ELEM(ASTC_2D_5X4, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_5X4_SRGB) \
TF2PF_ELEM(ASTC_2D_5X5, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_5X5_UNORM) \
TF2PF_ELEM(ASTC_2D_5X5, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_5X5_SRGB) \
TF2PF_ELEM(ASTC_2D_8X8, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_8X8_UNORM) \
TF2PF_ELEM(ASTC_2D_8X8, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_8X8_SRGB) \
TF2PF_ELEM(ASTC_2D_8X5, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_8X5_UNORM) \
TF2PF_ELEM(ASTC_2D_8X5, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_8X5_SRGB) \
TF2PF_ELEM(ASTC_2D_10X8, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_10X8_UNORM) \
TF2PF_ELEM(ASTC_2D_10X8, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_10X8_SRGB) \
TF2PF_ELEM(ASTC_2D_6X6, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_6X6_UNORM) \
TF2PF_ELEM(ASTC_2D_6X6, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_6X6_SRGB) \
TF2PF_ELEM(ASTC_2D_10X6, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_10X6_UNORM) \
TF2PF_ELEM(ASTC_2D_10X6, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_10X6_SRGB) \
TF2PF_ELEM(ASTC_2D_10X5, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_10X5_UNORM) \
TF2PF_ELEM(ASTC_2D_10X5, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_10X5_SRGB) \
TF2PF_ELEM(ASTC_2D_10X10, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_10X10_UNORM) \
TF2PF_ELEM(ASTC_2D_10X10, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_10X10_SRGB) \
TF2PF_ELEM(ASTC_2D_12X10, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_12X10_UNORM) \
TF2PF_ELEM(ASTC_2D_12X10, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_12X10_SRGB) \
TF2PF_ELEM(ASTC_2D_12X12, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_12X12_UNORM) \
TF2PF_ELEM(ASTC_2D_12X12, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_12X12_SRGB) \
TF2PF_ELEM(ASTC_2D_8X6, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_8X6_UNORM) \
TF2PF_ELEM(ASTC_2D_8X6, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_8X6_SRGB) \
TF2PF_ELEM(ASTC_2D_6X5, UNORM, UNORM, UNORM, UNORM, LINEAR, ASTC_2D_6X5_UNORM) \
TF2PF_ELEM(ASTC_2D_6X5, UNORM, UNORM, UNORM, UNORM, SRGB, ASTC_2D_6X5_SRGB) \
constexpr u32 Hash(TextureFormat format, ComponentType component, bool is_srgb = LINEAR) {
return Hash(format, component, component, component, component, is_srgb);
}
} // Anonymous namespace
PixelFormat PixelFormatFromTextureInfo(TextureFormat format, ComponentType red, ComponentType green,
ComponentType blue, ComponentType alpha,
bool is_srgb) noexcept {
switch (Hash(format, red, green, blue, alpha, is_srgb)) {
case Hash(TextureFormat::A8B8G8R8, UNORM):
return PixelFormat::A8B8G8R8_UNORM;
case Hash(TextureFormat::A8B8G8R8, SNORM):
return PixelFormat::A8B8G8R8_SNORM;
case Hash(TextureFormat::A8B8G8R8, UINT):
return PixelFormat::A8B8G8R8_UINT;
case Hash(TextureFormat::A8B8G8R8, SINT):
return PixelFormat::A8B8G8R8_SINT;
case Hash(TextureFormat::A8B8G8R8, UNORM, SRGB):
return PixelFormat::A8B8G8R8_SRGB;
case Hash(TextureFormat::B5G6R5, UNORM):
return PixelFormat::B5G6R5_UNORM;
case Hash(TextureFormat::A2B10G10R10, UNORM):
return PixelFormat::A2B10G10R10_UNORM;
case Hash(TextureFormat::A2B10G10R10, UINT):
return PixelFormat::A2B10G10R10_UINT;
case Hash(TextureFormat::A1B5G5R5, UNORM):
return PixelFormat::A1B5G5R5_UNORM;
case Hash(TextureFormat::A4B4G4R4, UNORM):
return PixelFormat::A4B4G4R4_UNORM;
case Hash(TextureFormat::G4R4, UNORM):
return PixelFormat::G4R4_UNORM;
case Hash(TextureFormat::A5B5G5R1, UNORM):
return PixelFormat::A5B5G5R1_UNORM;
case Hash(TextureFormat::R8, UNORM):
return PixelFormat::R8_UNORM;
case Hash(TextureFormat::R8, SNORM):
return PixelFormat::R8_SNORM;
case Hash(TextureFormat::R8, UINT):
return PixelFormat::R8_UINT;
case Hash(TextureFormat::R8, SINT):
return PixelFormat::R8_SINT;
case Hash(TextureFormat::G8R8, UNORM):
return PixelFormat::R8G8_UNORM;
case Hash(TextureFormat::G8R8, SNORM):
return PixelFormat::R8G8_SNORM;
case Hash(TextureFormat::G8R8, UINT):
return PixelFormat::R8G8_UINT;
case Hash(TextureFormat::G8R8, SINT):
return PixelFormat::R8G8_SINT;
case Hash(TextureFormat::R16G16B16A16, FLOAT):
return PixelFormat::R16G16B16A16_FLOAT;
case Hash(TextureFormat::R16G16B16A16, UNORM):
return PixelFormat::R16G16B16A16_UNORM;
case Hash(TextureFormat::R16G16B16A16, SNORM):
return PixelFormat::R16G16B16A16_SNORM;
case Hash(TextureFormat::R16G16B16A16, UINT):
return PixelFormat::R16G16B16A16_UINT;
case Hash(TextureFormat::R16G16B16A16, SINT):
return PixelFormat::R16G16B16A16_SINT;
case Hash(TextureFormat::R16G16, FLOAT):
return PixelFormat::R16G16_FLOAT;
case Hash(TextureFormat::R16G16, UNORM):
return PixelFormat::R16G16_UNORM;
case Hash(TextureFormat::R16G16, SNORM):
return PixelFormat::R16G16_SNORM;
case Hash(TextureFormat::R16G16, UINT):
return PixelFormat::R16G16_UINT;
case Hash(TextureFormat::R16G16, SINT):
return PixelFormat::R16G16_SINT;
case Hash(TextureFormat::R16, FLOAT):
return PixelFormat::R16_FLOAT;
case Hash(TextureFormat::R16, UNORM):
return PixelFormat::R16_UNORM;
case Hash(TextureFormat::R16, SNORM):
return PixelFormat::R16_SNORM;
case Hash(TextureFormat::R16, UINT):
return PixelFormat::R16_UINT;
case Hash(TextureFormat::R16, SINT):
return PixelFormat::R16_SINT;
case Hash(TextureFormat::B10G11R11, FLOAT):
return PixelFormat::B10G11R11_FLOAT;
case Hash(TextureFormat::R32G32B32A32, FLOAT):
return PixelFormat::R32G32B32A32_FLOAT;
case Hash(TextureFormat::R32G32B32A32, UINT):
return PixelFormat::R32G32B32A32_UINT;
case Hash(TextureFormat::R32G32B32A32, SINT):
return PixelFormat::R32G32B32A32_SINT;
case Hash(TextureFormat::R32G32B32, FLOAT):
return PixelFormat::R32G32B32_FLOAT;
case Hash(TextureFormat::R32G32, FLOAT):
return PixelFormat::R32G32_FLOAT;
case Hash(TextureFormat::R32G32, UINT):
return PixelFormat::R32G32_UINT;
case Hash(TextureFormat::R32G32, SINT):
return PixelFormat::R32G32_SINT;
case Hash(TextureFormat::R32, FLOAT):
return PixelFormat::R32_FLOAT;
case Hash(TextureFormat::R32, UINT):
return PixelFormat::R32_UINT;
case Hash(TextureFormat::R32, SINT):
return PixelFormat::R32_SINT;
case Hash(TextureFormat::E5B9G9R9, FLOAT):
return PixelFormat::E5B9G9R9_FLOAT;
case Hash(TextureFormat::Z32, FLOAT):
return PixelFormat::D32_FLOAT;
case Hash(TextureFormat::Z32, FLOAT, UINT, UINT, UINT, LINEAR):
return PixelFormat::D32_FLOAT;
case Hash(TextureFormat::Z16, UNORM):
return PixelFormat::D16_UNORM;
case Hash(TextureFormat::Z16, UNORM, UINT, UINT, UINT, LINEAR):
return PixelFormat::D16_UNORM;
case Hash(TextureFormat::X8Z24, UNORM):
return PixelFormat::X8_D24_UNORM;
case Hash(TextureFormat::X8Z24, UNORM, UINT, UINT, UINT, LINEAR):
return PixelFormat::X8_D24_UNORM;
case Hash(TextureFormat::Z24S8, UINT, UNORM, UNORM, UNORM, LINEAR):
return PixelFormat::S8_UINT_D24_UNORM;
case Hash(TextureFormat::Z24S8, UINT, UNORM, UINT, UINT, LINEAR):
return PixelFormat::S8_UINT_D24_UNORM;
case Hash(TextureFormat::G24R8, UINT, UNORM, UNORM, UNORM, LINEAR):
return PixelFormat::S8_UINT_D24_UNORM;
case Hash(TextureFormat::S8Z24, UNORM, UINT, UINT, UINT, LINEAR):
return PixelFormat::D24_UNORM_S8_UINT;
case Hash(TextureFormat::Z32_X24S8, FLOAT, UINT, UNORM, UNORM, LINEAR):
return PixelFormat::D32_FLOAT_S8_UINT;
case Hash(TextureFormat::R32B24G8, FLOAT, UINT, UNORM, UNORM, LINEAR):
return PixelFormat::D32_FLOAT_S8_UINT;
case Hash(TextureFormat::DXT1, UNORM, LINEAR):
return PixelFormat::BC1_RGBA_UNORM;
case Hash(TextureFormat::DXT1, UNORM, SRGB):
return PixelFormat::BC1_RGBA_SRGB;
case Hash(TextureFormat::DXT23, UNORM, LINEAR):
return PixelFormat::BC2_UNORM;
case Hash(TextureFormat::DXT23, UNORM, SRGB):
return PixelFormat::BC2_SRGB;
case Hash(TextureFormat::DXT45, UNORM, LINEAR):
return PixelFormat::BC3_UNORM;
case Hash(TextureFormat::DXT45, UNORM, SRGB):
return PixelFormat::BC3_SRGB;
case Hash(TextureFormat::DXN1, UNORM):
return PixelFormat::BC4_UNORM;
case Hash(TextureFormat::DXN1, SNORM):
return PixelFormat::BC4_SNORM;
case Hash(TextureFormat::DXN2, UNORM):
return PixelFormat::BC5_UNORM;
case Hash(TextureFormat::DXN2, SNORM):
return PixelFormat::BC5_SNORM;
case Hash(TextureFormat::BC7U, UNORM, LINEAR):
return PixelFormat::BC7_UNORM;
case Hash(TextureFormat::BC7U, UNORM, SRGB):
return PixelFormat::BC7_SRGB;
case Hash(TextureFormat::BC6H_S16, FLOAT):
return PixelFormat::BC6H_SFLOAT;
case Hash(TextureFormat::BC6H_U16, FLOAT):
return PixelFormat::BC6H_UFLOAT;
case Hash(TextureFormat::ASTC_2D_4X4, UNORM, LINEAR):
return PixelFormat::ASTC_2D_4X4_UNORM;
case Hash(TextureFormat::ASTC_2D_4X4, UNORM, SRGB):
return PixelFormat::ASTC_2D_4X4_SRGB;
case Hash(TextureFormat::ASTC_2D_5X4, UNORM, LINEAR):
return PixelFormat::ASTC_2D_5X4_UNORM;
case Hash(TextureFormat::ASTC_2D_5X4, UNORM, SRGB):
return PixelFormat::ASTC_2D_5X4_SRGB;
case Hash(TextureFormat::ASTC_2D_5X5, UNORM, LINEAR):
return PixelFormat::ASTC_2D_5X5_UNORM;
case Hash(TextureFormat::ASTC_2D_5X5, UNORM, SRGB):
return PixelFormat::ASTC_2D_5X5_SRGB;
case Hash(TextureFormat::ASTC_2D_8X8, UNORM, LINEAR):
return PixelFormat::ASTC_2D_8X8_UNORM;
case Hash(TextureFormat::ASTC_2D_8X8, UNORM, SRGB):
return PixelFormat::ASTC_2D_8X8_SRGB;
case Hash(TextureFormat::ASTC_2D_8X5, UNORM, LINEAR):
return PixelFormat::ASTC_2D_8X5_UNORM;
case Hash(TextureFormat::ASTC_2D_8X5, UNORM, SRGB):
return PixelFormat::ASTC_2D_8X5_SRGB;
case Hash(TextureFormat::ASTC_2D_10X8, UNORM, LINEAR):
return PixelFormat::ASTC_2D_10X8_UNORM;
case Hash(TextureFormat::ASTC_2D_10X8, UNORM, SRGB):
return PixelFormat::ASTC_2D_10X8_SRGB;
case Hash(TextureFormat::ASTC_2D_6X6, UNORM, LINEAR):
return PixelFormat::ASTC_2D_6X6_UNORM;
case Hash(TextureFormat::ASTC_2D_6X6, UNORM, SRGB):
return PixelFormat::ASTC_2D_6X6_SRGB;
case Hash(TextureFormat::ASTC_2D_10X6, UNORM, LINEAR):
return PixelFormat::ASTC_2D_10X6_UNORM;
case Hash(TextureFormat::ASTC_2D_10X6, UNORM, SRGB):
return PixelFormat::ASTC_2D_10X6_SRGB;
case Hash(TextureFormat::ASTC_2D_10X5, UNORM, LINEAR):
return PixelFormat::ASTC_2D_10X5_UNORM;
case Hash(TextureFormat::ASTC_2D_10X5, UNORM, SRGB):
return PixelFormat::ASTC_2D_10X5_SRGB;
case Hash(TextureFormat::ASTC_2D_10X10, UNORM, LINEAR):
return PixelFormat::ASTC_2D_10X10_UNORM;
case Hash(TextureFormat::ASTC_2D_10X10, UNORM, SRGB):
return PixelFormat::ASTC_2D_10X10_SRGB;
case Hash(TextureFormat::ASTC_2D_12X10, UNORM, LINEAR):
return PixelFormat::ASTC_2D_12X10_UNORM;
case Hash(TextureFormat::ASTC_2D_12X10, UNORM, SRGB):
return PixelFormat::ASTC_2D_12X10_SRGB;
case Hash(TextureFormat::ASTC_2D_12X12, UNORM, LINEAR):
return PixelFormat::ASTC_2D_12X12_UNORM;
case Hash(TextureFormat::ASTC_2D_12X12, UNORM, SRGB):
return PixelFormat::ASTC_2D_12X12_SRGB;
case Hash(TextureFormat::ASTC_2D_8X6, UNORM, LINEAR):
return PixelFormat::ASTC_2D_8X6_UNORM;
case Hash(TextureFormat::ASTC_2D_8X6, UNORM, SRGB):
return PixelFormat::ASTC_2D_8X6_SRGB;
case Hash(TextureFormat::ASTC_2D_6X5, UNORM, LINEAR):
return PixelFormat::ASTC_2D_6X5_UNORM;
case Hash(TextureFormat::ASTC_2D_6X5, UNORM, SRGB):
return PixelFormat::ASTC_2D_6X5_SRGB;
}
UNIMPLEMENTED_MSG("texture format={} srgb={} components={{{} {} {} {}}}",
static_cast<int>(format), is_srgb, static_cast<int>(red),
static_cast<int>(green), static_cast<int>(blue), static_cast<int>(alpha));
PixelFormat PixelFormatFromTextureInfo(TextureFormat format, ComponentType red, ComponentType green, ComponentType blue, ComponentType alpha, bool is_srgb) noexcept {
if (false) { /* no op */ }
#define TF2PF_ELEM(FMT, CR, CG, CB, CA, SRGB, RES) \
else if (format == TextureFormat::FMT \
&& red == ComponentType::CR && green == ComponentType::CG \
&& blue == ComponentType::CB && alpha == ComponentType::CA \
&& is_srgb == SRGB) return PixelFormat::RES;
TF2PF_LIST
#undef TF2PF_ELEM
UNIMPLEMENTED_MSG("texture format={} srgb={} components={{{} {} {} {}}}", int(format), is_srgb, int(red), int(green), int(blue), int(alpha));
return PixelFormat::A8B8G8R8_UNORM;
}
@@ -1168,7 +1168,6 @@ bool Device::GetSuitability(bool requires_swapchain) {
// Driver detection variables for workarounds in GetSuitability
const VkDriverId driver_id = properties.driver.driverID;
const bool is_intel_windows = driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS;
// VK_EXT_extended_dynamic_state2 below this will appear drivers that need workarounds.
@@ -1185,15 +1184,14 @@ bool Device::GetSuitability(bool requires_swapchain) {
}
// Intel Windows < 27.20.100.0: Broken VertexInputDynamicState
// Same for NVIDIA Proprietary < 580.119.02, unknown when VIDS was first NOT broken
// Disable VertexInputDynamicState on old Intel Windows drivers
if (extensions.vertex_input_dynamic_state && is_intel_windows) {
if (extensions.vertex_input_dynamic_state) {
const u32 version = (properties.properties.driverVersion << 3) >> 3;
if (version < VK_MAKE_API_VERSION(27, 20, 100, 0)) {
LOG_WARNING(Render_Vulkan,
"Intel Windows < 27.20.100.0: Disabling broken VK_EXT_vertex_input_dynamic_state");
RemoveExtensionFeature(extensions.vertex_input_dynamic_state,
features.vertex_input_dynamic_state,
VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME);
if ((driver_id == VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS && version < VK_MAKE_API_VERSION(27, 20, 100, 0))
|| (driver_id == VK_DRIVER_ID_NVIDIA_PROPRIETARY && version < VK_MAKE_API_VERSION(580, 119, 02, 0))) {
LOG_WARNING(Render_Vulkan, "Disabling broken VK_EXT_vertex_input_dynamic_state");
RemoveExtensionFeature(extensions.vertex_input_dynamic_state, features.vertex_input_dynamic_state, VK_EXT_VERTEX_INPUT_DYNAMIC_STATE_EXTENSION_NAME);
}
}
+12 -8
View File
@@ -156,11 +156,14 @@ add_executable(yuzu
debugger/controller.cpp
debugger/controller.h
game_list.cpp
game_list.h
game_list_p.h
game_list_worker.cpp
game_list_worker.h
game/game_list.cpp
game/game_list.h
game/game_list_p.h
game/game_list_worker.cpp
game/game_list_worker.h
game/game_card.h
game/game_card.cpp
hotkeys.cpp
hotkeys.h
install_dialog.cpp
@@ -234,10 +237,11 @@ add_executable(yuzu
data_dialog.h data_dialog.cpp data_dialog.ui
data_widget.ui
ryujinx_dialog.h ryujinx_dialog.cpp ryujinx_dialog.ui
main_window.h main_window.cpp
main_window.h main_window.cpp main.ui
configuration/system/new_user_dialog.h configuration/system/new_user_dialog.cpp configuration/system/new_user_dialog.ui
configuration/system/profile_avatar_dialog.h configuration/system/profile_avatar_dialog.cpp
configuration/addon/mod_select_dialog.h configuration/addon/mod_select_dialog.cpp configuration/addon/mod_select_dialog.ui
)
set_target_properties(yuzu PROPERTIES OUTPUT_NAME "eden")
@@ -361,12 +365,12 @@ endif()
if (APPLE)
# Normal icns
set(MACOSX_ICON "../../dist/eden.icns")
set(MACOSX_ICON "${CMAKE_SOURCE_DIR}/dist/eden.icns")
set_source_files_properties(${MACOSX_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
target_sources(yuzu PRIVATE ${MACOSX_ICON})
# Liquid glass
set(MACOSX_LIQUID_GLASS_ICON "../../dist/Assets.car")
set(MACOSX_LIQUID_GLASS_ICON "${CMAKE_SOURCE_DIR}/dist/Assets.car")
set_source_files_properties(${MACOSX_LIQUID_GLASS_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
target_sources(yuzu PRIVATE ${MACOSX_LIQUID_GLASS_ICON})
@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QFileInfo>
#include <qnamespace.h>
#include "mod_select_dialog.h"
#include "ui_mod_select_dialog.h"
ModSelectDialog::ModSelectDialog(const QStringList& mods, QWidget* parent)
: QDialog(parent), ui(new Ui::ModSelectDialog) {
ui->setupUi(this);
item_model = new QStandardItemModel(ui->treeView);
ui->treeView->setModel(item_model);
// We must register all custom types with the Qt Automoc system so that we are able to use it
// with signals/slots. In this case, QList falls under the umbrella of custom types.
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
for (const auto& mod : mods) {
const auto basename = QFileInfo(mod).fileName();
auto* const first_item = new QStandardItem;
first_item->setText(basename);
first_item->setData(mod);
first_item->setCheckable(true);
first_item->setCheckState(Qt::Checked);
item_model->appendRow(first_item);
}
ui->treeView->expandAll();
ui->treeView->resizeColumnToContents(0);
int rows = item_model->rowCount();
int height =
ui->treeView->contentsMargins().top() * 4 + ui->treeView->contentsMargins().bottom() * 4;
int width = 0;
for (int i = 0; i < rows; ++i) {
height += ui->treeView->sizeHintForRow(i);
width = qMax(width, item_model->item(i)->sizeHint().width());
}
width += ui->treeView->contentsMargins().left() * 4 + ui->treeView->contentsMargins().right() * 4;
ui->treeView->setMinimumHeight(qMin(height, 600));
ui->treeView->setMinimumWidth(qMin(width, 700));
adjustSize();
connect(this, &QDialog::accepted, this, [this]() {
QStringList selected_mods;
for (qsizetype i = 0; i < item_model->rowCount(); ++i) {
auto* const item = item_model->item(i);
if (item->checkState() == Qt::Checked)
selected_mods << item->data().toString();
}
emit modsSelected(selected_mods);
});
}
ModSelectDialog::~ModSelectDialog() {
delete ui;
}
@@ -0,0 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QDialog>
#include <QStandardItemModel>
namespace Ui {
class ModSelectDialog;
}
class ModSelectDialog : public QDialog {
Q_OBJECT
public:
explicit ModSelectDialog(const QStringList &mods, QWidget* parent = nullptr);
~ModSelectDialog();
signals:
void modsSelected(const QStringList &mods);
private:
Ui::ModSelectDialog* ui;
QStandardItemModel* item_model;
};
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ModSelectDialog</class>
<widget class="QDialog" name="ModSelectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>430</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>The specified folder or archive contains the following mods. Select which ones to install.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTreeView" name="treeView">
<property name="contextMenuPolicy">
<enum>Qt::ContextMenuPolicy::NoContextMenu</enum>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollMode::ScrollPerPixel</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>ModSelectDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>ModSelectDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+3 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
@@ -99,6 +99,8 @@ ConfigureDialog::ConfigureDialog(QWidget* parent, HotkeyRegistry& registry_,
}
});
connect(ui_tab.get(), &ConfigureUi::LanguageChanged, this, &ConfigureDialog::OnLanguageChanged);
connect(general_tab.get(), &ConfigureGeneral::ExternalContentDirsChanged, this,
&ConfigureDialog::ExternalContentDirsChanged);
connect(ui->selectorList, &QListWidget::itemSelectionChanged, this,
&ConfigureDialog::UpdateVisibleTabs);
+2 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2016 Citra Emulator Project
@@ -62,6 +62,7 @@ private slots:
signals:
void LanguageChanged(const QString& locale);
void ExternalContentDirsChanged();
private:
void changeEvent(QEvent* event) override;
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@@ -38,9 +38,9 @@ ConfigureFilesystem::ConfigureFilesystem(QWidget* parent)
connect(ui->reset_game_list_cache, &QPushButton::pressed, this,
&ConfigureFilesystem::ResetMetadata);
connect(ui->gamecard_inserted, &QCheckBox::STATE_CHANGED, this,
connect(ui->gamecard_inserted, &QCheckBox::stateChanged, this,
&ConfigureFilesystem::UpdateEnabledControls);
connect(ui->gamecard_current_game, &QCheckBox::STATE_CHANGED, this,
connect(ui->gamecard_current_game, &QCheckBox::stateChanged, this,
&ConfigureFilesystem::UpdateEnabledControls);
}
@@ -278,6 +278,7 @@ void ConfigureFilesystem::UpdateEnabledControls() {
!ui->gamecard_current_game->isChecked());
}
void ConfigureFilesystem::RetranslateUI() {
ui->retranslateUi(this);
}

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