Compare commits

...

50 commits

Author SHA1 Message Date
lizzie
8db6b82bdf fix dynarmic i hope 2026-03-07 18:34:19 +00:00
lizzie
f731f91175 fx 2026-03-07 18:34:19 +00:00
lizzie
b6b46bb657 fx 2026-03-07 18:34:19 +00:00
lizzie
761f20947b fix boost 2026-03-07 18:34:19 +00:00
lizzie
e4ae4b43bf fx 2026-03-07 18:34:19 +00:00
lizzie
c04aebe9a4 fix stuff 2026-03-07 18:34:19 +00:00
lizzie
e1801ab9f9 stupid macos 2026-03-07 18:34:19 +00:00
lizzie
8e0b550c68 fix1 2026-03-07 18:34:19 +00:00
lizzie
b79d4f80b3 fx 2026-03-07 18:34:19 +00:00
lizzie
63ba079494 fix spirv-tools 2026-03-07 18:34:19 +00:00
lizzie
d5d7587f5b fixes for ios spirv tools 2026-03-07 18:34:19 +00:00
lizzie
e4641ba887 fix license 2026-03-07 18:34:19 +00:00
lizzie
0e2133f5fb fix ffmpeg 2026-03-07 18:34:19 +00:00
lizzie
e9744b5520 fx 2026-03-07 18:34:19 +00:00
lizzie
ea4b24ee6b fx 2026-03-07 18:34:19 +00:00
lizzie
625395bfec license 2026-03-07 18:34:19 +00:00
lizzie
f8c6332ab5 ios toolchain cmake 2026-03-07 18:34:19 +00:00
lizzie
c0a947e90a license 2026-03-07 18:34:19 +00:00
lizzie
fce3bec917 license headers 2026-03-07 18:34:19 +00:00
lizzie
2c9d9788e3 flatten + cmake 2026-03-07 18:34:19 +00:00
lizzie
6883b30af8 flatten 2026-03-07 18:34:19 +00:00
lizzie
bfd09c4d8a loicense 2026-03-07 18:34:19 +00:00
lizzie
63ce379dd7 modernize #1 2026-03-07 18:34:19 +00:00
lizzie
fb69cb06d1 sudachi ios stuff 2026-03-07 18:34:19 +00:00
lizzie
11ad71b1e7
[docs] Obtanium, installing mods, external ES-DE config, section about CFW and settings (#3678)
Some checks are pending
tx-src / sources (push) Waiting to run
Check Strings / check-strings (push) Waiting to run
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3678
Reviewed-by: DraVee <chimera@dravee.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-07 18:16:05 +01:00
xbzk
ddac8c8eb5
[vk] fix crash introduced in 9a07bd0570 (#3685)
Some checks are pending
tx-src / sources (push) Waiting to run
Check Strings / check-strings (push) Waiting to run
Fix for current crash on master.
Just reverted only the necessary stuff so that PresentManager can hold a reference to khr and resist death upon application hold/restore.
@Lizzie shall judge.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3685
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-03-06 19:52:17 +01:00
lizzie
c062931c9b
[qt] add translation table entry for debug_knobs,serial_battery and serial_unit (#3682)
trivial qt change

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3682
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-06 16:38:39 +01:00
crueter
e4122dae1d
[desktop] addons: open mod folder in rc menu (#3662)
also fixed the multiselection being absolutely horrendous

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3662
2026-03-06 16:38:21 +01:00
lizzie
b75e81af5e
[video_core/engines] implement stub NV01 timer, inline other channel engines (#3640)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3640
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-06 15:05:39 +01:00
lizzie
2ed1328c93
[vk] use static_vector instead of small_vector for TFB and other bindings (#3641)
MK8D is a big offender, taking up lots of time memcpy'ing and memmov'ing small_vector<> AND to add salt to the wound it doesn't even do heap allocations (no game does I think) - so basically useless waste of compute time in hot path for NO reason :^)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3641
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-06 15:05:05 +01:00
lizzie
c70b857c4f
[video_core/engines] Macro HLE inline (#3653)
Should slightly boost perf on android, Desktop is mainly unaffected (for now)

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3653
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-06 15:04:38 +01:00
MaranBr
23566a1f7d
[prepo] Add support for missing PlayReport commands (#3674)
This fixes:

`[ 433.095195] Debug <Critical> core\hle\service\service.cpp:operator ():69: Assertion Failed!
Unknown / unimplemented function '10107': port='prepo:u' cmd_buf={[0]=0x110006, [1]=0x80000014, [2]=0x1, [3]=0x0, [4]=0x0, [5]=0x191080, [6]=0x5A7350F8, [7]=0x112, [8]=0x5A735158}`

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3674
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <chimera@dravee.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2026-03-06 15:02:59 +01:00
xbzk
529b069499
[android,ui] fixed top disalignment between buttons of each column in settings fragment (#3675)
this silly little thing tickles obsessive compulsive disturbed fellas a lot hu3
was shipped along PR 3660, which was rediscussed for other reason, hence this tiny lonely PR.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3675
Reviewed-by: DraVee <chimera@dravee.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-03-05 13:58:46 +01:00
lizzie
9a07bd0570
[vk] unify VkSurfaceKHR with Android and the rest of platforms; remove technically incorrect nullptr() ctor for handles (#2971)
Removes some odd #ifdef-ing that just can use a shrimple opaque type.

Also removes nullptr() ctor'ing for vulkan handles and such; it's not incorrect per se like how `void *p = 0;` isn't incorrect, just that, y'know, any static analyzer will go "woah". Also there isn't any guarantee that handles `sizeof(Handle) == sizeof(void*)` so may as well :)

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2971
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-05 07:32:18 +01:00
xbzk
05f6942bef
[android, ui] fixed setting reset to defaults infinite loop (#3659)
fixed a bug discovered by Pavel in which the settings' "reset to defaults" dialog would get stuck in a infinite loop, due to a recall prior to cleaning state.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3659
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: DraVee <chimera@dravee.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-03-05 00:58:49 +01:00
nekle
70c1e9abed
[android] Try and fix SD Card storage mount points for external paths (#3436)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3436
Reviewed-by: DraVee <chimera@dravee.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: nekle <nekle@protonmail.com>
Co-committed-by: nekle <nekle@protonmail.com>
2026-03-05 00:56:25 +01:00
wildcard
69e47bd2c0
[vulkan] Fix wrong stage index in prepare_stage render area check (#3672)
The OpenGL correctly uses const auto& info{stage_infos[stage]};

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3672
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: wildcard <wildcard@eden-emu.dev>
Co-committed-by: wildcard <wildcard@eden-emu.dev>
2026-03-05 00:54:48 +01:00
wildcard
cdf9b556b2
[vulkan]fix vuid 02751 (#3573)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3573
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <chimera@dravee.dev>
Co-authored-by: wildcard <wildcard@eden-emu.dev>
Co-committed-by: wildcard <wildcard@eden-emu.dev>
2026-03-05 00:54:26 +01:00
xbzk
7f5de6bcd6
[android/fs] external content loader + nca/xci patches (#3596)
Foreword: WHY DON'T EVERYBODY USE ONE FOLDER FOR EACH GAME+CONTENTS? AIN'T THIS THE FORMAT GAMES COME WHEN YOU BUE THEM? DO YOU LIVE WITH ALL YOUR FRIENDS AND HAVE A 2ND HOUSE FOR ALL THE CHILDREN?

Nice, i feel better now.

This feat extends Maufeat's work on external content loading. It harmonically additions:
"...also, if in each game folder X, you find a folder Y, and in this folder Y you detect ONLY a single game, then mount all external content for that game found in that folder Y and its subfolders."
Permanent (not toggleable). External Content folders are supported equally.

Also:
-Reworked several routines for preserving single source of truth between android and other systems;
-Fixed the annoying unknown format error for content files, by providing proper format detection.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3596
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-03-04 22:51:35 +01:00
wildcard
c682306788
[vulkan] Implement push descriptors for compute pipelines (#3666)
Implements push descriptor for compute pipelines along with a bug fix, the increment logic was, offset += sizeof(DescriptorUpdateEntry);
This only advances the byte offset by a single descriptor slot, regardless of the array's size (descriptorCount).Now suppose if a shader utilized an array of descriptors (eg, layout(binding = 0) uniform sampler2D textures[4]) and if this happened to fit within the MaxPushDescriptors limit, the template would consume 4 * sizeof(DescriptorUpdateEntry) bytes, but the offset for the next binding would only advance by 1 slot.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3666
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: wildcard <wildcard@eden-emu.dev>
Co-committed-by: wildcard <wildcard@eden-emu.dev>
2026-03-04 22:46:55 +01:00
PavelBARABANOV
9d2341eaea
[vk] Disable float16 math on non-MESA AMD drivers as 2026+ versions are broken (#3661)
thanks MaranBR

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3661
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
Co-committed-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
2026-03-03 14:27:47 +01:00
tarako
d720a7b4b4
[vulkan] Fixed dual-source blending to correctly map shader outputs. (#3637)
Fixes Skyward Sword HD eye gitch and a related MoltenVK crash due to the incorrect output mapping. Verified working on mac and android.

The test in vk_pipeline_cache.cpp is a bit ugly, but it didn't seem worth it to go lambda/macro just to make it look cleaner. Could change if necessary.

Co-authored-by: tarako <none@none.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3637
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: tarako <r76036296@gmail.com>
Co-committed-by: tarako <r76036296@gmail.com>
2026-03-03 01:54:57 +01:00
crueter
464212393e
[gitignore] add *.tar.zst (#3663)
the amount of times I've accidentally committed a tar.zst to this
repository...

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3663
2026-03-02 05:28:26 +01:00
MaranBr
ea209e6dab
[vulkan] Fine-tuning of frame pacing logic (#3628)
It makes fine adjustments to the frame pacing, ensuring better stability and precision.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3628
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2026-03-02 02:53:01 +01:00
MaranBr
b5f9f8b743
[vulkan] Fix Vulkan rendering, image layout, and synchronization issues (#3511)
This fixes a visual corruption issue that occurred intermittently after loading screens, where some games would start the scene with vertex explosions, artifacts or with all colors blown out, resembling neon.

Among the known games affected by this bug are Mario Kart 8 Deluxe, The Legend of Zelda: Breath of the Wild, The Legend of Zelda: Tears of the Kingdom, Kirby and the Forgotten Land, Luigi's Mansion 3, Xenoblade Chronicles 3 and possibly others as well.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3511
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
2026-03-02 02:51:50 +01:00
xbzk
a8fc994c0b
pr3655 gradientbordercardview fix (#3658)
This complements PR3655, where a change in theme setting caused gradientbordercardview theme color setting mismatch.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3658
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
2026-03-01 23:21:26 +01:00
lizzie
2991bd18ef
[vk] Enable VK_IMG_filter_cubic on powervr (#3643)
in powervr, the enum for cubic filtering is an alias for an ext, however in true powervr fashion they want you to use their stupid VK_IMG

and knowing powevr it probably bugs out if you dont

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3643
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-03-01 01:53:15 +01:00
lizzie
281d7a468e
[ports] Remove ENABLE_OPENSSL and mbedtls nixos remnants (#3638)
shell.nix still had references to mbedtls

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3638
Reviewed-by: MaranBr <maranbr@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-03-01 01:53:04 +01:00
PavelBARABANOV
12f89745be
[android] add IntSetting for static theme and set green as default (#3655)
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3655
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
Co-committed-by: PavelBARABANOV <pavelbarabanov94@gmail.com>
2026-02-28 19:34:55 +01:00
xXJSONDeruloXx
7de5eb6884
[android] fix persist manual game pause after android sleep/wake (#3651)
if user invokes the "pause game" option from the menu while in game, as expected this suspends the process till user manually hits resume.. except for one case: Android sleep/wake lifecycle.

If user manually pauses a running game, then sleeps their device, then wakes their device; the game will self-resume without user pressing "resume game".

Expected behavior IMO is that if user left the game process in manually paused state, app should respect this and persist the pause on system wake, so that user may manually press "resume game" to unfreeze the process.

Simple fix is to have a few params for user initiated pause and resume, and update the pause and run methods to handle as described above.

Please let me know if there is a cleaner way to implement!

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3651
Reviewed-by: crueter <crueter@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: xXJSONDeruloXx <danielhimebauch@gmail.com>
Co-committed-by: xXJSONDeruloXx <danielhimebauch@gmail.com>
2026-02-28 16:05:06 +01:00
124 changed files with 5438 additions and 1757 deletions

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

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

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

File diff suppressed because it is too large Load diff

View file

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

1
.gitignore vendored
View file

@ -63,3 +63,4 @@ artifacts
*.AppImage* *.AppImage*
/install* /install*
vulkansdk*.exe vulkansdk*.exe
*.tar.zst

View file

@ -0,0 +1,31 @@
diff --git a/libs/process/src/shell.cpp b/libs/process/src/shell.cpp
index bf4bbfd8..bc4aae89 100644
--- a/libs/process/src/shell.cpp
+++ b/libs/process/src/shell.cpp
@@ -19,7 +19,7 @@
#if defined(BOOST_PROCESS_V2_WINDOWS)
#include <windows.h>
#include <shellapi.h>
-#elif !defined(__OpenBSD__) && !defined(__ANDROID__)
+#elif !defined(__OpenBSD__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
#include <wordexp.h>
#endif
@@ -30,7 +30,7 @@ BOOST_PROCESS_V2_DECL const error_category& get_shell_category()
{
return system_category();
}
-#elif !defined(__OpenBSD__) && !defined(__ANDROID__)
+#elif !defined(__OpenBSD__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
struct shell_category_t final : public error_category
{
@@ -99,7 +99,7 @@ auto shell::args() const-> args_type
return input_.c_str();
}
-#elif !defined(__OpenBSD__) && !defined(__ANDROID__)
+#elif !defined(__OpenBSD__) && !defined(__ANDROID__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
void shell::parse_()
{

View file

@ -0,0 +1,33 @@
diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt
index 7ab2319..333e325 100644
--- a/source/CMakeLists.txt
+++ b/source/CMakeLists.txt
@@ -151,9 +151,11 @@ add_custom_command(OUTPUT ${SPIRV_TOOLS_BUILD_VERSION_INC}
COMMENT "Update build-version.inc in the SPIRV-Tools build directory (if necessary).")
# Convenience target for standalone generation of the build-version.inc file.
# This is not required for any dependence chain.
-add_custom_target(spirv-tools-build-version
- DEPENDS ${SPIRV_TOOLS_BUILD_VERSION_INC})
-set_property(TARGET spirv-tools-build-version PROPERTY FOLDER "SPIRV-Tools build")
+if (NOT IOS)
+ add_custom_target(spirv-tools-build-version
+ DEPENDS ${SPIRV_TOOLS_BUILD_VERSION_INC})
+ set_property(TARGET spirv-tools-build-version PROPERTY FOLDER "SPIRV-Tools build")
+endif()
list(APPEND PCH_DEPENDS
${CORE_TABLES_HEADER_INC_FILE}
@@ -338,8 +340,11 @@ function(spirv_tools_default_target_options target)
)
set_property(TARGET ${target} PROPERTY FOLDER "SPIRV-Tools libraries")
spvtools_check_symbol_exports(${target})
- add_dependencies(${target}
- spirv-tools-build-version core_tables extinst_tables)
+ if (IOS)
+ add_dependencies(${target} core_tables extinst_tables)
+ else ()
+ add_dependencies(${target} spirv-tools-build-version core_tables extinst_tables)
+ endif()
endfunction()
if (SPIRV_TOOLS_BUILD_SHARED)

View file

@ -212,7 +212,7 @@ option(YUZU_LEGACY "Apply patches that improve compatibility with older GPUs (e.
option(NIGHTLY_BUILD "Use Nightly qualifiers in the update checker and build metadata" OFF) option(NIGHTLY_BUILD "Use Nightly qualifiers in the update checker and build metadata" OFF)
cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID" OFF) cmake_dependent_option(YUZU_ROOM "Enable dedicated room functionality" ON "NOT ANDROID AND NOT IOS" OFF)
cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF) cmake_dependent_option(YUZU_ROOM_STANDALONE "Enable standalone room executable" ON "YUZU_ROOM" OFF)
cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "NOT ANDROID" OFF) cmake_dependent_option(YUZU_CMD "Compile the eden-cli executable" ON "NOT ANDROID" OFF)
@ -283,7 +283,7 @@ if (YUZU_ROOM)
add_compile_definitions(YUZU_ROOM) add_compile_definitions(YUZU_ROOM)
endif() endif()
if ((ANDROID OR APPLE OR UNIX) AND (NOT PLATFORM_LINUX OR ANDROID) AND NOT WIN32) if ((ANDROID OR APPLE OR UNIX OR IOS) AND (NOT PLATFORM_LINUX OR ANDROID) AND NOT WIN32)
if(CXX_APPLE OR CXX_CLANG) if(CXX_APPLE OR CXX_CLANG)
# libc++ has stop_token and jthread as experimental # libc++ has stop_token and jthread as experimental
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexperimental-library")
@ -487,7 +487,12 @@ if (APPLE)
# Umbrella framework for everything GUI-related # Umbrella framework for everything GUI-related
find_library(COCOA_LIBRARY Cocoa REQUIRED) find_library(COCOA_LIBRARY Cocoa REQUIRED)
find_library(IOKIT_LIBRARY IOKit REQUIRED) find_library(IOKIT_LIBRARY IOKit REQUIRED)
if (IOS)
find_library(OBJC_LIBRARY objc REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY} ${OBJC_LIBRARY})
else()
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY}) set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${IOKIT_LIBRARY} ${COREVIDEO_LIBRARY})
endif()
elseif (WIN32) elseif (WIN32)
# Target Windows 10 # Target Windows 10
add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00) add_compile_definitions(_WIN32_WINNT=0x0A00 WINVER=0x0A00)

View file

@ -61,6 +61,10 @@ See the [sign-up instructions](docs/SIGNUP.md) for information on registration.
Alternatively, if you wish to add translations, go to the [Eden project on Transifex](https://app.transifex.com/edenemu/eden-emulator) and review [the translations README](./dist/languages). Alternatively, if you wish to add translations, go to the [Eden project on Transifex](https://app.transifex.com/edenemu/eden-emulator) and review [the translations README](./dist/languages).
## Documentation
We have a user manual! See our [User Handbook](./docs/user/README.md).
## Building ## Building
See the [General Build Guide](docs/Build.md) See the [General Build Guide](docs/Build.md)
@ -69,7 +73,9 @@ For information on provided development tooling, see the [Tools directory](./too
## Download ## Download
You can download the latest releases from [here](https://github.com/eden-emulator/Releases/releases). You can download the latest releases from [here](https://git.eden-emu.dev/eden-emu/eden/releases).
Save us some bandwidth! We have [mirrors available](./docs/user/ThirdParty.md#mirrors) as well.
## Support ## Support

View file

@ -17,7 +17,8 @@
"version": "1.57", "version": "1.57",
"find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem", "find_args": "CONFIG OPTIONAL_COMPONENTS headers context system fiber filesystem",
"patches": [ "patches": [
"0001-clang-cl.patch" "0001-clang-cl.patch",
"0002-ios-fix.patch"
] ]
}, },
"fmt": { "fmt": {

View file

@ -4,6 +4,7 @@
- [Arch Linux](#arch-linux) - [Arch Linux](#arch-linux)
- [Gentoo Linux](#gentoo-linux) - [Gentoo Linux](#gentoo-linux)
- [macOS](#macos) - [macOS](#macos)
- [iOS](#ios)
- [Solaris](#solaris) - [Solaris](#solaris)
- [HaikuOS](#haikuos) - [HaikuOS](#haikuos)
- [OpenBSD](#openbsd) - [OpenBSD](#openbsd)
@ -31,6 +32,16 @@ If you're having issues with building, always consult that ebuild.
macOS is largely untested. Expect crashes, significant Vulkan issues, and other fun stuff. macOS is largely untested. Expect crashes, significant Vulkan issues, and other fun stuff.
## iOS
iOS has a dedicated build script, we **highly** recommend using that instead of doing anything else, we don't support any other configuration than the one present in said build script.
To build, it's simply as easy as doing
```sh
chmod +x .ci/ios/build.sh
.ci/ios/build.sh
```
## Solaris ## Solaris
Always consult [the OpenIndiana package list](https://pkg.openindiana.org/hipster/en/index.shtml) to cross-verify availability. Always consult [the OpenIndiana package list](https://pkg.openindiana.org/hipster/en/index.shtml) to cross-verify availability.

View file

@ -1,5 +1,7 @@
# Eden Build Documentation # Eden Build Documentation
Are you just a casual user? Take a look at our [User Handbook](./user) then!
This contains documentation created by developers. This contains build instructions, guidelines, instructions/layouts for [cool stuff we made](./CPMUtil), and more. This contains documentation created by developers. This contains build instructions, guidelines, instructions/layouts for [cool stuff we made](./CPMUtil), and more.
- **[General Build Instructions](Build.md)** - **[General Build Instructions](Build.md)**
@ -11,7 +13,6 @@ This contains documentation created by developers. This contains build instructi
- **[CPM - CMake Package Manager](./CPMUtil)** - **[CPM - CMake Package Manager](./CPMUtil)**
- **[Platform-Specific Caveats](Caveats.md)** - **[Platform-Specific Caveats](Caveats.md)**
- **[The NVIDIA SM86 (Maxwell) GPU](./NvidiaGpu.md)** - **[The NVIDIA SM86 (Maxwell) GPU](./NvidiaGpu.md)**
- **[User Handbook](./user)**
- **[Dynarmic](./dynarmic)** - **[Dynarmic](./dynarmic)**
- **[Cross compilation](./CrossCompile.md)** - **[Cross compilation](./CrossCompile.md)**
- **[Driver Bugs](./DriverBugs.md)** - **[Driver Bugs](./DriverBugs.md)**

View file

@ -1,100 +0,0 @@
# Importing Games into Steam with Steam Rom Manager
Use this when you want to import your games inside Eden into Steam to launch with artwork from Steam Game Mode without needing to launch Eden first.
**Click [Here](https://evilperson1337.notion.site/Importing-Games-into-Steam-with-Steam-Rom-Manager-2b757c2edaf680d7a491c92b138f1fcc) for a version of this guide with images & visual elements.**
---
### Pre-Requisites
- Steam Deck Set up and Configured
- Eden set up and Configured
- Internet Access
---
## Steps
1. Press the **STEAM** button and then go to *Power → Switch to Desktop* to enter the Desktop mode.
1. Install ***Steam ROM Manager***, there are 2 ways you can accomplish this, either manually or through [*EmuDeck*](https://www.emudeck.com/#downloads).
---
### Manual Installation
1. Open the *Discover Store* and search for *Steam ROM Manager.*
2. Select the **Install** button to install the program.
---
### Installing Through *EmuDeck*
<aside>
***NOTE***: This assumes you have already set up EmuDeck, if not - just run through the guided installation and select *Steam ROM Manager* as one of the options.
</aside>
1. Open **EmuDeck**, then navigate to *Manage Emulators.*
2. Scroll down to the bottom of the page to the *Manage your Tools & Frontends* section. Click **Steam ROM Manager**.
3. Click the **Install** button on the right hand side to install it.
---
2. Open the Start Menu and Launch ***Steam ROM Manager***
1. The program will now launch and show you a window with parsers.
<aside>
***TIP***: Your layout may look different depending on how you installed *Steam ROM Manager*. You may need to go to **Settings → Theme** and change it to *Classic* to follow along.
</aside>
2. Switch off all Parsers by hitting the *Toggle Parsers* switch.
3. Scroll down the list on the left-hand side and look for a parser called *Nintendo Switch - Eden* and switch it on. This parser may not exist depending on how you installed *Steam ROM Manager* (EmuDeck creates it for you). Follow these steps to create it if it is missing.
---
### Creating the Eden Parser
1. Select Create Parser and in the *Community Presets* option look for **Nintendo Switch - Yuzu**.
2. Change the **Parser title** from *Nintendo Switch - Yuzu* to *Nintendo Switch - Eden.*
3. Hit the **Browse** option under the *ROMs directory* section. Select the directory containing your Switch ROMs.
4. Under *Steam collections*, you can add a Steam category name. This just organizes the games under a common category in your Steam Library, this is optional but recommended.
5. Scroll down slightly to the **Executable Configuration → Executable**, select **Browse** and select the Eden AppImage.
6. Leave everything else the same and hit **Save** to save the parser.
---
4. Click the Eden parser to view the options on the right, select **Test** at the bottom of the screen to ensure that *Steam ROM Manager* detects your games correctly.
1. *Steam ROM Manager* will start to scan the specified ROMs directory and match them to games. Look over the results to ensure they are accurate. If you do not see any entries - check your parsers ROMs directory field.
1. When you are happy with the results, click the **Add Games****Parse** to start the actual Parsing.
1. The program will now identify the games and pull artwork from [*SteamGridDB*](https://www.steamgriddb.com/).
2. Review the game matches and ensure everything is there.
---
### Correcting a Mismatch
If the game is not identified correctly, you may need to tell *Steam ROM Manager* what the game is manually.
1. Hover over the game card and click the magnifying glass icon.
2. Search for the game on the *Search SteamGridDB* section and scroll through the results, selecting the one you want.
3. Ensure the *Name* and *Game ID* update in the **Per-App Exceptions** and press **Save and close**. The game should now update.
---
### Excluding Matches
You may want to tell Steam ROM Manager to ignore some files (updates/DLC/etc.) that it finds in the directory. This is how you do so.
1. Hit the **Exclude Games** button in the bottom right.
2. Deselect the game you want to exclude, the poster artwork should go dim and the **Number Excluded** number should increment up. Repeat with any other exclusions you want to add.
3. Hit **Save Excludes** when you are happy with your selections.
---
3. When you are happy with the results, select **Save to Steam** to save the results.
1. The program will now start writing the entries into the Steam Library. You should get pop up notifications of the progress, but you can monitor the progress by selecting the **Log** on the left-hand side if needed.
2. Restart Steam to have the changes take effect. Check your library to ensure that your games are there, in a category if you defined one in the parser.
3. Try to launch a game and ensure everything is working. You are now good to go.

11
docs/user/CFW.md Normal file
View file

@ -0,0 +1,11 @@
# User Handbook - Custom Firmware (CFW)
At the moment of writing, we do not support CFW such as Atmosphere, due to:
- Lacking the required LLE emulation capabilities to properly emulate the full firmware.
- Lack of implementation on some of the key internals.
- Nobody has bothered to do it (PRs always welcome!)
We do however, maintain HLE compatibility with the former mentioned CFW, applications that require Atmosphere to run will run fine in the emulator without any adjustments.
If they don't run - then that's a bug!

View file

@ -1,5 +1,7 @@
# User Handbook - Graphics # User Handbook - Graphics
Graphical enhancements and visual quality improvments. This doesn't cover texture mods.
## Visual Enhancements ## Visual Enhancements
### Anti-aliasing ### Anti-aliasing
@ -89,7 +91,7 @@ The OpenGL backend would invoke behaviour that would result in swarst/LLVMpipe w
### HaikuOS compatibility ### HaikuOS compatibility
HaikuOS bundles a Mesa library that doesn't support full core OpenGL 4.6 (required by the emulator). This leads to HaikuOS being one of the few computer platforms where Vulkan is the only available option for users. If OpenGL is desired, Mesa has to be built manually from source. For debugging purpouses `lavapipe` is recommended over the GPU driver; there is in-kernel support for NVIDIA cards through. HaikuOS bundles a Mesa library that doesn't support full core OpenGL 4.6 (required by the emulator). This leads to HaikuOS being one of the few computer platforms where Vulkan is the only available option for users. If OpenGL is desired, Mesa has to be built manually from source. For debugging purposes `lavapipe` is recommended over the GPU driver; there is in-kernel support for NVIDIA cards through.
### Fixes for Windows 10 and above having "Device loss" ### Fixes for Windows 10 and above having "Device loss"

206
docs/user/Mods.md Normal file
View file

@ -0,0 +1,206 @@
# User Handbook - Installing Mods
## General Notes
**Note:** When installing a mod, always read the mod's installation instructions.
This is especially important if a mod uses a framework such as **ARCropolis**, **Skyline**, or **Atmosphere plugins**. In those cases, follow the framework's instructions instead of using Eden's normal mod folder.
For example, **Super Smash Bros. Ultimate** uses such a framework. See the related section below for details.
---
# Installing Mods for Most Games
1. Right click a game in the game list.
2. Click **"Open Mod Data Location"**.
3. Extract the mod into that folder.
Each mod should be placed inside **its own subfolder**.
---
# Enabling or Disabling Mods
1. Right click the game in the game list.
2. Click **Configure Game**.
3. In the **Add-Ons** tab, enable or disable mods, updates, and DLC by ticking or unticking their boxes.
---
# Important Note About SD Card Paths
Some mods are designed for real Nintendo Switch consoles and refer to the **SD card root**.
The emulated SD card is located at:
```
%AppData%\eden\sdmc
```
Example:
```
Switch instruction: sd:/ultimate/mods
Eden equivalent: sdmc/ultimate/mods
```
---
# Framework-Based Mods (Super Smash Bros. Ultimate)
Some games require external mod frameworks instead of the built-in mod loader.
The most common example is **Super Smash Bros. Ultimate**.
These mods are installed directly to the **emulated SD card**, not the normal Eden mod folder.
---
# Installing the ARCropolis Modding Framework
**Note:** Some mod packs bundle ARCropolis with their installer (for example, Smash Ult-S).
---
## 1. Download ARCropolis
Download the latest release:
https://github.com/Raytwo/ARCropolis/releases/
---
## 2. Install ARCropolis
Extract the **`atmosphere`** folder into:
```
%AppData%\eden\sdmc
```
This is the **emulated SD card directory**.
Verify installation by checking that the following file exists:
```
sdmc\atmosphere\contents\01006A800016E000\romfs\skyline\plugins\libarcropolis.nro
```
---
## 3. Download Skyline
Download the latest Skyline release:
https://github.com/skyline-dev/skyline/releases
Skyline used to be bundled with ARCropolis but is now distributed separately to avoid compatibility issues caused by outdated bundled versions.
---
## 4. Install Skyline
Extract the **`exefs`** folder into:
```
sdmc\atmosphere\contents\01006A800016E000
```
The `exefs` folder should be **next to the `romfs` folder**.
Verify installation by checking that the following file exists:
```
%AppData%\eden\sdmc\atmosphere\contents\01006A800016E000\exefs\subsdk9
```
---
## 5. Launch the Game Once
Start the game and make sure you see the **ARCropolis version text on the title screen**.
This will also create the folders required for installing mods.
---
## 6. Install Smash Ultimate Mods
Install mods inside:
```
sdmc\ultimate\mods
```
Each mod must be placed inside **its own subfolder**.
Example:
```
sdmc\ultimate\mods\ExampleMod
```
---
# Troubleshooting
## ARCropolis text does not appear on startup
Check the following:
- `libarcropolis.nro` exists in:
```
sdmc\atmosphere\contents\01006A800016E000\romfs\skyline\plugins
```
- `subsdk9` exists in:
```
sdmc\atmosphere\contents\01006A800016E000\exefs
```
- Files were extracted to:
```
%AppData%\eden\sdmc
```
---
## Mods are not loading
Make sure mods are installed inside:
```
sdmc\ultimate\mods
```
Each mod must have its **own subfolder**.
Correct example:
```
sdmc\ultimate\mods\ExampleMod
```
Incorrect example:
```
sdmc\ultimate\mods\ExampleMod\ExampleMod
```
---
## Installing mods in the wrong folder
ARCropolis mods **do not go in Eden's normal mod folder**.
Do **not** install Smash mods here:
```
user\load\01006A800016E000
```
That folder is only used for traditional **RomFS mods**, not ARCropolis.

View file

@ -4,10 +4,14 @@ The "FAQ".
This handbook is primarily aimed at the end-user - baking useful knowledge for enhancing their emulation experience. This handbook is primarily aimed at the end-user - baking useful knowledge for enhancing their emulation experience.
A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/README.md).
## Basics ## Basics
- **[The Basics](Basics.md)** - **[The Basics](Basics.md)**
- **[Quickstart](./QuickStart.md)** - **[Quickstart](./QuickStart.md)**
- **[Settings](./Settings.md)**
- **[Installing Mods](./Mods.md)**
- **[Run On macOS](./RunOnMacOS.md)** - **[Run On macOS](./RunOnMacOS.md)**
- **[Audio](Audio.md)** - **[Audio](Audio.md)**
- **[Graphics](Graphics.md)** - **[Graphics](Graphics.md)**
@ -17,22 +21,29 @@ This handbook is primarily aimed at the end-user - baking useful knowledge for e
- **[Using Amiibo](./UsingAmiibo.md)** - **[Using Amiibo](./UsingAmiibo.md)**
- **[Using Cheats](./UsingCheats.md)** - **[Using Cheats](./UsingCheats.md)**
- **[Importing Saves](./ImportingSaves.md)** - **[Importing Saves](./ImportingSaves.md)**
- **[Add Eden to Steam ROM Manager](./AddEdenToSRM.md)**
- **[Add Games to Steam ROM Manager](./AddGamesToSRM.md)**
- **[Installing Atmosphere Mods](./InstallingAtmosphereMods.md)** - **[Installing Atmosphere Mods](./InstallingAtmosphereMods.md)**
- **[Installing Updates & DLCs](./InstallingUpdatesDLC.md)** - **[Installing Updates & DLCs](./InstallingUpdatesDLC.md)**
- **[Controller Profiles](./ControllerProfiles.md)** - **[Controller Profiles](./ControllerProfiles.md)**
- **[Alter Date & Time](./AlterDateTime.md)** - **[Alter Date & Time](./AlterDateTime.md)**
## 3rd-party Integration
- **[Configuring Steam ROM Manager](./SteamROM.md)**
- **[Server hosting](ServerHosting.md)**
- **[Syncthing Guide](./SyncthingGuide.md)**
- **[Third Party](./ThirdParty.md)**
- **[Obtainium](./ThirdParty.md#configuring-obtainium)**
- **[ES-DE](./ThirdParty.md#configuring-es-de)**
- **[Mirrors](./ThirdParty.md#mirrors)**
## Advanced ## Advanced
- **[Custom Firmware](./CFW.md)**
- **[How To Access Logs](./HowToAccessLogs.md)** - **[How To Access Logs](./HowToAccessLogs.md)**
- **[Gyro Controls](./GyroControls.md)** - **[Gyro Controls](./GyroControls.md)**
- **[Platforms and Architectures](Architectures.md)** - **[Platforms and Architectures](Architectures.md)**
- **[Server hosting](ServerHosting.md)**
- **[Command Line](CommandLine.md)** - **[Command Line](CommandLine.md)**
- **[Native Application Development](Native.md)** - **[Native Application Development](Native.md)**
- **[Adding Boolean Settings Toggles](AddingBooleanToggles.md)** - **[Adding Boolean Settings Toggles](AddingBooleanToggles.md)**
- **[Adding Debug Knobs](./AddingDebugKnobs.md)** - **[Adding Debug Knobs](./AddingDebugKnobs.md)**
- **[Syncthing Guide](./SyncthingGuide.md)**
- **[Testing](Testing.md)** - **[Testing](Testing.md)**

54
docs/user/Settings.md Normal file
View file

@ -0,0 +1,54 @@
# User Handbook - Settings
As the emulator continues to grow, so does the number of settings that come and go.
Most of the development adds new settings that enhance performance/compatibility, only to be removed later in newer versions due to newfound discoveries or because they were "a hacky workaround".
As such, this guide will NOT mention those kind of settings, we'd rather mention settings which have a long shelf time (i.e won't get removed in future releases) and are likely to be unchanged.
Some of the options are self explainatory, and they do exactly what they say they do (i.e "Pause when not in focus"); such options will be also skipped due to triviality.
## Foreword
Before touching the settings, please see the game boots with stock options. We try our best to ensure users can boot any game using the default settings. If they don't work, then you may try fiddling with options - but please, first use stock options.
## General
- `General/Force X11 as Graphics Backend`: Wayland on *NIX has prominent issues that are unlikely to be resolved; the kind that are "not our fault, it's Wayland issue", this "temporary" hack forces X11 as the backend, regardless of the desktop manager's default.
- `General/Enable Gamemode`: This only does anything when you have Feral Interactive's Gamemode library installed somewhere, if you do, this will help boost FPS by telling the OS to explicitly prioritize *this* application for "gaming" - only for *NIX systems.
- `Hotkeys`: Deceptively to remove a hotkey you must right click and a menu will appear to remove that specific hotkey.
- `UI/Language`: Changes language *of the interface* NOT the emulated program!
- `Debug/Enable Auto Stub`: May help to "fix" some games by just lying and saying that everything they do returns "success" instead of outright crashing for any function/service that is NOT implemented.
- `Debug/Show log in console`: Does as said, note that the program may need to be reopened (Windows) for changes to take effect.
- `Debug/Flush log output`: Classically, every write to the log is "buffered", that is, changes aren't written to the disk UNTIL the program has decided it is time to write, until then it keeps data in a buffer which resides on RAM. If the program crashes, the OS will automatically discard said buffer (any RAM associated with a dead process is automatically discarded/reused for some other purpose); this means critical data may not be logged to the disk on time, which may lead to missing log lines. Use this if you're wanting to remove that factor when debugging, sometimes a hard crash may "eat" some of the log lines IF this option isn't enabled.
- `Debug/Disable Macro HLE:` The emulator has HLE emulation of macro programs for Maxwell, this means that some details are purpousefully skipped; this option forces all macro programs to be ran without skipping anything.
## System
- `System/RNG Seed`: Set to 0 (and uncheck) to disable ASLR systemwide (this makes mods like CTGP to stop working); by default it enables ASLR to replicate console behaviour.
- `Network/Enable Airplane Mode`: Enable this if a game is crashing before loading AND the logs mention anything related to "web" or "internet" services.
## CPU
- `CPU/Virtual table bouncing`: Some games have the tendency to crash on loading due to an indirect bad jump (Pokemon ZA being the worst offender); this option lies to the game and tells it to just pretend it never executed a given function. This is fine for most casual users, but developers of switch applications **must** disable this. This temporary "hack" should hopefully be gone in 6-7 months from now on.
- `Fastmem`, aka. `CPU/Enable Host MMU`: Enables "fastmem"; a detailed description of fastmem can be found [here](../dynarmic/Design.md#fast-memory-fastmem).
- `CPU/Unsafe FMA`: Enables deliberate innacurate FMA behaviour which may affect how FMA returns any given operation - this may introduce tiny floating point errors which can cascade in sensitive code (i.e FFmpeg).
- `CPU/Faster FRSQRTE and FRECPE`: Introduces accuracy errors on square root and reciprocals in exchange for less checks - this introduces inaccuracies with some cases but it's mostly safe.
- `CPU/Faster ASIMD Instructions`: Skips rounding mode checks for ARM ASIMD instructions - this means some code dpeending on these rounding modes may misbehave.
- `CPU/Disable address space checks`: Before each memory access, the emulator checks the address is in range, if not it faults; this option makes it so the emulator skips the check entirely (which may be expensive for a myriad of reasons). However at the same time this allows the guest program to "break out" of the emulation context by writing to arbitrary addresses.
- `CPU/Ignore global monitor`: This relies on a quirk present on x86 to avoid the ARM global monitor emulation, this may increase performance in mutex-heavy contexts (i.e games waiting for next frames or such); but also can cause deadlocks and fun to debug issues.
It is important to note the majority of precision-reducing instructions do not benefit cases where they are not used, which means the performance gains will vary per game.
# Graphics
See also [an extended breakdown of some options](./Graphics.md).
- `Extras/Extended Dynamic State` and `Extras/Vertex Input Dynamic State`: These Vulkan extensions essentially allow you to reuse the same pipeline but just change the state between calls (so called "dynamic state"); the "extended" levels signifies how much state can be placed on this "dynamic" range, for example the amount of depth culling to use can be placed on the dynamic state, avoiding costly reloads and flushes. While this by itself is a fine option, SOME vendors (notably PowerVR and Mali) have problems with anything related to EDS3. EDS3 contains EDS2, and EDS2 contains EDS1. Essentially this means more extended data the driver has to keep track of, at the benefit of avoiding costly flushes.
- `Advanced/Use persistent cache`: This saves compiled shaders onto the disk, independent of any driver's own disk saved shaders (yes, some drivers, notably NVIDIA, save a secondary shader cache onto disk) - disable this only if you're debugging or working on the GPU backend. This option is meant to massively help to reduce shader stutters (after playing for one session that compiles them).
- `Advanced/Use Vulkan pipeline cache`: This is NOT the same as `Use persistent cache`; it's a separate flag that tells the Vulkan backend to create pipeline caches, which are a detail that can be used to massively improve performance and remove pipeline creation overhead. This is a Vulkan feature.
## Controls
Most of the controls should work out of the box. If not, please use a joystick calibrator to ensure it's not an issue with your own controller, for example:
- https://github.com/dkosmari/calibrate-joystick

View file

@ -1,4 +1,6 @@
# Importing Eden into Steam with Steam Rom Manager # User Handbook - Configuring Steam ROM Manager
## Importing Eden into Steam with Steam Rom Manager
Use this when you want to import the Eden AppImage into your Steam Library along with artwork using *Steam ROM Manager.* Use this when you want to import the Eden AppImage into your Steam Library along with artwork using *Steam ROM Manager.*
@ -6,7 +8,7 @@ Use this when you want to import the Eden AppImage into your Steam Library along
--- ---
### Pre-Requisites #### Pre-Requisites
- Eden set up and configured - Eden set up and configured
- Internet Connection - Internet Connection
@ -14,9 +16,9 @@ Use this when you want to import the Eden AppImage into your Steam Library along
--- ---
## Steps ### Steps
### Initial Setup #### Initial Setup
1. Press the **STEAM** button and then go to *Power → Switch to Desktop* to enter the Desktop mode. 1. Press the **STEAM** button and then go to *Power → Switch to Desktop* to enter the Desktop mode.
@ -24,14 +26,14 @@ Use this when you want to import the Eden AppImage into your Steam Library along
--- ---
### Manual Installation #### Manual Installation
1. Open the *Discover Store* and search for *Steam ROM Manager.* 1. Open the *Discover Store* and search for *Steam ROM Manager.*
2. Select the **Install** button to install the program. 2. Select the **Install** button to install the program.
--- ---
### Installing Through *EmuDeck* #### Installing Through *EmuDeck*
<aside> <aside>
@ -45,9 +47,9 @@ Use this when you want to import the Eden AppImage into your Steam Library along
--- ---
### Adding Eden into *Steam ROM Manager* #### Adding Eden into *Steam ROM Manager*
### EmuDeck Users #### EmuDeck Users
EmuDeck will automatically create an *Emulators - Emulators* parser for ***Steam ROM Manager*** that uses shell scripts to launch them. We will follow this convention. EmuDeck will automatically create an *Emulators - Emulators* parser for ***Steam ROM Manager*** that uses shell scripts to launch them. We will follow this convention.
@ -87,7 +89,7 @@ EmuDeck will automatically create an *Emulators - Emulators* parser for ***Steam
--- ---
### Non-EmuDeck Users #### Non-EmuDeck Users
We will need to create a new parser for the Emulators. Unlike with the EmuDeck model, we will have the parser look for AppImages. We will need to create a new parser for the Emulators. Unlike with the EmuDeck model, we will have the parser look for AppImages.
@ -126,7 +128,7 @@ We will need to create a new parser for the Emulators. Unlike with the EmuDeck
--- ---
### Adding Eden to Steam #### Adding Eden to Steam
Now that we have the parser or shell script created, we can actually add it to Steam. Now that we have the parser or shell script created, we can actually add it to Steam.
@ -137,7 +139,7 @@ Now that we have the parser or shell script created, we can actually add it to S
--- ---
### Correcting a Mismatch #### Correcting a Mismatch
If the emulator is not identified correctly, you may need to tell *Steam ROM Manager* what the game is manually. If the emulator is not identified correctly, you may need to tell *Steam ROM Manager* what the game is manually.
@ -147,7 +149,7 @@ Now that we have the parser or shell script created, we can actually add it to S
--- ---
### Excluding Matches #### Excluding Matches
You may want to tell Steam ROM Manager to ignore some files that it finds in the directory. This is how you do so. You may want to tell Steam ROM Manager to ignore some files that it finds in the directory. This is how you do so.
@ -160,3 +162,104 @@ Now that we have the parser or shell script created, we can actually add it to S
5. The program will now start writing the entries into the Steam Library. You should get pop up notifications of the progress, but you can monitor the progress by selecting the **Log** on the left-hand side if needed. 5. The program will now start writing the entries into the Steam Library. You should get pop up notifications of the progress, but you can monitor the progress by selecting the **Log** on the left-hand side if needed.
6. Restart Steam to have the changes take effect. Check your library to ensure that your games are there, in a category if you defined one in the parser. 6. Restart Steam to have the changes take effect. Check your library to ensure that your games are there, in a category if you defined one in the parser.
7. Try to launch the Emulator from Steam and ensure everything is working. You are now good to go. 7. Try to launch the Emulator from Steam and ensure everything is working. You are now good to go.
## Importing Games into Steam with Steam Rom Manager
Use this when you want to import your games inside Eden into Steam to launch with artwork from Steam Game Mode without needing to launch Eden first.
**Click [Here](https://evilperson1337.notion.site/Importing-Games-into-Steam-with-Steam-Rom-Manager-2b757c2edaf680d7a491c92b138f1fcc) for a version of this guide with images & visual elements.**
---
#### Pre-Requisites
- Steam Deck Set up and Configured
- Eden set up and Configured
- Internet Access
---
### Steps
1. Press the **STEAM** button and then go to *Power → Switch to Desktop* to enter the Desktop mode.
1. Install ***Steam ROM Manager***, there are 2 ways you can accomplish this, either manually or through [*EmuDeck*](https://www.emudeck.com/#downloads).
---
#### Manual Installation
1. Open the *Discover Store* and search for *Steam ROM Manager.*
2. Select the **Install** button to install the program.
---
#### Installing Through *EmuDeck*
<aside>
***NOTE***: This assumes you have already set up EmuDeck, if not - just run through the guided installation and select *Steam ROM Manager* as one of the options.
</aside>
1. Open **EmuDeck**, then navigate to *Manage Emulators.*
2. Scroll down to the bottom of the page to the *Manage your Tools & Frontends* section. Click **Steam ROM Manager**.
3. Click the **Install** button on the right hand side to install it.
---
2. Open the Start Menu and Launch ***Steam ROM Manager***
1. The program will now launch and show you a window with parsers.
<aside>
***TIP***: Your layout may look different depending on how you installed *Steam ROM Manager*. You may need to go to **Settings → Theme** and change it to *Classic* to follow along.
</aside>
2. Switch off all Parsers by hitting the *Toggle Parsers* switch.
3. Scroll down the list on the left-hand side and look for a parser called *Nintendo Switch - Eden* and switch it on. This parser may not exist depending on how you installed *Steam ROM Manager* (EmuDeck creates it for you). Follow these steps to create it if it is missing.
---
#### Creating the Eden Parser
1. Select Create Parser and in the *Community Presets* option look for **Nintendo Switch - Yuzu**.
2. Change the **Parser title** from *Nintendo Switch - Yuzu* to *Nintendo Switch - Eden.*
3. Hit the **Browse** option under the *ROMs directory* section. Select the directory containing your Switch ROMs.
4. Under *Steam collections*, you can add a Steam category name. This just organizes the games under a common category in your Steam Library, this is optional but recommended.
5. Scroll down slightly to the **Executable Configuration → Executable**, select **Browse** and select the Eden AppImage.
6. Leave everything else the same and hit **Save** to save the parser.
---
4. Click the Eden parser to view the options on the right, select **Test** at the bottom of the screen to ensure that *Steam ROM Manager* detects your games correctly.
1. *Steam ROM Manager* will start to scan the specified ROMs directory and match them to games. Look over the results to ensure they are accurate. If you do not see any entries - check your parsers ROMs directory field.
1. When you are happy with the results, click the **Add Games****Parse** to start the actual Parsing.
1. The program will now identify the games and pull artwork from [*SteamGridDB*](https://www.steamgriddb.com/).
2. Review the game matches and ensure everything is there.
---
#### Correcting a Mismatch
If the game is not identified correctly, you may need to tell *Steam ROM Manager* what the game is manually.
1. Hover over the game card and click the magnifying glass icon.
2. Search for the game on the *Search SteamGridDB* section and scroll through the results, selecting the one you want.
3. Ensure the *Name* and *Game ID* update in the **Per-App Exceptions** and press **Save and close**. The game should now update.
---
#### Excluding Matches
You may want to tell Steam ROM Manager to ignore some files (updates/DLC/etc.) that it finds in the directory. This is how you do so.
1. Hit the **Exclude Games** button in the bottom right.
2. Deselect the game you want to exclude, the poster artwork should go dim and the **Number Excluded** number should increment up. Repeat with any other exclusions you want to add.
3. Hit **Save Excludes** when you are happy with your selections.
---
3. When you are happy with the results, select **Save to Steam** to save the results.
1. The program will now start writing the entries into the Steam Library. You should get pop up notifications of the progress, but you can monitor the progress by selecting the **Log** on the left-hand side if needed.
2. Restart Steam to have the changes take effect. Check your library to ensure that your games are there, in a category if you defined one in the parser.
3. Try to launch a game and ensure everything is working. You are now good to go.

View file

@ -7,3 +7,62 @@ While most of the links mentioned in this guide are relatively "safe"; we urge u
- [Nightly Eden builds](https://github.com/pflyly/eden-nightly) - [Nightly Eden builds](https://github.com/pflyly/eden-nightly)
- [NixOS Eden Flake](https://github.com/Grantimatter/eden-flake) - [NixOS Eden Flake](https://github.com/Grantimatter/eden-flake)
- [ES-DE Frontend Support](https://github.com/GlazedBelmont/es-de-android-custom-systems) - [ES-DE Frontend Support](https://github.com/GlazedBelmont/es-de-android-custom-systems)
## Mirrors
The main origin repository is always at https://git.eden-emu.dev/eden-emu/eden.
- https://github.com/eden-emulator/mirror
- https://git.crueter.xyz/mirror/eden
- https://collective.taymaerz.de/eden/eden
Other mirrors obviously exist on the internet, but we can't guarantee their reliability and/or availability.
If you're someone wanting to make a mirror, simply setup forgejo and automatically mirror from the origin repository. Or you could mirror a mirror to save us bandwidth... your choice!
## Configuring Obtainium
Very nice handy app, here's a quick rundown how to configure:
1. Copy the URL: https://git.eden-emu.dev/eden-emu/eden/ (or one of your favourite mirrors)
2. Open Obtainium and tap `Add App`.
3. Paste the URL into the `App Source URL` field.
4. Override Source: Look for the `Override Source` dropdown menu and select `Forgejo (Codeberg)`.
5. Click `Add:` Obtainium should now be able to parse the releases and find the APK files.
Note: Even though the site isn't Codeberg, it uses the same Forgejo/Gitea backend, and this setting tells Obtainium how to read the release data.
## Configuring ES-DE
### Method 1
1. Download ZIP from [here](https://github.com/GlazedBelmont/es-de-android-custom-systems)
2. Unzip the file and extract `es_systems.xml` and `es_find_rules.xml` to `\Odin2\Internal shared storage\ES-DE\custom_systems`.
3. Press `Start -> Other Settings -> Alternative Emulators` and set it to Eden (Standalone).
### Method 2
1. Navigate to `\Odin2\Internal shared storage\ES-DE\custom_systems`.
2. Add this to your `es_find_rules.xml`:
```xml
<!-- Standard aka. normal release -->
<emulator name="EDEN">
<rule type="androidpackage">
<entry>dev.eden.eden_emulator/org.yuzu.yuzu_emu.activities.EmulationActivity</entry>
</rule>
</emulator>
<!-- Optimized -->
<emulator name="EDEN">
<rule type="androidpackage">
<entry>com.miHoYo.Yuanshen/org.yuzu.yuzu_emu.activities.EmulationActivity</entry>
</rule>
</emulator>
```
3. Add this line of text to your `es_systems.xml` underneath where the rest of your switch system entries are:
```xml
<command label="Eden (Standalone)">%EMULATOR_EDEN% %ACTION%=android.nfc.action.TECH_DISCOVERED %DATA%=%ROMPROVIDER%</command>
```

View file

@ -38,13 +38,18 @@ This file is based off of Yuzu and Dynarmic.
if (CMAKE_OSX_ARCHITECTURES) if (CMAKE_OSX_ARCHITECTURES)
set(MULTIARCH_BUILD 1) set(MULTIARCH_BUILD 1)
set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}") set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}")
if (IOS)
# TODO: Right... the toolchain file won't properly accomodate OSX_ARCHITECTURE
# they aren't defining it as a list properly I assume?
set(ARCHITECTURE_arm64 1 PARENT_SCOPE)
add_definitions(-DARCHITECTURE_arm64=1)
else ()
# hope and pray the architecture names match # hope and pray the architecture names match
foreach(ARCH IN ${CMAKE_OSX_ARCHITECTURES}) foreach(ARCH IN ${CMAKE_OSX_ARCHITECTURES})
set(ARCHITECTURE_${ARCH} 1 PARENT_SCOPE) set(ARCHITECTURE_${ARCH} 1 PARENT_SCOPE)
add_definitions(-DARCHITECTURE_${ARCH}=1) add_definitions(-DARCHITECTURE_${ARCH}=1)
endforeach() endforeach()
endif()
return() return()
endif() endif()

View file

@ -51,6 +51,12 @@ elseif (CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
set(CXX_APPLE ON) set(CXX_APPLE ON)
endif() endif()
# This fixes some quirks with xcrun or weird iOS toolchain cmake files
if (IOS)
unset(CXX_CLANG)
set(CXX_APPLE ON)
endif()
# https://gitlab.kitware.com/cmake/cmake/-/merge_requests/11112 # https://gitlab.kitware.com/cmake/cmake/-/merge_requests/11112
# This works totally fine on MinGW64, but not CLANG{,ARM}64 # This works totally fine on MinGW64, but not CLANG{,ARM}64
if(MINGW AND CXX_CLANG) if(MINGW AND CXX_CLANG)

View file

@ -110,7 +110,8 @@
], ],
"patches": [ "patches": [
"0001-netbsd-fix.patch", "0001-netbsd-fix.patch",
"0002-allow-static-only.patch" "0002-allow-static-only.patch",
"0003-ios-fix.patch"
] ]
}, },
"spirv-headers": { "spirv-headers": {

View file

@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project # SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2021 yuzu Emulator Project # SPDX-FileCopyrightText: 2021 yuzu Emulator Project
@ -11,9 +11,9 @@ set(FFmpeg_HWACCEL_FLAGS)
set(FFmpeg_HWACCEL_INCLUDE_DIRS) set(FFmpeg_HWACCEL_INCLUDE_DIRS)
set(FFmpeg_HWACCEL_LDFLAGS) set(FFmpeg_HWACCEL_LDFLAGS)
if (UNIX AND NOT ANDROID) if (UNIX AND NOT ANDROID AND NOT IOS)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
if (NOT ANDROID) if (NOT ANDROID AND NOT IOS)
pkg_check_modules(LIBVA libva) pkg_check_modules(LIBVA libva)
pkg_check_modules(CUDA cuda) pkg_check_modules(CUDA cuda)
pkg_check_modules(FFNVCODEC ffnvcodec) pkg_check_modules(FFNVCODEC ffnvcodec)
@ -182,6 +182,10 @@ else()
find_program(BASH_PROGRAM bash REQUIRED) find_program(BASH_PROGRAM bash REQUIRED)
set(FFmpeg_CROSS_COMPILE_FLAGS "") set(FFmpeg_CROSS_COMPILE_FLAGS "")
# `configure` parameters builds only exactly what yuzu needs from FFmpeg
# `--disable-vdpau` is needed to avoid linking issues
set(FFmpeg_CC ${CMAKE_C_COMPILER_LAUNCHER} ${CMAKE_C_COMPILER})
set(FFmpeg_CXX ${CMAKE_CXX_COMPILER_LAUNCHER} ${CMAKE_CXX_COMPILER})
if (ANDROID) if (ANDROID)
string(TOLOWER "${CMAKE_HOST_SYSTEM_NAME}" FFmpeg_HOST_SYSTEM_NAME) string(TOLOWER "${CMAKE_HOST_SYSTEM_NAME}" FFmpeg_HOST_SYSTEM_NAME)
set(TOOLCHAIN "${ANDROID_NDK}/toolchains/llvm/prebuilt/${FFmpeg_HOST_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}") set(TOOLCHAIN "${ANDROID_NDK}/toolchains/llvm/prebuilt/${FFmpeg_HOST_SYSTEM_NAME}-${CMAKE_HOST_SYSTEM_PROCESSOR}")
@ -197,12 +201,22 @@ else()
--extra-ldflags="--ld-path=${TOOLCHAIN}/bin/ld.lld" --extra-ldflags="--ld-path=${TOOLCHAIN}/bin/ld.lld"
--extra-ldflags="-nostdlib" --extra-ldflags="-nostdlib"
) )
elseif(IOS)
execute_process(COMMAND xcrun --sdk iphoneos --show-sdk-path OUTPUT_VARIABLE SYSROOT)
# Lovely extra newline apple adds that **we** must remove... thank you apple!
string(STRIP "${SYSROOT}" SYSROOT)
set(FFmpeg_CC xcrun --sdk iphoneos clang -arch arm64)
set(FFmpeg_CXX xcrun --sdk iphoneos clang++ -arch arm64)
list(APPEND FFmpeg_CROSS_COMPILE_FLAGS
--arch=arm64
--enable-cross-compile
--sysroot="${SYSROOT}"
--extra-ldflags="-miphoneos-version-min=16.0"
--install-name-dir='@rpath'
--disable-audiotoolbox
)
endif() endif()
# `configure` parameters builds only exactly what yuzu needs from FFmpeg
# `--disable-vdpau` is needed to avoid linking issues
set(FFmpeg_CC ${CMAKE_C_COMPILER_LAUNCHER} ${CMAKE_C_COMPILER})
set(FFmpeg_CXX ${CMAKE_CXX_COMPILER_LAUNCHER} ${CMAKE_CXX_COMPILER})
add_custom_command( add_custom_command(
OUTPUT OUTPUT
${FFmpeg_MAKEFILE} ${FFmpeg_MAKEFILE}

View file

@ -9,7 +9,7 @@ pkgs.mkShellNoCC {
# libraries # libraries
openssl boost fmt nlohmann_json lz4 zlib zstd openssl boost fmt nlohmann_json lz4 zlib zstd
enet libopus vulkan-headers vulkan-utility-libraries enet libopus vulkan-headers vulkan-utility-libraries
spirv-tools spirv-headers vulkan-loader unzip mbedtls spirv-tools spirv-headers vulkan-loader unzip
glslang python3 httplib cpp-jwt ffmpeg-headless glslang python3 httplib cpp-jwt ffmpeg-headless
libusb1 cubeb libusb1 cubeb
# eden # eden

View file

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

View file

@ -80,7 +80,6 @@ android {
listOf( listOf(
"-DENABLE_QT=0", // Don't use QT "-DENABLE_QT=0", // Don't use QT
"-DENABLE_WEB_SERVICE=1", // Enable web service "-DENABLE_WEB_SERVICE=1", // Enable web service
"-DENABLE_OPENSSL=ON",
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work "-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
"-DYUZU_USE_CPM=ON", "-DYUZU_USE_CPM=ON",
"-DCPMUTIL_FORCE_BUNDLED=ON", "-DCPMUTIL_FORCE_BUNDLED=ON",

View file

@ -152,6 +152,10 @@ object NativeLibrary {
external fun surfaceDestroyed() external fun surfaceDestroyed()
external fun getAppletCaptureBuffer(): ByteArray
external fun getAppletCaptureWidth(): Int
external fun getAppletCaptureHeight(): Int
/** /**
* Unpauses emulation from a paused state. * Unpauses emulation from a paused state.
*/ */
@ -603,6 +607,12 @@ object NativeLibrary {
*/ */
external fun addFileToFilesystemProvider(path: String) external fun addFileToFilesystemProvider(path: String)
/**
* Adds a game-folder file to the manual filesystem provider, respecting the internal gate for
* game-folder external-content mounting.
*/
external fun addGameFolderFileToFilesystemProvider(path: String)
/** /**
* Clears all files added to the manual filesystem provider in our EmulationSession instance * Clears all files added to the manual filesystem provider in our EmulationSession instance
*/ */

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@ -204,9 +204,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
} }
override fun onPause() { override fun onPause() {
super.onPause()
nfcReader.stopScanning() nfcReader.stopScanning()
stopMotionSensorListener() stopMotionSensorListener()
super.onPause()
} }
override fun onDestroy() { override fun onDestroy() {
@ -339,6 +339,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
} }
override fun onSensorChanged(event: SensorEvent) { override fun onSensorChanged(event: SensorEvent) {
if (!NativeLibrary.isRunning() || NativeLibrary.isPaused()) {
return
}
val rotation = this.display?.rotation val rotation = this.display?.rotation
if (rotation == Surface.ROTATION_90) { if (rotation == Surface.ROTATION_90) {
flipMotionOrientation = true flipMotionOrientation = true

View file

@ -1,10 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.fetcher package org.yuzu.yuzu_emu.features.fetcher
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() { class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecoration() {
@ -15,8 +16,20 @@ class SpacingItemDecoration(private val spacing: Int) : RecyclerView.ItemDecorat
state: RecyclerView.State state: RecyclerView.State
) { ) {
outRect.bottom = spacing outRect.bottom = spacing
if (parent.getChildAdapterPosition(view) == 0) {
val position = parent.getChildAdapterPosition(view)
if (position == RecyclerView.NO_POSITION) return
if (position == 0) {
outRect.top = spacing outRect.top = spacing
return
}
// If the item is in the first row, but NOT in first column add top spacing as well
val layoutManager = parent.layoutManager
if (layoutManager is GridLayoutManager && layoutManager.spanSizeLookup.getSpanGroupIndex(position, layoutManager.spanCount) == 0) {
outRect.top = spacing
return
} }
} }
} }

View file

@ -34,6 +34,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
MAX_ANISOTROPY("max_anisotropy"), MAX_ANISOTROPY("max_anisotropy"),
THEME("theme"), THEME("theme"),
THEME_MODE("theme_mode"), THEME_MODE("theme_mode"),
STATIC_THEME_COLOR("static_theme_color"),
APP_LANGUAGE("app_language"), APP_LANGUAGE("app_language"),
OVERLAY_SCALE("control_scale"), OVERLAY_SCALE("control_scale"),
OVERLAY_OPACITY("control_opacity"), OVERLAY_OPACITY("control_opacity"),

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@ -68,7 +68,9 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.reset_setting_confirmation) .setMessage(R.string.reset_setting_confirmation)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
when (val item = settingsViewModel.clickedItem) { val item = settingsViewModel.clickedItem ?: return@setPositiveButton
clearDialogState()
when (item) {
is AnalogInputSetting -> { is AnalogInputSetting -> {
val stickParam = NativeInput.getStickParam( val stickParam = NativeInput.getStickParam(
item.playerIndex, item.playerIndex,
@ -107,12 +109,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
} }
else -> { else -> {
settingsViewModel.clickedItem!!.setting.reset() item.setting.reset()
settingsViewModel.setAdapterItemChanged(position) settingsViewModel.setAdapterItemChanged(position)
} }
} }
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int ->
clearDialogState()
}
.setOnCancelListener {
clearDialogState()
}
.create() .create()
} }
@ -186,27 +193,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
updateButtonState(isValid) updateButtonState(isValid)
} }
/*
* xbzk: these two events, along with attachRepeat feature,
* were causing spinbox buttons to respond twice per press
* cutting these out to retain accelerated press functionality
* TODO: clean this out later if no issues arise
*
spinboxBinding.buttonDecrement.setOnClickListener {
val current = spinboxBinding.editValue.text.toString().toIntOrNull() ?: currentValue
val newValue = current - 1
spinboxBinding.editValue.setText(newValue.toString())
updateValidity(newValue)
}
spinboxBinding.buttonIncrement.setOnClickListener {
val current = spinboxBinding.editValue.text.toString().toIntOrNull() ?: currentValue
val newValue = current + 1
spinboxBinding.editValue.setText(newValue.toString())
updateValidity(newValue)
}
*/
fun attachRepeat(button: View, delta: Int) { fun attachRepeat(button: View, delta: Int) {
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
var runnable: Runnable? = null var runnable: Runnable? = null
@ -439,9 +425,13 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
private fun closeDialog() { private fun closeDialog() {
settingsViewModel.setAdapterItemChanged(position) settingsViewModel.setAdapterItemChanged(position)
clearDialogState()
dismiss()
}
private fun clearDialogState() {
settingsViewModel.clickedItem = null settingsViewModel.clickedItem = null
settingsViewModel.setSliderProgress(-1f) settingsViewModel.setSliderProgress(-1f)
dismiss()
} }
private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {

View file

@ -1066,7 +1066,10 @@ class SettingsFragmentPresenter(
IntSetting.THEME.getValueAsString() IntSetting.THEME.getValueAsString()
override val defaultValue: Int = IntSetting.THEME.defaultValue override val defaultValue: Int = IntSetting.THEME.defaultValue
override fun reset() = IntSetting.THEME.setInt(defaultValue) override fun reset() {
IntSetting.THEME.setInt(defaultValue)
settingsViewModel.setShouldRecreate(true)
}
} }
add(HeaderSetting(R.string.app_settings)) add(HeaderSetting(R.string.app_settings))
@ -1124,23 +1127,24 @@ class SettingsFragmentPresenter(
} }
val staticThemeColor: AbstractIntSetting = object : AbstractIntSetting { val staticThemeColor: AbstractIntSetting = object : AbstractIntSetting {
val preferences = PreferenceManager.getDefaultSharedPreferences(
YuzuApplication.appContext
)
override fun getInt(needsGlobal: Boolean): Int = override fun getInt(needsGlobal: Boolean): Int =
preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) IntSetting.STATIC_THEME_COLOR.getInt(needsGlobal)
override fun setInt(value: Int) { override fun setInt(value: Int) {
preferences.edit() { putInt(Settings.PREF_STATIC_THEME_COLOR, value) } IntSetting.STATIC_THEME_COLOR.setInt(value)
settingsViewModel.setShouldRecreate(true) settingsViewModel.setShouldRecreate(true)
} }
override val key: String = Settings.PREF_STATIC_THEME_COLOR override val key: String = IntSetting.STATIC_THEME_COLOR.key
override val isRuntimeModifiable: Boolean = true override val isRuntimeModifiable: Boolean = true
override fun getValueAsString(needsGlobal: Boolean): String = override fun getValueAsString(needsGlobal: Boolean): String =
preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0).toString() IntSetting.STATIC_THEME_COLOR.getValueAsString(needsGlobal)
override val defaultValue: Any = 0
override val defaultValue: Any = IntSetting.STATIC_THEME_COLOR.defaultValue
override fun reset() { override fun reset() {
preferences.edit() { putInt(Settings.PREF_STATIC_THEME_COLOR, 0) } IntSetting.STATIC_THEME_COLOR.reset()
settingsViewModel.setShouldRecreate(true) settingsViewModel.setShouldRecreate(true)
} }
} }

View file

@ -15,6 +15,7 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.BatteryManager import android.os.BatteryManager
import android.os.BatteryManager.* import android.os.BatteryManager.*
@ -97,6 +98,7 @@ import org.yuzu.yuzu_emu.utils.collect
import org.yuzu.yuzu_emu.utils.CustomSettingsHandler import org.yuzu.yuzu_emu.utils.CustomSettingsHandler
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.nio.ByteBuffer
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.or import kotlin.or
@ -141,6 +143,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var wasInputOverlayAutoHidden = false private var wasInputOverlayAutoHidden = false
private var overlayTouchActive = false private var overlayTouchActive = false
private var pausedFrameBitmap: Bitmap? = null
var shouldUseCustom = false var shouldUseCustom = false
private var isQuickSettingsMenuOpen = false private var isQuickSettingsMenuOpen = false
@ -703,6 +706,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible = binding.inGameMenu.menu.findItem(R.id.menu_quick_settings)?.isVisible =
BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean() BooleanSetting.ENABLE_QUICK_SETTINGS.getBoolean()
binding.pausedIcon.setOnClickListener {
if (this::emulationState.isInitialized && emulationState.isPaused) {
resumeEmulationFromUi()
}
}
binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply { binding.inGameMenu.menu.findItem(R.id.menu_lock_drawer).apply {
val lockMode = IntSetting.LOCK_DRAWER.getInt() val lockMode = IntSetting.LOCK_DRAWER.getInt()
val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) { val titleId = if (lockMode == DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {
@ -728,11 +737,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
when (it.itemId) { when (it.itemId) {
R.id.menu_pause_emulation -> { R.id.menu_pause_emulation -> {
if (emulationState.isPaused) { if (emulationState.isPaused) {
emulationState.run(false) resumeEmulationFromUi()
updatePauseMenuEntry(false)
} else { } else {
emulationState.pause() pauseEmulationAndCaptureFrame()
updatePauseMenuEntry(true)
} }
binding.inGameMenu.requestFocus() binding.inGameMenu.requestFocus()
true true
@ -826,6 +833,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
R.id.menu_exit -> { R.id.menu_exit -> {
clearPausedFrame()
emulationState.stop() emulationState.stop()
NativeConfig.reloadGlobalConfig() NativeConfig.reloadGlobalConfig()
emulationViewModel.setIsEmulationStopping(true) emulationViewModel.setIsEmulationStopping(true)
@ -1197,6 +1205,71 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
private fun pauseEmulationAndCaptureFrame() {
emulationState.pause()
updatePauseMenuEntry(true)
capturePausedFrameFromCore()
updatePausedFrameVisibility()
}
private fun capturePausedFrameFromCore() {
lifecycleScope.launch(Dispatchers.Default) {
val frameData = NativeLibrary.getAppletCaptureBuffer()
val width = NativeLibrary.getAppletCaptureWidth()
val height = NativeLibrary.getAppletCaptureHeight()
if (frameData.isEmpty() || width <= 0 || height <= 0) {
Log.warning(
"[EmulationFragment] Paused frame capture returned empty/invalid data. " +
"size=${frameData.size}, width=$width, height=$height"
)
return@launch
}
val expectedSize = width * height * 4
if (frameData.size < expectedSize) {
Log.warning(
"[EmulationFragment] Paused frame buffer smaller than expected. " +
"size=${frameData.size}, expected=$expectedSize"
)
return@launch
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(frameData, 0, expectedSize))
withContext(Dispatchers.Main) {
pausedFrameBitmap?.recycle()
pausedFrameBitmap = bitmap
updatePausedFrameVisibility()
}
}
}
private fun updatePausedFrameVisibility() {
val b = _binding ?: return
val showPausedUi = this::emulationState.isInitialized && emulationState.isPaused
b.pausedIcon.setVisible(showPausedUi)
val bitmap = if (showPausedUi) pausedFrameBitmap else null
b.pausedFrameImage.setImageBitmap(bitmap)
b.pausedFrameImage.setVisible(bitmap != null)
}
private fun resumeEmulationFromUi() {
clearPausedFrame()
emulationState.resume()
updatePauseMenuEntry(emulationState.isPaused)
updatePausedFrameVisibility()
}
private fun clearPausedFrame() {
val b = _binding
b?.pausedFrameImage?.setVisible(false)
b?.pausedFrameImage?.setImageDrawable(null)
pausedFrameBitmap?.recycle()
pausedFrameBitmap = null
}
private fun handleLoadAmiiboSelection(): Boolean { private fun handleLoadAmiiboSelection(): Boolean {
val binding = _binding ?: return true val binding = _binding ?: return true
@ -1290,8 +1363,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
override fun onPause() { override fun onPause() {
if (this::emulationState.isInitialized) { if (this::emulationState.isInitialized) {
if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) { if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
emulationState.pause() pauseEmulationAndCaptureFrame()
updatePauseMenuEntry(true) } else {
updatePausedFrameVisibility()
} }
} }
super.onPause() super.onPause()
@ -1301,6 +1375,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDestroyView() super.onDestroyView()
amiiboLoadJob?.cancel() amiiboLoadJob?.cancel()
amiiboLoadJob = null amiiboLoadJob = null
clearPausedFrame()
_binding?.surfaceInputOverlay?.touchEventListener = null _binding?.surfaceInputOverlay?.touchEventListener = null
_binding = null _binding = null
isAmiiboPickerOpen = false isAmiiboPickerOpen = false
@ -1321,6 +1396,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
b.inGameMenu.post { b.inGameMenu.post {
if (!this::emulationState.isInitialized || _binding == null) return@post if (!this::emulationState.isInitialized || _binding == null) return@post
updatePauseMenuEntry(emulationState.isPaused) updatePauseMenuEntry(emulationState.isPaused)
updatePausedFrameVisibility()
} }
} }
@ -1760,6 +1836,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
// Only update surface reference, don't trigger state changes // Only update surface reference, don't trigger state changes
emulationState.updateSurfaceReference(holder.surface) emulationState.updateSurfaceReference(holder.surface)
} }
updatePausedFrameVisibility()
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
@ -2090,6 +2167,29 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
} }
@Synchronized
fun resume() {
if (state != State.PAUSED) {
Log.warning("[EmulationFragment] Resume called while emulation is not paused.")
return
}
if (!emulationCanStart.invoke()) {
Log.warning("[EmulationFragment] Resume blocked by emulationCanStart check.")
return
}
val currentSurface = surface
if (currentSurface == null || !currentSurface.isValid) {
Log.debug("[EmulationFragment] Resume requested with invalid surface.")
return
}
NativeLibrary.surfaceChanged(currentSurface)
Log.debug("[EmulationFragment] Resuming emulation.")
NativeLibrary.unpauseEmulation()
NativeLibrary.playTimeManagerStart()
state = State.RUNNING
}
@Synchronized @Synchronized
fun changeProgram(programIndex: Int) { fun changeProgram(programIndex: Int) {
emulationThread.join() emulationThread.join()
@ -2111,7 +2211,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized @Synchronized
fun updateSurface() { fun updateSurface() {
if (surface != null) { if (surface != null && state == State.RUNNING) {
NativeLibrary.surfaceChanged(surface) NativeLibrary.surfaceChanged(surface)
} }
} }
@ -2127,20 +2227,20 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@Synchronized @Synchronized
fun clearSurface() { fun clearSurface() {
if (surface == null) { if (surface == null) {
Log.warning("[EmulationFragment] clearSurface called, but surface already null.") Log.debug("[EmulationFragment] clearSurface called, but surface already null.")
} else { } else {
if (state == State.RUNNING) {
pause()
}
NativeLibrary.surfaceDestroyed()
surface = null surface = null
Log.debug("[EmulationFragment] Surface destroyed.") Log.debug("[EmulationFragment] Surface destroyed.")
when (state) { when (state) {
State.RUNNING -> { State.PAUSED -> Log.debug(
state = State.PAUSED
}
State.PAUSED -> Log.warning(
"[EmulationFragment] Surface cleared while emulation paused." "[EmulationFragment] Surface cleared while emulation paused."
) )
else -> Log.warning( else -> Log.debug(
"[EmulationFragment] Surface cleared while emulation stopped." "[EmulationFragment] Surface cleared while emulation stopped."
) )
} }
@ -2148,29 +2248,35 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
} }
private fun runWithValidSurface(programIndex: Int = 0) { private fun runWithValidSurface(programIndex: Int = 0) {
NativeLibrary.surfaceChanged(surface)
if (!emulationCanStart.invoke()) { if (!emulationCanStart.invoke()) {
return return
} }
val currentSurface = surface
if (currentSurface == null || !currentSurface.isValid) {
Log.debug("[EmulationFragment] runWithValidSurface called with invalid surface.")
return
}
when (state) { when (state) {
State.STOPPED -> { State.STOPPED -> {
NativeLibrary.surfaceChanged(currentSurface)
emulationThread = Thread({ emulationThread = Thread({
Log.debug("[EmulationFragment] Starting emulation thread.") Log.debug("[EmulationFragment] Starting emulation thread.")
NativeLibrary.run(gamePath, programIndex, true) NativeLibrary.run(gamePath, programIndex, true)
}, "NativeEmulation") }, "NativeEmulation")
emulationThread.start() emulationThread.start()
state = State.RUNNING
} }
State.PAUSED -> { State.PAUSED -> {
Log.debug("[EmulationFragment] Resuming emulation.") Log.debug(
NativeLibrary.unpauseEmulation() "[EmulationFragment] Surface restored while emulation paused; " +
NativeLibrary.playTimeManagerStart() "waiting for explicit resume."
)
} }
else -> Log.debug("[EmulationFragment] Bug, run called while already running.") else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
} }
state = State.RUNNING
} }
private enum class State { private enum class State {

View file

@ -127,10 +127,6 @@ class AddonViewModel : ViewModel() {
return return
} }
// Check if there are multiple update versions
val updates = _patchList.value.filter { PatchType.from(it.type) == PatchType.Update }
val hasMultipleUpdates = updates.size > 1
NativeConfig.setDisabledAddons( NativeConfig.setDisabledAddons(
game!!.programId, game!!.programId,
_patchList.value.mapNotNull { _patchList.value.mapNotNull {
@ -140,7 +136,7 @@ class AddonViewModel : ViewModel() {
if (PatchType.from(it.type) == PatchType.Update) { if (PatchType.from(it.type) == PatchType.Update) {
if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) { if (it.name.contains("(NAND)") || it.name.contains("(SDMC)")) {
it.name it.name
} else if (hasMultipleUpdates) { } else if (it.numericVersion != 0L) {
"Update@${it.numericVersion}" "Update@${it.numericVersion}"
} else { } else {
it.name it.name

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-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.overlay package org.yuzu.yuzu_emu.overlay
@ -20,7 +20,6 @@ import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import android.view.WindowInsets import android.view.WindowInsets
@ -42,10 +41,10 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
/** /**
* Draws the interactive input overlay on top of the * Draws the interactive input overlay on top of the
* [SurfaceView] that is rendering emulation. * emulation rendering surface.
*/ */
class InputOverlay(context: Context, attrs: AttributeSet?) : class InputOverlay(context: Context, attrs: AttributeSet?) :
SurfaceView(context, attrs), View(context, attrs),
OnTouchListener { OnTouchListener {
private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet() private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet() private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()

View file

@ -424,7 +424,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
) )
val uriString = result.toString() val uriString = result.toString()
val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString } val folder = gamesViewModel.folders.value.firstOrNull {
it.uriString == uriString && it.type == org.yuzu.yuzu_emu.model.DirectoryType.EXTERNAL_CONTENT
}
if (folder != null) { if (folder != null) {
Toast.makeText( Toast.makeText(
applicationContext, applicationContext,

View file

@ -61,6 +61,12 @@ object DirectoryInitialization {
saveConfig = true saveConfig = true
} }
val staticThemeColor = preferences.migratePreference<Int>(Settings.PREF_STATIC_THEME_COLOR)
if (staticThemeColor != null) {
IntSetting.STATIC_THEME_COLOR.setInt(staticThemeColor)
saveConfig = true
}
val blackBackgrounds = val blackBackgrounds =
preferences.migratePreference<Boolean>(Settings.PREF_BLACK_BACKGROUNDS) preferences.migratePreference<Boolean>(Settings.PREF_BLACK_BACKGROUNDS)
if (blackBackgrounds != null) { if (blackBackgrounds != null) {

View file

@ -51,11 +51,24 @@ object GameHelper {
// Scan External Content directories and register all NSP/XCI files // Scan External Content directories and register all NSP/XCI files
val externalContentDirs = NativeConfig.getExternalContentDirs() val externalContentDirs = NativeConfig.getExternalContentDirs()
for (externalDir in externalContentDirs) { val uniqueExternalContentDirs = linkedSetOf<String>()
externalContentDirs.forEach { externalDir ->
if (externalDir.isNotEmpty()) {
uniqueExternalContentDirs.add(externalDir)
}
}
val mountedContainerUris = mutableSetOf<String>()
for (externalDir in uniqueExternalContentDirs) {
if (externalDir.isNotEmpty()) { if (externalDir.isNotEmpty()) {
val externalDirUri = externalDir.toUri() val externalDirUri = externalDir.toUri()
if (FileUtil.isTreeUriValid(externalDirUri)) { if (FileUtil.isTreeUriValid(externalDirUri)) {
scanExternalContentRecursive(FileUtil.listFiles(externalDirUri), 3) scanContentContainersRecursive(FileUtil.listFiles(externalDirUri), 3) {
val containerUri = it.uri.toString()
if (mountedContainerUris.add(containerUri)) {
NativeLibrary.addFileToFilesystemProvider(containerUri)
}
}
} }
} }
} }
@ -65,10 +78,13 @@ object GameHelper {
val gameDirUri = gameDir.uriString.toUri() val gameDirUri = gameDir.uriString.toUri()
val isValid = FileUtil.isTreeUriValid(gameDirUri) val isValid = FileUtil.isTreeUriValid(gameDirUri)
if (isValid) { if (isValid) {
val scanDepth = if (gameDir.deepScan) 3 else 1
addGamesRecursive( addGamesRecursive(
games, games,
FileUtil.listFiles(gameDirUri), FileUtil.listFiles(gameDirUri),
if (gameDir.deepScan) 3 else 1 scanDepth,
mountedContainerUris
) )
} else { } else {
badDirs.add(index) badDirs.add(index)
@ -103,9 +119,10 @@ object GameHelper {
// be done better imo. // be done better imo.
private val externalContentExtensions = setOf("nsp", "xci") private val externalContentExtensions = setOf("nsp", "xci")
private fun scanExternalContentRecursive( private fun scanContentContainersRecursive(
files: Array<MinimalDocumentFile>, files: Array<MinimalDocumentFile>,
depth: Int depth: Int,
onContainerFound: (MinimalDocumentFile) -> Unit
) { ) {
if (depth <= 0) { if (depth <= 0) {
return return
@ -113,14 +130,15 @@ object GameHelper {
files.forEach { files.forEach {
if (it.isDirectory) { if (it.isDirectory) {
scanExternalContentRecursive( scanContentContainersRecursive(
FileUtil.listFiles(it.uri), FileUtil.listFiles(it.uri),
depth - 1 depth - 1,
onContainerFound
) )
} else { } else {
val extension = FileUtil.getExtension(it.uri).lowercase() val extension = FileUtil.getExtension(it.uri).lowercase()
if (externalContentExtensions.contains(extension)) { if (externalContentExtensions.contains(extension)) {
NativeLibrary.addFileToFilesystemProvider(it.uri.toString()) onContainerFound(it)
} }
} }
} }
@ -129,7 +147,8 @@ object GameHelper {
private fun addGamesRecursive( private fun addGamesRecursive(
games: MutableList<Game>, games: MutableList<Game>,
files: Array<MinimalDocumentFile>, files: Array<MinimalDocumentFile>,
depth: Int depth: Int,
mountedContainerUris: MutableSet<String>
) { ) {
if (depth <= 0) { if (depth <= 0) {
return return
@ -140,11 +159,20 @@ object GameHelper {
addGamesRecursive( addGamesRecursive(
games, games,
FileUtil.listFiles(it.uri), FileUtil.listFiles(it.uri),
depth - 1 depth - 1,
mountedContainerUris
) )
} else { } else {
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { val extension = FileUtil.getExtension(it.uri).lowercase()
val game = getGame(it.uri, true) val filePath = it.uri.toString()
if (externalContentExtensions.contains(extension) &&
mountedContainerUris.add(filePath)) {
NativeLibrary.addGameFolderFileToFilesystemProvider(filePath)
}
if (Game.extensions.contains(extension)) {
val game = getGame(it.uri, true, false)
if (game != null) { if (game != null) {
games.add(game) games.add(game)
} }
@ -153,14 +181,20 @@ object GameHelper {
} }
} }
fun getGame(uri: Uri, addedToLibrary: Boolean): Game? { fun getGame(
uri: Uri,
addedToLibrary: Boolean,
registerFilesystemProvider: Boolean = true
): Game? {
val filePath = uri.toString() val filePath = uri.toString()
if (!GameMetadata.getIsValid(filePath)) { if (!GameMetadata.getIsValid(filePath)) {
return null return null
} }
if (registerFilesystemProvider) {
// Needed to update installed content information // Needed to update installed content information
NativeLibrary.addFileToFilesystemProvider(filePath) NativeLibrary.addFileToFilesystemProvider(filePath)
}
var name = GameMetadata.getTitle(filePath) var name = GameMetadata.getTitle(filePath)

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-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils package org.yuzu.yuzu_emu.utils
@ -80,17 +80,15 @@ object PathUtil {
} }
} }
// This really shouldn't be necessary, but the Android API seemingly
// doesn't have a way of doing this?
// Apparently, on certain devices the mount location can vary, so add
// extra cases here if we discover any new ones.
fun getRemovableStoragePath(idString: String): String? { fun getRemovableStoragePath(idString: String): String? {
var pathFile: File val possibleMountPaths = listOf("/mnt/media_rw/$idString", "/storage/$idString")
pathFile = File("/mnt/media_rw/$idString"); for (mountPath in possibleMountPaths) {
val pathFile = File(mountPath);
if (pathFile.exists()) { if (pathFile.exists()) {
return pathFile.absolutePath return pathFile.absolutePath
} }
}
return null return null
} }

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Eden Emulator Project // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils package org.yuzu.yuzu_emu.utils
@ -52,7 +52,7 @@ object ThemeHelper {
} }
private fun getSelectedStaticThemeColor(): Int { private fun getSelectedStaticThemeColor(): Int {
val themeIndex = preferences.getInt(Settings.PREF_STATIC_THEME_COLOR, 0) val themeIndex = IntSetting.STATIC_THEME_COLOR.getInt(false)
val themes = arrayOf( val themes = arrayOf(
R.style.Theme_Eden_Main, R.style.Theme_Eden_Main,
R.style.Theme_Yuzu_Main_Violet, R.style.Theme_Yuzu_Main_Violet,

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@ -11,8 +11,7 @@ import android.graphics.*
import android.util.AttributeSet import android.util.AttributeSet
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import androidx.preference.PreferenceManager
class GradientBorderCardView @JvmOverloads constructor( class GradientBorderCardView @JvmOverloads constructor(
context: Context, context: Context,
@ -44,12 +43,7 @@ class GradientBorderCardView @JvmOverloads constructor(
} }
private fun updateThemeState() { private fun updateThemeState() {
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val themeIndex = IntSetting.STATIC_THEME_COLOR.getInt(false)
val themeIndex = try {
prefs.getInt(Settings.PREF_STATIC_THEME_COLOR, 0)
} catch (e: Exception) {
0 // Default to Eden theme if error
}
isEdenTheme = themeIndex == 0 isEdenTheme = themeIndex == 0
invalidate() invalidate()
} }

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-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project # SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@ -27,10 +27,7 @@ if (ARCHITECTURE_arm64)
target_link_libraries(yuzu-android PRIVATE adrenotools) target_link_libraries(yuzu-android PRIVATE adrenotools)
endif() endif()
if (ENABLE_OPENSSL OR ENABLE_WEB_SERVICE)
target_link_libraries(yuzu-android PRIVATE OpenSSL::SSL cpp-jwt::cpp-jwt) target_link_libraries(yuzu-android PRIVATE OpenSSL::SSL cpp-jwt::cpp-jwt)
endif()
if (ENABLE_UPDATE_CHECKER) if (ENABLE_UPDATE_CHECKER)
target_compile_definitions(yuzu-android PUBLIC ENABLE_UPDATE_CHECKER) target_compile_definitions(yuzu-android PUBLIC ENABLE_UPDATE_CHECKER)
endif() endif()

View file

@ -33,6 +33,12 @@ void AndroidConfig::ReadAndroidValues() {
if (global) { if (global) {
ReadAndroidUIValues(); ReadAndroidUIValues();
ReadUIValues(); ReadUIValues();
BeginGroup(Settings::TranslateCategory(Settings::Category::DataStorage));
Settings::values.ext_content_from_game_dirs = ReadBooleanSetting(
std::string("ext_content_from_game_dirs"),
std::make_optional(
Settings::values.ext_content_from_game_dirs.GetDefault()));
EndGroup();
ReadOverlayValues(); ReadOverlayValues();
} }
ReadDriverValues(); ReadDriverValues();

View file

@ -56,6 +56,7 @@ namespace AndroidSettings {
Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android}; Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android};
Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android}; Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android};
Settings::Setting<s32> static_theme_color{linkage, 5, "static_theme_color", Settings::Category::Android};
Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds", Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds",
Settings::Category::Android}; Settings::Category::Android};
Settings::Setting<s32> app_language{linkage, 0, "app_language", Settings::Category::Android}; Settings::Setting<s32> app_language{linkage, 0, "app_language", Settings::Category::Android};

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project // SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -14,6 +17,14 @@
#include "jni/native.h" #include "jni/native.h"
void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { void EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
if (!surface) {
LOG_INFO(Frontend, "EmuWindow_Android::OnSurfaceChanged received null surface");
m_window_width = 0;
m_window_height = 0;
window_info.render_surface = nullptr;
return;
}
m_window_width = ANativeWindow_getWidth(surface); m_window_width = ANativeWindow_getWidth(surface);
m_window_height = ANativeWindow_getHeight(surface); m_window_height = ANativeWindow_getHeight(surface);

View file

@ -96,6 +96,11 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobj
return false; return false;
} }
if ((file_type == Loader::FileType::NSP || file_type == Loader::FileType::XCI) &&
!Loader::IsBootableGameContainer(file, file_type)) {
return false;
}
u64 program_id = 0; u64 program_id = 0;
Loader::ResultStatus res = loader->ReadProgramId(program_id); Loader::ResultStatus res = loader->ReadProgramId(program_id);
if (res != Loader::ResultStatus::Success) { if (res != Loader::ResultStatus::Success) {

View file

@ -89,6 +89,8 @@
#include "jni/native.h" #include "jni/native.h"
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/capture.h"
#include "video_core/textures/decoders.h"
#include "video_core/vulkan_common/vulkan_instance.h" #include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h" #include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/shader_notify.h" #include "video_core/shader_notify.h"
@ -215,108 +217,9 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
return; return;
} }
const auto extension = Common::ToLower(filepath.substr(filepath.find_last_of('.') + 1)); if (m_manual_provider->AddEntriesFromContainer(file)) {
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; return;
} }
}
// Handle XCI files
if (extension == "xci") {
FileSys::XCI xci{file};
if (xci.GetStatus() == Loader::ResultStatus::Success) {
const auto nsp = xci.GetSecurePartitionNSP();
if (nsp) {
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
entry.second->GetBaseFile());
}
}
}
return;
}
}
auto loader = Loader::GetLoader(m_system, file); auto loader = Loader::GetLoader(m_system, file);
if (!loader) { if (!loader) {
@ -337,6 +240,13 @@ void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath)
} }
} }
void EmulationSession::ConfigureFilesystemProviderFromGameFolder(const std::string& filepath) {
if (!Settings::values.ext_content_from_game_dirs.GetValue()) {
return;
}
ConfigureFilesystemProvider(filepath);
}
void EmulationSession::InitializeSystem(bool reload) { void EmulationSession::InitializeSystem(bool reload) {
if (!reload) { if (!reload) {
// Initialize logging system // Initialize logging system
@ -780,9 +690,10 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceChanged(JNIEnv* env, jobject i
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) { void Java_org_yuzu_yuzu_1emu_NativeLibrary_surfaceDestroyed(JNIEnv* env, jobject instance) {
ANativeWindow_release(EmulationSession::GetInstance().NativeWindow()); if (auto* native_window = EmulationSession::GetInstance().NativeWindow(); native_window) {
ANativeWindow_release(native_window);
}
EmulationSession::GetInstance().SetNativeWindow(nullptr); EmulationSession::GetInstance().SetNativeWindow(nullptr);
EmulationSession::GetInstance().SurfaceChanged();
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance, void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject instance,
@ -969,6 +880,40 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz
return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
} }
jbyteArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureBuffer(JNIEnv* env, jclass clazz) {
using namespace VideoCore::Capture;
if (!EmulationSession::GetInstance().IsRunning()) {
return env->NewByteArray(0);
}
const auto tiled = EmulationSession::GetInstance().System().GPU().GetAppletCaptureBuffer();
if (tiled.size() < TiledSize) {
return env->NewByteArray(0);
}
std::vector<u8> linear(LinearWidth * LinearHeight * BytesPerPixel);
Tegra::Texture::UnswizzleTexture(linear, tiled, BytesPerPixel, LinearWidth, LinearHeight,
LinearDepth, BlockHeight, BlockDepth);
auto buffer = env->NewByteArray(static_cast<jsize>(linear.size()));
if (!buffer) {
return env->NewByteArray(0);
}
env->SetByteArrayRegion(buffer, 0, static_cast<jsize>(linear.size()),
reinterpret_cast<const jbyte*>(linear.data()));
return buffer;
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureWidth(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::Capture::LinearWidth);
}
jint Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletCaptureHeight(JNIEnv* env, jclass clazz) {
return static_cast<jint>(VideoCore::Capture::LinearHeight);
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz, void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass clazz,
jboolean reload) { jboolean reload) {
// Initialize the emulated system. // Initialize the emulated system.
@ -1572,6 +1517,12 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* e
Common::Android::GetJString(env, jpath)); Common::Android::GetJString(env, jpath));
} }
void Java_org_yuzu_yuzu_1emu_NativeLibrary_addGameFolderFileToFilesystemProvider(
JNIEnv* env, jobject jobj, jstring jpath) {
EmulationSession::GetInstance().ConfigureFilesystemProviderFromGameFolder(
Common::Android::GetJString(env, jpath));
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) { void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries(); EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
} }

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -46,6 +49,7 @@ public:
const Core::PerfStatsResults& PerfStats(); const Core::PerfStatsResults& PerfStats();
int ShadersBuilding(); int ShadersBuilding();
void ConfigureFilesystemProvider(const std::string& filepath); void ConfigureFilesystemProvider(const std::string& filepath);
void ConfigureFilesystemProviderFromGameFolder(const std::string& filepath);
void InitializeSystem(bool reload); void InitializeSystem(bool reload);
void SetAppletId(int applet_id); void SetAppletId(int applet_id);
Core::SystemResultStatus InitializeEmulation(const std::string& filepath, Core::SystemResultStatus InitializeEmulation(const std::string& filepath,

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="#E6FFFFFF" />
</shape>

View file

@ -108,6 +108,22 @@
</FrameLayout> </FrameLayout>
<FrameLayout
android:id="@+id/paused_frame_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<ImageView
android:id="@+id/paused_frame_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:visibility="gone" />
</FrameLayout>
<FrameLayout <FrameLayout
android:id="@+id/input_container" android:id="@+id/input_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -142,6 +158,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="false"> android:fitsSystemWindows="false">
<ImageView
android:id="@+id/paused_icon"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:background="@drawable/circle_white"
android:contentDescription="@string/emulation_unpause"
android:padding="14dp"
android:src="@drawable/ic_play"
android:visibility="gone"
app:tint="@android:color/black" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/show_stats_overlay_text" android:id="@+id/show_stats_overlay_text"
style="@style/TextAppearance.Material3.BodySmall" style="@style/TextAppearance.Material3.BodySmall"

View file

@ -1222,12 +1222,12 @@
<!-- Static Themes --> <!-- Static Themes -->
<string name="static_theme_color">Theme Color</string> <string name="static_theme_color">Theme Color</string>
<string name="eden_theme">Eden (Default)</string> <string name="eden_theme">Eden</string>
<string name="violet">Violet</string> <string name="violet">Violet</string>
<string name="blue">Blue</string> <string name="blue">Blue</string>
<string name="cyan">Cyan</string> <string name="cyan">Cyan</string>
<string name="red">Red</string> <string name="red">Red</string>
<string name="green">Green</string> <string name="green">Green (Default)</string>
<string name="yellow">Yellow</string> <string name="yellow">Yellow</string>
<string name="orange">Orange</string> <string name="orange">Orange</string>
<string name="pink">Pink</string> <string name="pink">Pink</string>

View file

@ -14,11 +14,14 @@ extern std::atomic<bool> g_has_battery;
#elif defined(__APPLE__) #elif defined(__APPLE__)
#include <TargetConditionals.h> #include <TargetConditionals.h>
#if TARGET_OS_MAC #if defined(TARGET_OS_MAC) && TARGET_OS_MAC
#if TARGET_OS_IPHONE
// ios doesnt have this
#else
#include <IOKit/ps/IOPSKeys.h> #include <IOKit/ps/IOPSKeys.h>
#include <IOKit/ps/IOPowerSources.h> #include <IOKit/ps/IOPowerSources.h>
#endif #endif
#endif
#elif defined(__linux__) #elif defined(__linux__)
#include <fstream> #include <fstream>
#include <string> #include <string>
@ -48,7 +51,9 @@ namespace Common {
info.percentage = g_battery_percentage.load(std::memory_order_relaxed); info.percentage = g_battery_percentage.load(std::memory_order_relaxed);
info.charging = g_is_charging.load(std::memory_order_relaxed); info.charging = g_is_charging.load(std::memory_order_relaxed);
info.has_battery = g_has_battery.load(std::memory_order_relaxed); info.has_battery = g_has_battery.load(std::memory_order_relaxed);
#elif defined(__APPLE__) && TARGET_OS_IPHONE
// Not implemented
info.has_battery = false;
#elif defined(__APPLE__) && TARGET_OS_MAC #elif defined(__APPLE__) && TARGET_OS_MAC
CFTypeRef info_ref = IOPSCopyPowerSourcesInfo(); CFTypeRef info_ref = IOPSCopyPowerSourcesInfo();
CFArrayRef sources = IOPSCopyPowerSourcesList(info_ref); CFArrayRef sources = IOPSCopyPowerSourcesList(info_ref);
@ -96,7 +101,6 @@ namespace Common {
#else #else
info.has_battery = false; info.has_battery = false;
#endif #endif
return info; return info;
} }
} }

View file

@ -27,7 +27,11 @@
#include <sys/random.h> #include <sys/random.h>
#elif defined(__APPLE__) #elif defined(__APPLE__)
#include <sys/types.h> #include <sys/types.h>
#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE
// Not available on iOS for some fucking stupid reason...
#else
#include <sys/random.h> #include <sys/random.h>
#endif
#include <mach/vm_map.h> #include <mach/vm_map.h>
#include <mach/mach.h> #include <mach/mach.h>
#endif #endif

View file

@ -756,6 +756,8 @@ struct Values {
Category::DataStorage}; Category::DataStorage};
Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path", Setting<std::string> gamecard_path{linkage, std::string(), "gamecard_path",
Category::DataStorage}; Category::DataStorage};
Setting<bool> ext_content_from_game_dirs{linkage, true, "ext_content_from_game_dirs",
Category::DataStorage};
std::vector<std::string> external_content_dirs; std::vector<std::string> external_content_dirs;
// Debugging // Debugging

View file

@ -38,7 +38,6 @@ struct CipherContext {
static inline const std::string GetCipherName(Mode mode, u32 key_size) { static inline const std::string GetCipherName(Mode mode, u32 key_size) {
std::string cipher; std::string cipher;
std::size_t effective_bits = key_size * 8; std::size_t effective_bits = key_size * 8;
switch (mode) { switch (mode) {
case Mode::CTR: case Mode::CTR:
cipher = "CTR"; cipher = "CTR";
@ -53,7 +52,6 @@ static inline const std::string GetCipherName(Mode mode, u32 key_size) {
default: default:
UNREACHABLE(); UNREACHABLE();
} }
return fmt::format("AES-{}-{}", effective_bits, cipher); return fmt::format("AES-{}-{}", effective_bits, cipher);
}; };
@ -87,8 +85,7 @@ static EVP_CIPHER *GetCipher(Mode mode, u32 key_size) {
// TODO: WHY TEMPLATE??????? // TODO: WHY TEMPLATE???????
template <typename Key, std::size_t KeySize> template <typename Key, std::size_t KeySize>
Crypto::AESCipher<Key, KeySize>::AESCipher(Key key, Mode mode) Crypto::AESCipher<Key, KeySize>::AESCipher(Key key, Mode mode) : ctx(std::make_unique<CipherContext>()) {
: ctx(std::make_unique<CipherContext>()) {
ctx->encryption_context = EVP_CIPHER_CTX_new(); ctx->encryption_context = EVP_CIPHER_CTX_new();
ctx->decryption_context = EVP_CIPHER_CTX_new(); ctx->decryption_context = EVP_CIPHER_CTX_new();
@ -99,9 +96,7 @@ Crypto::AESCipher<Key, KeySize>::AESCipher(Key key, Mode mode)
UNIMPLEMENTED(); UNIMPLEMENTED();
} }
ASSERT_MSG(ctx->encryption_context && ctx->decryption_context && ctx->cipher, ASSERT(ctx->encryption_context && ctx->decryption_context && ctx->cipher && "OpenSSL cipher context failed init!");
"OpenSSL cipher context failed init!");
// now init ciphers // now init ciphers
ASSERT(EVP_CipherInit_ex2(ctx->encryption_context, ctx->cipher, key.data(), NULL, 1, NULL)); ASSERT(EVP_CipherInit_ex2(ctx->encryption_context, ctx->cipher, key.data(), NULL, 1, NULL));
ASSERT(EVP_CipherInit_ex2(ctx->decryption_context, ctx->cipher, key.data(), NULL, 0, NULL)); ASSERT(EVP_CipherInit_ex2(ctx->decryption_context, ctx->cipher, key.data(), NULL, 0, NULL));
@ -165,8 +160,7 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, std::size_t size, u8* des
template <typename Key, std::size_t KeySize> template <typename Key, std::size_t KeySize>
void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, std::size_t size, u8* dest, void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, std::size_t size, u8* dest,
std::size_t sector_id, std::size_t sector_size, Op op) { std::size_t sector_id, std::size_t sector_size, Op op) {
ASSERT_MSG(size % sector_size == 0, "XTS decryption size must be a multiple of sector size."); ASSERT(size % sector_size == 0 && "XTS decryption size must be a multiple of sector size.");
for (std::size_t i = 0; i < size; i += sector_size) { for (std::size_t i = 0; i < size; i += sector_size) {
SetIV(CalculateNintendoTweak(sector_id++)); SetIV(CalculateNintendoTweak(sector_id++));
Transcode(src + i, sector_size, dest + i, op); Transcode(src + i, sector_size, dest + i, op);
@ -177,8 +171,7 @@ template <typename Key, std::size_t KeySize>
void AESCipher<Key, KeySize>::SetIV(std::span<const u8> data) { void AESCipher<Key, KeySize>::SetIV(std::span<const u8> data) {
const int ret_enc = EVP_CipherInit_ex(ctx->encryption_context, nullptr, nullptr, nullptr, data.data(), -1); const int ret_enc = EVP_CipherInit_ex(ctx->encryption_context, nullptr, nullptr, nullptr, data.data(), -1);
const int ret_dec = EVP_CipherInit_ex(ctx->decryption_context, nullptr, nullptr, nullptr, data.data(), -1); const int ret_dec = EVP_CipherInit_ex(ctx->decryption_context, nullptr, nullptr, nullptr, data.data(), -1);
ASSERT(ret_enc == 1 && ret_dec == 1 && "Failed to set IV on OpenSSL contexts");
ASSERT_MSG(ret_enc == 1 && ret_dec == 1, "Failed to set IV on OpenSSL contexts");
} }
template class AESCipher<Key128>; template class AESCipher<Key128>;

View file

@ -1,4 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace Crypto {} // namespace Crypto

View file

@ -1,19 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/assert.h"
#include "core/file_sys/vfs.h"
#include "key_manager.h"
#include "mbedtls/cipher.h"
namespace Crypto {
typedef std::array<u8, 0x20> SHA256Hash;
inline SHA256Hash operator"" _HASH(const char* data, size_t len) {
if (len != 0x40)
return {};
}
} // namespace Crypto

View file

@ -117,6 +117,12 @@ void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
bool IsDirValidAndNonEmpty(const VirtualDir& dir) { bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty()); return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
} }
bool IsVersionedExternalUpdateDisabled(const std::vector<std::string>& disabled, u32 version) {
const std::string disabled_key = fmt::format("Update@{}", version);
return std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend() ||
std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
}
} // Anonymous namespace } // Anonymous namespace
PatchManager::PatchManager(u64 title_id_, PatchManager::PatchManager(u64 title_id_,
@ -155,8 +161,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
if (!update_versions.empty()) { if (!update_versions.empty()) {
checked_external = true; checked_external = true;
for (const auto& update_entry : update_versions) { for (const auto& update_entry : update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version); if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false; update_disabled = false;
enabled_version = update_entry.version; enabled_version = update_entry.version;
break; break;
@ -175,8 +180,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
if (!manual_update_versions.empty()) { if (!manual_update_versions.empty()) {
checked_manual = true; checked_manual = true;
for (const auto& update_entry : manual_update_versions) { for (const auto& update_entry : manual_update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version); if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false; update_disabled = false;
enabled_version = update_entry.version; enabled_version = update_entry.version;
break; break;
@ -580,8 +584,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
if (!update_versions.empty()) { if (!update_versions.empty()) {
checked_external = true; checked_external = true;
for (const auto& update_entry : update_versions) { for (const auto& update_entry : update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version); if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false; update_disabled = false;
enabled_version = update_entry.version; enabled_version = update_entry.version;
update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version); update_raw = external_provider->GetEntryForVersion(update_tid, type, update_entry.version);
@ -600,8 +603,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
if (!manual_update_versions.empty()) { if (!manual_update_versions.empty()) {
checked_manual = true; checked_manual = true;
for (const auto& update_entry : manual_update_versions) { for (const auto& update_entry : manual_update_versions) {
std::string disabled_key = fmt::format("Update@{}", update_entry.version); if (!IsVersionedExternalUpdateDisabled(disabled, update_entry.version)) {
if (std::find(disabled.cbegin(), disabled.cend(), disabled_key) == disabled.cend()) {
update_disabled = false; update_disabled = false;
enabled_version = update_entry.version; enabled_version = update_entry.version;
update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version); update_raw = manual_provider->GetEntryForVersion(update_tid, type, update_entry.version);
@ -704,9 +706,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
version_str = FormatTitleVersion(update_entry.version); version_str = FormatTitleVersion(update_entry.version);
} }
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
const auto update_disabled = const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend(); IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
Patch update_patch = {.enabled = !update_disabled, Patch update_patch = {.enabled = !update_disabled,
.name = "Update", .name = "Update",
@ -732,9 +733,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
version_str = FormatTitleVersion(update_entry.version); version_str = FormatTitleVersion(update_entry.version);
} }
std::string disabled_key = fmt::format("Update@{}", update_entry.version);
const auto update_disabled = const auto update_disabled =
std::find(disabled.cbegin(), disabled.cend(), disabled_key) != disabled.cend(); IsVersionedExternalUpdateDisabled(disabled, update_entry.version);
Patch update_patch = {.enabled = !update_disabled, Patch update_patch = {.enabled = !update_disabled,
@ -771,7 +771,8 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::nullopt, std::nullopt, ContentRecordType::Program, update_tid); std::nullopt, std::nullopt, ContentRecordType::Program, update_tid);
for (const auto& [slot, entry] : all_updates) { for (const auto& [slot, entry] : all_updates) {
if (slot == ContentProviderUnionSlot::External) { if (slot == ContentProviderUnionSlot::External ||
slot == ContentProviderUnionSlot::FrontendManual) {
continue; continue;
} }

View file

@ -104,6 +104,206 @@ static std::string GetCNMTName(TitleType type, u64 title_id) {
return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id); return fmt::format("{}_{:016x}.cnmt", TITLE_TYPE_NAMES[index], title_id);
} }
static std::shared_ptr<NSP> OpenContainerAsNsp(const VirtualFile& file, Loader::FileType type) {
if (!file) {
return nullptr;
}
if (type == Loader::FileType::Unknown || type == Loader::FileType::Error) {
type = Loader::IdentifyFile(file);
if (type == Loader::FileType::Unknown) {
type = Loader::GuessFromFilename(file->GetName());
}
}
if (type == Loader::FileType::NSP) {
auto nsp = std::make_shared<NSP>(file);
return nsp->GetStatus() == Loader::ResultStatus::Success ? nsp : nullptr;
}
if (type == Loader::FileType::XCI) {
XCI xci(file);
if (xci.GetStatus() != Loader::ResultStatus::Success) {
return nullptr;
}
auto secure_partition = xci.GetSecurePartitionNSP();
if (secure_partition == nullptr) {
return nullptr;
}
return secure_partition;
}
// SAF-backed files can occasionally fail type-guessing despite being valid NSP/XCI.
// As a last resort, probe both container parsers directly.
{
auto nsp = std::make_shared<NSP>(file);
if (nsp->GetStatus() == Loader::ResultStatus::Success) {
return nsp;
}
}
{
XCI xci(file);
if (xci.GetStatus() == Loader::ResultStatus::Success) {
auto secure_partition = xci.GetSecurePartitionNSP();
if (secure_partition != nullptr) {
return secure_partition;
}
}
}
return nullptr;
}
template <typename Callback>
bool ForEachContainerEntry(const std::shared_ptr<NSP>& nsp, bool only_content,
std::optional<u64> base_program_id, Callback&& on_entry) {
if (!nsp) {
return false;
}
const auto& ncas = nsp->GetNCAs();
if (ncas.empty()) {
return false;
}
std::map<u64, u32> versions;
std::map<u64, std::string> version_strings;
for (const auto& [title_id, nca_map] : ncas) {
for (const auto& [type_pair, nca] : nca_map) {
if (!nca) {
continue;
}
const auto& [title_type, content_type] = type_pair;
if (content_type == ContentRecordType::Meta) {
const auto subdirs = nca->GetSubdirectories();
if (!subdirs.empty()) {
for (const auto& inner_file : subdirs[0]->GetFiles()) {
if (inner_file->GetExtension() == "cnmt") {
const CNMT cnmt(inner_file);
versions[cnmt.GetTitleID()] = cnmt.GetTitleVersion();
break;
}
}
}
}
if (title_type == TitleType::Update && content_type == ContentRecordType::Control) {
const auto romfs = nca->GetRomFS();
if (!romfs) {
continue;
}
const auto extracted = ExtractRomFS(romfs);
if (!extracted) {
continue;
}
auto nacp_file = extracted->GetFile("control.nacp");
if (!nacp_file) {
nacp_file = extracted->GetFile("Control.nacp");
}
if (!nacp_file) {
continue;
}
const NACP nacp(nacp_file);
auto version_string = nacp.GetVersionString();
if (!version_string.empty()) {
version_strings[title_id] = std::move(version_string);
}
}
}
}
bool added_entries = false;
for (const auto& [title_id, nca_map] : ncas) {
if (base_program_id.has_value() && GetBaseTitleID(title_id) != *base_program_id) {
continue;
}
for (const auto& [type_pair, nca] : nca_map) {
const auto& [title_type, content_type] = type_pair;
if (only_content && title_type != TitleType::Update && title_type != TitleType::AOC) {
continue;
}
auto entry_file = nca ? nca->GetBaseFile() : nullptr;
if (!entry_file) {
continue;
}
u32 version = 0;
std::string version_string;
if (title_type == TitleType::Update) {
if (const auto version_it = versions.find(title_id); version_it != versions.end()) {
version = version_it->second;
}
if (const auto version_str_it = version_strings.find(title_id);
version_str_it != version_strings.end()) {
version_string = version_str_it->second;
}
}
on_entry(title_type, content_type, title_id, entry_file, version, version_string);
added_entries = true;
}
}
return added_entries;
}
static void UpsertExternalVersionEntry(std::vector<ExternalUpdateEntry>& multi_version_entries,
u64 title_id, u32 version,
const std::string& version_string,
ContentRecordType content_type, const VirtualFile& file) {
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()) {
ExternalUpdateEntry update_entry;
update_entry.title_id = title_id;
update_entry.version = version;
update_entry.version_string = version_string;
update_entry.files[static_cast<std::size_t>(content_type)] = file;
multi_version_entries.push_back(std::move(update_entry));
return;
}
it->files[static_cast<std::size_t>(content_type)] = file;
if (it->version_string.empty() && !version_string.empty()) {
it->version_string = version_string;
}
}
template <typename EntryMap, typename VersionMap>
static bool AddExternalEntriesFromContainer(const std::shared_ptr<NSP>& nsp, EntryMap& entries,
VersionMap& versions,
std::vector<ExternalUpdateEntry>& multi_version_entries) {
return ForEachContainerEntry(
nsp, true, std::nullopt,
[&entries, &versions,
&multi_version_entries](TitleType title_type, ContentRecordType content_type, u64 title_id,
const VirtualFile& file, u32 version,
const std::string& version_string) {
entries[{title_id, content_type, title_type}] = file;
if (title_type == TitleType::Update) {
versions[title_id] = version;
UpsertExternalVersionEntry(multi_version_entries, title_id, version, version_string,
content_type, file);
}
});
}
ContentRecordType GetCRTypeFromNCAType(NCAContentType type) { ContentRecordType GetCRTypeFromNCAType(NCAContentType type) {
switch (type) { switch (type) {
case NCAContentType::Program: case NCAContentType::Program:
@ -1008,6 +1208,26 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
} }
} }
bool ManualContentProvider::AddEntriesFromContainer(VirtualFile file, bool only_content,
std::optional<u64> base_program_id) {
const auto nsp = OpenContainerAsNsp(file, Loader::FileType::Unknown);
if (!nsp) {
return false;
}
return ForEachContainerEntry(
nsp, only_content, base_program_id,
[this](TitleType title_type, ContentRecordType content_type, u64 title_id,
const VirtualFile& entry_file, u32 version, const std::string& version_string) {
if (title_type == TitleType::Update) {
AddEntryWithVersion(title_type, content_type, title_id, version, version_string,
entry_file);
} else {
AddEntry(title_type, content_type, title_id, entry_file);
}
});
}
void ManualContentProvider::ClearAllEntries() { void ManualContentProvider::ClearAllEntries() {
entries.clear(); entries.clear();
multi_version_entries.clear(); multi_version_entries.clear();
@ -1091,14 +1311,6 @@ VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecor
return nullptr; return nullptr;
} }
bool ManualContentProvider::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[size_t(type)])
++count;
return count > 0;
}
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories) ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
: load_dirs(std::move(load_directories)) { : load_dirs(std::move(load_directories)) {
ExternalContentProvider::Refresh(); ExternalContentProvider::Refresh();
@ -1159,247 +1371,22 @@ void ExternalContentProvider::ScanDirectory(const VirtualDir& dir) {
} }
void ExternalContentProvider::ProcessNSP(const VirtualFile& file) { void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
auto nsp = NSP(file); const auto nsp = OpenContainerAsNsp(file, Loader::FileType::NSP);
if (nsp.GetStatus() != Loader::ResultStatus::Success) { if (!nsp) {
return; return;
} }
LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName()); LOG_DEBUG(Service_FS, "Processing NSP file: {}", file->GetName());
AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
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::array<VirtualFile, size_t(ContentRecordType::Count)>> 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}][size_t(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) {
existing.files = files_map;
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) { void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
auto xci = XCI(file); const auto nsp = OpenContainerAsNsp(file, Loader::FileType::XCI);
if (xci.GetStatus() != Loader::ResultStatus::Success) { if (!nsp) {
return; return;
} }
auto nsp = xci.GetSecurePartitionNSP(); AddExternalEntriesFromContainer(nsp, entries, versions, multi_version_entries);
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::array<VirtualFile, size_t(ContentRecordType::Count)>> 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}][size_t(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) {
existing.files = files_map;
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 { bool ExternalContentProvider::HasEntry(u64 title_id, ContentRecordType type) const {
@ -1491,12 +1478,4 @@ VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRec
return nullptr; 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[size_t(type)])
++count;
return count > 1;
}
} // namespace FileSys } // namespace FileSys

View file

@ -9,6 +9,7 @@
#include <array> #include <array>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include <ankerl/unordered_dense.h> #include <ankerl/unordered_dense.h>
@ -262,6 +263,8 @@ public:
VirtualFile file); VirtualFile file);
void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id, void AddEntryWithVersion(TitleType title_type, ContentRecordType content_type, u64 title_id,
u32 version, const std::string& version_string, VirtualFile file); u32 version, const std::string& version_string, VirtualFile file);
bool AddEntriesFromContainer(VirtualFile file, bool only_content = false,
std::optional<u64> base_program_id = std::nullopt);
void ClearAllEntries(); void ClearAllEntries();
void Refresh() override; void Refresh() override;
@ -276,7 +279,6 @@ public:
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const; std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const; VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
private: private:
std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries; std::map<std::tuple<TitleType, ContentRecordType, u64>, VirtualFile> entries;
@ -303,7 +305,6 @@ public:
std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const; std::vector<ExternalUpdateEntry> ListUpdateVersions(u64 title_id) const;
VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const; VirtualFile GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const;
bool HasMultipleVersions(u64 title_id, ContentRecordType type) const;
private: private:
void ScanDirectory(const VirtualDir& dir); void ScanDirectory(const VirtualDir& dir);

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@ -28,8 +28,10 @@ public:
{10101, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::Old>, "SaveReportWithUserOld"}, {10101, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::Old>, "SaveReportWithUserOld"},
{10102, &PlayReport::SaveReport<Core::Reporter::PlayReportType::Old2>, "SaveReportOld2"}, {10102, &PlayReport::SaveReport<Core::Reporter::PlayReportType::Old2>, "SaveReportOld2"},
{10103, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::Old2>, "SaveReportWithUserOld2"}, {10103, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::Old2>, "SaveReportWithUserOld2"},
{10104, &PlayReport::SaveReport<Core::Reporter::PlayReportType::New>, "SaveReport"}, {10104, &PlayReport::SaveReport<Core::Reporter::PlayReportType::Old3>, "SaveReportOld3"},
{10105, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::New>, "SaveReportWithUser"}, {10105, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::Old3>, "SaveReportWithUserOld3"},
{10106, &PlayReport::SaveReport<Core::Reporter::PlayReportType::New>, "SaveReport"},
{10107, &PlayReport::SaveReportWithUser<Core::Reporter::PlayReportType::New>, "SaveReportWithUser"},
{10200, &PlayReport::RequestImmediateTransmission, "RequestImmediateTransmission"}, {10200, &PlayReport::RequestImmediateTransmission, "RequestImmediateTransmission"},
{10300, &PlayReport::GetTransmissionStatus, "GetTransmissionStatus"}, {10300, &PlayReport::GetTransmissionStatus, "GetTransmissionStatus"},
{10400, &PlayReport::GetSystemSessionId, "GetSystemSessionId"}, {10400, &PlayReport::GetSystemSessionId, "GetSystemSessionId"},

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@ -9,11 +9,15 @@
#include <ostream> #include <ostream>
#include <string> #include <string>
#include <concepts> #include <concepts>
#include <algorithm>
#include "common/concepts.h" #include "common/concepts.h"
#include "common/fs/path_util.h" #include "common/fs/path_util.h"
#include "common/logging/log.h" #include "common/logging/log.h"
#include "common/string_util.h" #include "common/string_util.h"
#include "core/core.h" #include "core/core.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/common_funcs.h"
#include "core/file_sys/submission_package.h"
#include "core/hle/kernel/k_process.h" #include "core/hle/kernel/k_process.h"
#include "core/loader/deconstructed_rom_directory.h" #include "core/loader/deconstructed_rom_directory.h"
#include "core/loader/kip.h" #include "core/loader/kip.h"
@ -37,6 +41,49 @@ std::optional<FileType> IdentifyFileLoader(FileSys::VirtualFile file) {
return std::nullopt; return std::nullopt;
} }
std::shared_ptr<FileSys::NSP> OpenContainerAsNsp(FileSys::VirtualFile file, FileType type,
u64 program_id = 0,
std::size_t program_index = 0) {
if (!file) {
return nullptr;
}
if (type == FileType::NSP) {
auto nsp = std::make_shared<FileSys::NSP>(file, program_id, program_index);
return nsp->GetStatus() == ResultStatus::Success ? nsp : nullptr;
}
if (type == FileType::XCI) {
FileSys::XCI xci{file, program_id, program_index};
if (xci.GetStatus() != ResultStatus::Success) {
return nullptr;
}
auto secure_nsp = xci.GetSecurePartitionNSP();
if (secure_nsp == nullptr || secure_nsp->GetStatus() != ResultStatus::Success) {
return nullptr;
}
return secure_nsp;
}
return nullptr;
}
bool HasApplicationProgramContent(const std::shared_ptr<FileSys::NSP>& nsp) {
if (!nsp) {
return false;
}
const auto& ncas = nsp->GetNCAs();
return std::any_of(ncas.cbegin(), ncas.cend(), [](const auto& title_entry) {
const auto& nca_map = title_entry.second;
return nca_map.find(
{FileSys::TitleType::Application, FileSys::ContentRecordType::Program}) !=
nca_map.end();
});
}
} // namespace } // namespace
FileType IdentifyFile(FileSys::VirtualFile file) { FileType IdentifyFile(FileSys::VirtualFile file) {
@ -62,6 +109,27 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
} }
} }
bool IsContainerType(FileType type) {
return type == FileType::NSP || type == FileType::XCI;
}
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
std::size_t program_index) {
if (!file) {
return false;
}
if (type == FileType::Unknown) {
type = IdentifyFile(file);
}
if (!IsContainerType(type)) {
return false;
}
return HasApplicationProgramContent(OpenContainerAsNsp(file, type, program_id, program_index));
}
FileType GuessFromFilename(const std::string& name) { FileType GuessFromFilename(const std::string& name) {
if (name == "main") if (name == "main")
return FileType::DeconstructedRomDirectory; return FileType::DeconstructedRomDirectory;

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-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -46,12 +49,29 @@ enum class FileType {
}; };
/** /**
* Identifies the type of a bootable file based on the magic value in its header. * Identifies the type of a supported file/container based on its structure.
* @param file open file * @param file open file
* @return FileType of file * @return FileType of file
*/ */
FileType IdentifyFile(FileSys::VirtualFile file); FileType IdentifyFile(FileSys::VirtualFile file);
/**
* Returns whether the file type represents a container format that can bundle multiple titles
* (currently NSP/XCI).
*/
bool IsContainerType(FileType type);
/**
* Returns whether a container file is bootable as a game (has Application/Program content).
*
* @param file open file
* @param type optional file type; if Unknown it is auto-detected.
* @param program_id optional program id hint for multi-program containers.
* @param program_index optional program index hint for multi-program containers.
*/
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type = FileType::Unknown,
u64 program_id = 0, std::size_t program_index = 0);
/** /**
* Guess the type of a bootable file from its name * Guess the type of a bootable file from its name
* @param name String name of bootable file * @param name String name of bootable file

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-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -55,19 +58,30 @@ AppLoader_NSP::~AppLoader_NSP() = default;
FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) { FileType AppLoader_NSP::IdentifyType(const FileSys::VirtualFile& nsp_file) {
const FileSys::NSP nsp(nsp_file); const FileSys::NSP nsp(nsp_file);
if (nsp.GetStatus() == ResultStatus::Success) { if (nsp.GetStatus() != ResultStatus::Success) {
return FileType::Error;
}
// Extracted Type case // Extracted Type case
if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr && if (nsp.IsExtractedType() && nsp.GetExeFS() != nullptr &&
FileSys::IsDirectoryExeFS(nsp.GetExeFS())) { FileSys::IsDirectoryExeFS(nsp.GetExeFS())) {
return FileType::NSP; return FileType::NSP;
} }
// Non-Extracted Type case // Non-extracted NSPs can legitimately contain only update/DLC content.
const auto program_id = nsp.GetProgramTitleID(); // Identify the container format itself; bootability is validated by Load().
if (!nsp.IsExtractedType() && if (!nsp.GetNCAs().empty()) {
nsp.GetNCA(program_id, FileSys::ContentRecordType::Program) != nullptr && return FileType::NSP;
AppLoader_NCA::IdentifyType( }
nsp.GetNCAFile(program_id, FileSys::ContentRecordType::Program)) == FileType::NCA) {
// Fallback when NCAs couldn't be parsed (e.g. missing keys) but the PFS still contains NCAs.
for (const auto& entry : nsp.GetFiles()) {
if (entry == nullptr) {
continue;
}
const auto& name = entry->GetName();
if (name.size() >= 4 && name.substr(name.size() - 4) == ".nca") {
return FileType::NSP; return FileType::NSP;
} }
} }

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-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -44,10 +47,13 @@ AppLoader_XCI::~AppLoader_XCI() = default;
FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) { FileType AppLoader_XCI::IdentifyType(const FileSys::VirtualFile& xci_file) {
const FileSys::XCI xci(xci_file); const FileSys::XCI xci(xci_file);
if (xci.GetStatus() == ResultStatus::Success && if (xci.GetStatus() != ResultStatus::Success) {
xci.GetNCAByType(FileSys::NCAContentType::Program) != nullptr && return FileType::Error;
AppLoader_NCA::IdentifyType(xci.GetNCAFileByType(FileSys::NCAContentType::Program)) == }
FileType::NCA) {
// Identify XCI as a valid container even when it does not include a bootable Program NCA.
// Bootability is handled by AppLoader_XCI::Load().
if (xci.GetSecurePartitionNSP() != nullptr) {
return FileType::XCI; return FileType::XCI;
} }

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-FileCopyrightText: Copyright 2019 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -53,6 +56,7 @@ public:
enum class PlayReportType { enum class PlayReportType {
Old, Old,
Old2, Old2,
Old3,
New, New,
System, System,
}; };

View file

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

View file

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

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

@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUI.swift - Sudachi
// Created by Jarrod Norwell on 4/3/2024.
//
import Foundation
import QuartzCore.CAMetalLayer
public struct AppUI {
public static let shared = AppUI()
fileprivate let appUIObjC = AppUIObjC.shared()
public func configure(layer: CAMetalLayer, with size: CGSize) {
appUIObjC.configure(layer: layer, with: size)
}
public func information(for url: URL) -> AppUIInformation {
appUIObjC.gameInformation.information(for: url)
}
public func insert(game url: URL) {
appUIObjC.insert(game: url)
}
public func insert(games urls: [URL]) {
appUIObjC.insert(games: urls)
}
public func bootOS() {
appUIObjC.bootOS()
}
public func pause() {
appUIObjC.pause()
}
public func play() {
appUIObjC.play()
}
public func ispaused() -> Bool {
return appUIObjC.ispaused()
}
public func FirstFrameShowed() -> Bool {
return appUIObjC.hasfirstfame()
}
public func canGetFullPath() -> Bool {
return appUIObjC.canGetFullPath()
}
public func exit() {
appUIObjC.quit()
}
public func step() {
appUIObjC.step()
}
public func orientationChanged(orientation: UIInterfaceOrientation, with layer: CAMetalLayer, size: CGSize) {
appUIObjC.orientationChanged(orientation: orientation, with: layer, size: size)
}
public func touchBegan(at point: CGPoint, for index: UInt) {
appUIObjC.touchBegan(at: point, for: index)
}
public func touchEnded(for index: UInt) {
appUIObjC.touchEnded(for: index)
}
public func touchMoved(at point: CGPoint, for index: UInt) {
appUIObjC.touchMoved(at: point, for: index)
}
public func gyroMoved(x: Float, y: Float, z: Float, accelX: Float, accelY: Float, accelZ: Float, controllerId: Int32, deltaTimestamp: Int32) {
// Calling the Objective-C function with both gyroscope and accelerometer data
appUIObjC.virtualControllerGyro(controllerId,
deltaTimestamp: deltaTimestamp,
gyroX: x, gyroY: y, gyroZ: z,
accelX: accelX, accelY: accelY, accelZ: accelZ)
}
public func thumbstickMoved(analog: VirtualControllerAnalogType, x: Float, y: Float, controllerid: Int) {
appUIObjC.thumbstickMoved(analog, x: CGFloat(x), y: CGFloat(y), controllerId: Int32(controllerid))
}
public func virtualControllerButtonDown(button: VirtualControllerButtonType, controllerid: Int) {
appUIObjC.virtualControllerButtonDown(button, controllerId: Int32(controllerid))
}
public func virtualControllerButtonUp(button: VirtualControllerButtonType, controllerid: Int) {
appUIObjC.virtualControllerButtonUp(button, controllerId: Int32(controllerid))
}
public func settingsSaved() {
appUIObjC.settingsChanged()
}
}

View file

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIGameInformation.h - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface AppUIInformation : NSObject
@property (nonatomic, strong) NSString *developer;
@property (nonatomic, strong) NSData *iconData;
@property (nonatomic) BOOL isHomebrew;
@property (nonatomic) uint64_t programID;
@property (nonatomic, strong) NSString *title, *version;
-(AppUIInformation *) initWithDeveloper:(NSString *)developer iconData:(NSData *)iconData isHomebrew:(BOOL)isHomebrew programID:(uint64_t)programID title:(NSString *)title version:(NSString *)version;
@end
@interface AppUIGameInformation : NSObject
+(AppUIGameInformation *) sharedInstance NS_SWIFT_NAME(shared());
-(AppUIInformation *) informationForGame:(NSURL *)url NS_SWIFT_NAME(information(for:));
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,442 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIGameInformation.mm - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#import <Foundation/Foundation.h>
#import "AppUIGameInformation.h"
#import "EmulationSession/EmulationSession.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "core/core.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/patch_manager.h"
#include "core/loader/loader.h"
#include "core/loader/nro.h"
#include "frontend_common/yuzu_config.h"
struct GameMetadata {
std::string title;
u64 programId;
std::string developer;
std::string version;
std::vector<u8> icon;
bool isHomebrew;
};
class SdlConfig final : public Config {
public:
explicit SdlConfig(std::optional<std::string> config_path);
~SdlConfig() override;
void ReloadAllValues() override;
void SaveAllValues() override;
protected:
void ReadSdlValues();
void ReadSdlPlayerValues(std::size_t player_index);
void ReadSdlControlValues();
void ReadHidbusValues() override;
void ReadDebugControlValues() override;
void ReadPathValues() override {}
void ReadShortcutValues() override {}
void ReadUIValues() override {}
void ReadUIGamelistValues() override {}
void ReadUILayoutValues() override {}
void ReadMultiplayerValues() override {}
void SaveSdlValues();
void SaveSdlPlayerValues(std::size_t player_index);
void SaveSdlControlValues();
void SaveHidbusValues() override;
void SaveDebugControlValues() override;
void SavePathValues() override {}
void SaveShortcutValues() override {}
void SaveUIValues() override {}
void SaveUIGamelistValues() override {}
void SaveUILayoutValues() override {}
void SaveMultiplayerValues() override {}
std::vector<Settings::BasicSetting*>& FindRelevantList(Settings::Category category) override;
public:
static const std::array<int, Settings::NativeButton::NumButtons> default_buttons;
static const std::array<int, Settings::NativeMotion::NumMotions> default_motions;
static const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> default_analogs;
static const std::array<int, 2> default_stick_mod;
static const std::array<int, 2> default_ringcon_analogs;
};
#define SDL_MAIN_HANDLED
#include <SDL.h>
#include "common/logging/log.h"
#include "input_common/main.h"
const std::array<int, Settings::NativeButton::NumButtons> SdlConfig::default_buttons = {
SDL_SCANCODE_A, SDL_SCANCODE_S, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_T,
SDL_SCANCODE_G, SDL_SCANCODE_F, SDL_SCANCODE_H, SDL_SCANCODE_Q, SDL_SCANCODE_W,
SDL_SCANCODE_M, SDL_SCANCODE_N, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_B,
};
const std::array<int, Settings::NativeMotion::NumMotions> SdlConfig::default_motions = {
SDL_SCANCODE_7,
SDL_SCANCODE_8,
};
const std::array<std::array<int, 4>, Settings::NativeAnalog::NumAnalogs> SdlConfig::default_analogs{
{
{
SDL_SCANCODE_UP,
SDL_SCANCODE_DOWN,
SDL_SCANCODE_LEFT,
SDL_SCANCODE_RIGHT,
},
{
SDL_SCANCODE_I,
SDL_SCANCODE_K,
SDL_SCANCODE_J,
SDL_SCANCODE_L,
},
}};
const std::array<int, 2> SdlConfig::default_stick_mod = {
SDL_SCANCODE_D,
0,
};
const std::array<int, 2> SdlConfig::default_ringcon_analogs{{
0,
0,
}};
SdlConfig::SdlConfig(const std::optional<std::string> config_path) {
Initialize(config_path);
ReadSdlValues();
SaveSdlValues();
}
SdlConfig::~SdlConfig() {
if (global) {
SdlConfig::SaveAllValues();
}
}
void SdlConfig::ReloadAllValues() {
Reload();
ReadSdlValues();
SaveSdlValues();
}
void SdlConfig::SaveAllValues() {
SaveValues();
SaveSdlValues();
}
void SdlConfig::ReadSdlValues() {
ReadSdlControlValues();
}
void SdlConfig::ReadSdlControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
ReadSdlPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
ReadDebugControlValues();
ReadHidbusValues();
EndGroup();
}
void SdlConfig::ReadSdlPlayerValues(const std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix.append("player_").append(ToString(player_index)).append("_");
}
auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig()) {
const auto profile_name =
ReadStringSetting(std::string(player_prefix).append("profile_name"));
if (profile_name.empty()) {
// Use the global input config
player = Settings::values.players.GetValue(true)[player_index];
player.profile_name = "";
return;
}
}
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
auto& player_buttons = player.buttons[i];
player_buttons = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeButton::mapping[i]), default_param);
if (player_buttons.empty()) {
player_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
auto& player_analogs = player.analogs[i];
player_analogs = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]), default_param);
if (player_analogs.empty()) {
player_analogs = default_param;
}
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
auto& player_motions = player.motions[i];
player_motions = ReadStringSetting(
std::string(player_prefix).append(Settings::NativeMotion::mapping[i]), default_param);
if (player_motions.empty()) {
player_motions = default_param;
}
}
}
void SdlConfig::ReadDebugControlValues() {
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
auto& debug_pad_buttons = Settings::values.debug_pad_buttons[i];
debug_pad_buttons = ReadStringSetting(
std::string("debug_pad_").append(Settings::NativeButton::mapping[i]), default_param);
if (debug_pad_buttons.empty()) {
debug_pad_buttons = default_param;
}
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
auto& debug_pad_analogs = Settings::values.debug_pad_analogs[i];
debug_pad_analogs = ReadStringSetting(
std::string("debug_pad_").append(Settings::NativeAnalog::mapping[i]), default_param);
if (debug_pad_analogs.empty()) {
debug_pad_analogs = default_param;
}
}
}
void SdlConfig::ReadHidbusValues() {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
0, 0, default_ringcon_analogs[0], default_ringcon_analogs[1], 0, 0.05f);
auto& ringcon_analogs = Settings::values.ringcon_analogs;
ringcon_analogs = ReadStringSetting(std::string("ring_controller"), default_param);
if (ringcon_analogs.empty()) {
ringcon_analogs = default_param;
}
}
void SdlConfig::SaveSdlValues() {
LOG_DEBUG(Config, "Saving SDL configuration values");
SaveSdlControlValues();
WriteToIni();
}
void SdlConfig::SaveSdlControlValues() {
BeginGroup(Settings::TranslateCategory(Settings::Category::Controls));
Settings::values.players.SetGlobal(!IsCustomConfig());
for (std::size_t p = 0; p < Settings::values.players.GetValue().size(); ++p) {
SaveSdlPlayerValues(p);
}
if (IsCustomConfig()) {
EndGroup();
return;
}
SaveDebugControlValues();
SaveHidbusValues();
EndGroup();
}
void SdlConfig::SaveSdlPlayerValues(const std::size_t player_index) {
std::string player_prefix;
if (type != ConfigType::InputProfile) {
player_prefix = std::string("player_").append(ToString(player_index)).append("_");
}
const auto& player = Settings::values.players.GetValue()[player_index];
if (IsCustomConfig() && player.profile_name.empty()) {
// No custom profile selected
return;
}
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
WriteStringSetting(std::string(player_prefix).append(Settings::NativeButton::mapping[i]),
player.buttons[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
WriteStringSetting(std::string(player_prefix).append(Settings::NativeAnalog::mapping[i]),
player.analogs[i], std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeMotion::NumMotions; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_motions[i]);
WriteStringSetting(std::string(player_prefix).append(Settings::NativeMotion::mapping[i]),
player.motions[i], std::make_optional(default_param));
}
}
void SdlConfig::SaveDebugControlValues() {
for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) {
const std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]);
WriteStringSetting(std::string("debug_pad_").append(Settings::NativeButton::mapping[i]),
Settings::values.debug_pad_buttons[i],
std::make_optional(default_param));
}
for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
default_analogs[i][0], default_analogs[i][1], default_analogs[i][2],
default_analogs[i][3], default_stick_mod[i], 0.5f);
WriteStringSetting(std::string("debug_pad_").append(Settings::NativeAnalog::mapping[i]),
Settings::values.debug_pad_analogs[i],
std::make_optional(default_param));
}
}
void SdlConfig::SaveHidbusValues() {
const std::string default_param = InputCommon::GenerateAnalogParamFromKeys(
0, 0, default_ringcon_analogs[0], default_ringcon_analogs[1], 0, 0.05f);
WriteStringSetting(std::string("ring_controller"), Settings::values.ringcon_analogs,
std::make_optional(default_param));
}
std::vector<Settings::BasicSetting*>& SdlConfig::FindRelevantList(Settings::Category category) {
return Settings::values.linkage.by_category[category];
}
std::unordered_map<std::string, GameMetadata> m_game_metadata_cache;
GameMetadata CacheGameMetadata(const std::string& path) {
const auto file =
Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file, 0, 0);
GameMetadata entry;
loader->ReadTitle(entry.title);
loader->ReadProgramId(entry.programId);
loader->ReadIcon(entry.icon);
const FileSys::PatchManager pm{
entry.programId, EmulationSession::GetInstance().System().GetFileSystemController(),
EmulationSession::GetInstance().System().GetContentProvider()};
const auto control = pm.GetControlMetadata();
if (control.first != nullptr) {
entry.developer = control.first->GetDeveloperName();
entry.version = control.first->GetVersionString();
} else {
FileSys::NACP nacp;
if (loader->ReadControlData(nacp) == Loader::ResultStatus::Success) {
entry.developer = nacp.GetDeveloperName();
} else {
entry.developer = "";
}
entry.version = "1.0.0";
}
if (loader->GetFileType() == Loader::FileType::NRO) {
auto loader_nro = reinterpret_cast<Loader::AppLoader_NRO*>(loader.get());
entry.isHomebrew = loader_nro->IsHomebrew();
} else {
entry.isHomebrew = false;
}
m_game_metadata_cache[path] = entry;
return entry;
}
GameMetadata GameMetadata(const std::string& path, bool reload = false) {
if (!EmulationSession::GetInstance().IsInitialized()) {
NSURL *dir_url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
const char *directory_cstr = [[dir_url path] UTF8String];
Common::FS::SetAppDirectory(directory_cstr);
EmulationSession::GetInstance().System().Initialize();
EmulationSession::GetInstance().InitializeSystem(false);
}
if (reload) {
return CacheGameMetadata(path);
}
if (auto search = m_game_metadata_cache.find(path); search != m_game_metadata_cache.end()) {
return search->second;
}
return CacheGameMetadata(path);
}
@implementation AppUIInformation
-(AppUIInformation *) initWithDeveloper:(NSString *)developer iconData:(NSData *)iconData isHomebrew:(BOOL)isHomebrew programID:(uint64_t)programID
title:(NSString *)title version:(NSString *)version {
if (self = [super init]) {
self.developer = developer;
self.iconData = iconData;
self.isHomebrew = isHomebrew;
self.programID = programID;
self.title = title;
self.version = version;
} return self;
}
@end
@implementation AppUIGameInformation
+(AppUIGameInformation *) sharedInstance {
static AppUIGameInformation *sharedInstance = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
-(AppUIInformation *) informationForGame:(NSURL *)url {
auto gameMetadata = GameMetadata([url.path UTF8String]);
return [[AppUIInformation alloc] initWithDeveloper:[NSString stringWithCString:gameMetadata.developer.c_str() encoding:NSUTF8StringEncoding]
iconData:[NSData dataWithBytes:gameMetadata.icon.data() length:gameMetadata.icon.size()]
isHomebrew:gameMetadata.isHomebrew programID:gameMetadata.programId
title:[NSString stringWithCString:gameMetadata.title.c_str() encoding:NSUTF8StringEncoding]
version:[NSString stringWithCString:gameMetadata.version.c_str() encoding:NSUTF8StringEncoding]];
}
@end

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

@ -0,0 +1,95 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIObjC.h - Sudachi
// Created by Jarrod Norwell on 1/8/24.
//
#import <Foundation/Foundation.h>
#import <MetalKit/MetalKit.h>
#import "AppUIGameInformation/AppUIGameInformation.h"
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, VirtualControllerAnalogType) {
VirtualControllerAnalogTypeLeft = 0,
VirtualControllerAnalogTypeRight = 1
};
typedef NS_ENUM(NSUInteger, VirtualControllerButtonType) {
VirtualControllerButtonTypeA = 0,
VirtualControllerButtonTypeB = 1,
VirtualControllerButtonTypeX = 2,
VirtualControllerButtonTypeY = 3,
VirtualControllerButtonTypeL = 4,
VirtualControllerButtonTypeR = 5,
VirtualControllerButtonTypeTriggerL = 6,
VirtualControllerButtonTypeTriggerR = 7,
VirtualControllerButtonTypeTriggerZL = 8,
VirtualControllerButtonTypeTriggerZR = 9,
VirtualControllerButtonTypePlus = 10,
VirtualControllerButtonTypeMinus = 11,
VirtualControllerButtonTypeDirectionalPadLeft = 12,
VirtualControllerButtonTypeDirectionalPadUp = 13,
VirtualControllerButtonTypeDirectionalPadRight = 14,
VirtualControllerButtonTypeDirectionalPadDown = 15,
VirtualControllerButtonTypeSL = 16,
VirtualControllerButtonTypeSR = 17,
VirtualControllerButtonTypeHome = 18,
VirtualControllerButtonTypeCapture = 19
};
@interface AppUIObjC : NSObject {
CAMetalLayer *_layer;
CGSize _size;
}
@property (nonatomic, strong) AppUIGameInformation *gameInformation;
+(AppUIObjC *) sharedInstance NS_SWIFT_NAME(shared());
-(void) configureLayer:(CAMetalLayer *)layer withSize:(CGSize)size NS_SWIFT_NAME(configure(layer:with:));
-(void) bootOS;
-(void) pause;
-(void) play;
-(BOOL) ispaused;
-(BOOL) canGetFullPath;
-(void) quit;
-(void) insertGame:(NSURL *)url NS_SWIFT_NAME(insert(game:));
-(void) insertGames:(NSArray<NSURL *> *)games NS_SWIFT_NAME(insert(games:));
-(void) step;
-(BOOL) hasfirstfame;
-(void) touchBeganAtPoint:(CGPoint)point index:(NSUInteger)index NS_SWIFT_NAME(touchBegan(at:for:));
-(void) touchEndedForIndex:(NSUInteger)index;
-(void) touchMovedAtPoint:(CGPoint)point index:(NSUInteger)index NS_SWIFT_NAME(touchMoved(at:for:));
-(void) thumbstickMoved:(VirtualControllerAnalogType)analog
x:(CGFloat)x
y:(CGFloat)y
controllerId:(int)controllerId;
-(void) virtualControllerGyro:(int)controllerId
deltaTimestamp:(int)delta_timestamp
gyroX:(float)gyro_x
gyroY:(float)gyro_y
gyroZ:(float)gyro_z
accelX:(float)accel_x
accelY:(float)accel_y
accelZ:(float)accel_z;
-(void) virtualControllerButtonDown:(VirtualControllerButtonType)button
controllerId:(int)controllerId;
-(void) virtualControllerButtonUp:(VirtualControllerButtonType)button
controllerId:(int)controllerId;
-(void) orientationChanged:(UIInterfaceOrientation)orientation with:(CAMetalLayer *)layer size:(CGSize)size NS_SWIFT_NAME(orientationChanged(orientation:with:size:));
-(void) settingsChanged;
@end
NS_ASSUME_NONNULL_END

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

@ -0,0 +1,263 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// AppUIObjC.mm - Sudachi
// Created by Jarrod Norwell on 1/8/24.
//
#import "AppUIObjC.h"
#import "Config/Config.h"
#import "EmulationSession/EmulationSession.h"
#import "DirectoryManager/DirectoryManager.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "common/fs/fs.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/savedata_factory.h"
#include "core/loader/nro.h"
#include "frontend_common/content_manager.h"
#include "common/settings_enums.h"
#include "network/announce_multiplayer_session.h"
#include "common/announce_multiplayer_room.h"
#include "network/network.h"
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
#include "common/fs/path_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h"
#include "core/frontend/applets/general.h"
#include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#include "frontend_common/yuzu_config.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/hid_types.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#import <mach/mach.h>
@implementation AppUIObjC
-(AppUIObjC *) init {
if (self = [super init]) {
_gameInformation = [AppUIGameInformation sharedInstance];
NSURL *dir_url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] firstObject];
const char *directory_cstr = [[dir_url path] UTF8String];
Common::FS::SetAppDirectory(directory_cstr);
Config{"config", Config::ConfigType::GlobalConfig};
EmulationSession::GetInstance().System().Initialize();
EmulationSession::GetInstance().InitializeSystem(false);
EmulationSession::GetInstance().InitializeGpuDriver();
Settings::values.dump_shaders.SetValue(true);
Settings::values.use_asynchronous_shaders.SetValue(true);
// Settings::values.astc_recompression.SetValue(Settings::AstcRecompression::Bc3);
Settings::values.shader_backend.SetValue(Settings::ShaderBackend::SpirV);
// Settings::values.resolution_setup.SetValue(Settings::ResolutionSetup::Res1X);
// Settings::values.scaling_filter.SetValue(Settings::ScalingFilter::Bilinear);
} return self;
}
+(AppUIObjC *) sharedInstance {
static AppUIObjC *sharedInstance = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (BOOL)ispaused {
return EmulationSession::GetInstance().IsPaused();
}
-(void) pause {
EmulationSession::GetInstance().System().Pause();
EmulationSession::GetInstance().HaltEmulation();
EmulationSession::GetInstance().PauseEmulation();
}
-(void) play {
EmulationSession::GetInstance().System().Run();
EmulationSession::GetInstance().RunEmulation();
EmulationSession::GetInstance().UnPauseEmulation();
}
-(BOOL)hasfirstfame {
@try {
auto* window = &EmulationSession::GetInstance().Window();
if (window && window->HasFirstFrame()) {
return YES;
}
}
@catch (NSException *exception) {
NSLog(@"Exception occurred: %@", exception);
// Handle the exception, maybe return a default value
return NO;
}
return NO;
}
- (BOOL)canGetFullPath {
@try {
Core::System& system = EmulationSession::GetInstance().System();
auto bis_system = system.GetFileSystemController().GetSystemNANDContents();
if (bis_system == nullptr) {
return NO;
}
constexpr u64 QLaunchId = static_cast<u64>(Service::AM::AppletProgramId::QLaunch);
auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
if (qlaunch_applet_nca == nullptr) {
return NO;
}
const auto filename = qlaunch_applet_nca->GetFullPath();
// If GetFullPath() is successful
return YES;
} @catch (NSException *exception) {
// Handle the exception if needed
return NO;
}
}
-(void) quit {
EmulationSession::GetInstance().ShutdownEmulation();
}
-(void) configureLayer:(CAMetalLayer *)layer withSize:(CGSize)size {
_layer = layer;
_size = size;
EmulationSession::GetInstance().SetNativeWindow((__bridge CA::MetalLayer*)layer, size);
}
-(void) bootOS {
EmulationSession::GetInstance().BootOS();
}
-(void) insertGame:(NSURL *)url {
EmulationSession::GetInstance().InitializeEmulation([url.path UTF8String], [_gameInformation informationForGame:url].programID, true);
}
-(void) insertGames:(NSArray<NSURL *> *)games {
for (NSURL *url in games) {
EmulationSession::GetInstance().ConfigureFilesystemProvider([url.path UTF8String]);
}
}
-(void) step {
void(EmulationSession::GetInstance().System().Run());
}
-(void) touchBeganAtPoint:(CGPoint)point index:(NSUInteger)index {
float h_ratio, w_ratio;
h_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().height / (_size.height * [[UIScreen mainScreen] nativeScale]);
w_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().width / (_size.width * [[UIScreen mainScreen] nativeScale]);
EmulationSession::GetInstance().Window().OnTouchPressed([[NSNumber numberWithUnsignedInteger:index] intValue],
(point.x) * [[UIScreen mainScreen] nativeScale] * w_ratio,
((point.y) * [[UIScreen mainScreen] nativeScale] * h_ratio));
}
-(void) touchEndedForIndex:(NSUInteger)index {
EmulationSession::GetInstance().Window().OnTouchReleased([[NSNumber numberWithUnsignedInteger:index] intValue]);
}
-(void) touchMovedAtPoint:(CGPoint)point index:(NSUInteger)index {
float h_ratio, w_ratio;
h_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().height / (_size.height * [[UIScreen mainScreen] nativeScale]);
w_ratio = EmulationSession::GetInstance().Window().GetFramebufferLayout().width / (_size.width * [[UIScreen mainScreen] nativeScale]);
EmulationSession::GetInstance().Window().OnTouchMoved([[NSNumber numberWithUnsignedInteger:index] intValue],
(point.x) * [[UIScreen mainScreen] nativeScale] * w_ratio,
((point.y) * [[UIScreen mainScreen] nativeScale] * h_ratio));
}
-(void) thumbstickMoved:(VirtualControllerAnalogType)analog
x:(CGFloat)x
y:(CGFloat)y
controllerId:(int)controllerId {
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadJoystickEvent(controllerId, [[NSNumber numberWithUnsignedInteger:analog] intValue], CGFloat(x), CGFloat(y));
}
-(void) virtualControllerButtonDown:(VirtualControllerButtonType)button
controllerId:(int)controllerId {
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadButtonEvent(controllerId, [[NSNumber numberWithUnsignedInteger:button] intValue], true);
}
-(void) virtualControllerGyro:(int)controllerId
deltaTimestamp:(int)delta_timestamp
gyroX:(float)gyro_x
gyroY:(float)gyro_y
gyroZ:(float)gyro_z
accelX:(float)accel_x
accelY:(float)accel_y
accelZ:(float)accel_z
{
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadMotionEvent(controllerId, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
-(void) virtualControllerButtonUp:(VirtualControllerButtonType)button
controllerId:(int)controllerId {
EmulationSession::GetInstance().OnGamepadConnectEvent(controllerId);
EmulationSession::GetInstance().Window().OnGamepadButtonEvent(controllerId, [[NSNumber numberWithUnsignedInteger:button] intValue], false);
}
-(void) orientationChanged:(UIInterfaceOrientation)orientation with:(CAMetalLayer *)layer size:(CGSize)size {
_layer = layer;
_size = size;
EmulationSession::GetInstance().Window().OnSurfaceChanged((__bridge CA::MetalLayer*)layer, size);
}
-(void) settingsChanged {
Config{"config", Config::ConfigType::GlobalConfig};
}
@end

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

@ -0,0 +1,29 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
enable_language(Swift OBJCXX)
add_executable(eden-ios
AppUI-Bridging-Header.h
AppUI.swift
AppUIGameInformation.h
AppUIGameInformation.mm
AppUIObjC.h
AppUIObjC.mm
Config.h
Config.mm
EmulationSession.h
EmulationSession.mm
EmulationWindow.h
EmulationWindow.mm
)
target_link_libraries(eden-ios PRIVATE common core input_common frontend_common video_core glad)
target_link_libraries(eden-ios PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
target_link_libraries(eden-ios PRIVATE SDL2::SDL2)
create_target_directory_groups(eden-ios)
target_compile_options(eden-ios PRIVATE
-Wno-conversion
-Wno-unused-variable
-Wno-unused-parameter
-Wno-missing-field-initializers)

19
src/ios/Config.h Normal file
View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <common/settings_common.h>
#include "common/common_types.h"
#include "common/settings_setting.h"
#include "common/settings_enums.h"
namespace IOSSettings {
struct Values {
Settings::Linkage linkage;
Settings::Setting<bool> touchscreen{linkage, true, "touchscreen", Settings::Category::Overlay};
};
extern Values values;
} // namespace IOSSettings

11
src/ios/Config.mm Normal file
View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "Config.h"
namespace IOSSettings {
Values values;
} // namespace IOSSettings

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

@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationSession.h - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#pragma once
#import <QuartzCore/CAMetalLayer.h>
#import <Metal/Metal.hpp>
#import "EmulationWindow/EmulationWindow.h"
#include "common/detached_tasks.h"
#include "core/core.h"
#include "core/file_sys/registered_cache.h"
#include "core/hle/service/acc/profile_manager.h"
#include "core/perf_stats.h"
#include "frontend_common/content_manager.h"
#include "video_core/rasterizer_interface.h"
class EmulationSession final {
public:
explicit EmulationSession();
~EmulationSession() = default;
static EmulationSession& GetInstance();
const Core::System& System() const;
Core::System& System();
FileSys::ManualContentProvider* GetContentProvider();
InputCommon::InputSubsystem& GetInputSubsystem();
const EmulationWindow& Window() const;
EmulationWindow& Window();
CA::MetalLayer* NativeWindow() const;
void SetNativeWindow(CA::MetalLayer* native_window, CGSize size);
void SurfaceChanged();
void InitializeGpuDriver();
bool IsRunning() const;
bool IsPaused() const;
void PauseEmulation();
void UnPauseEmulation();
void HaltEmulation();
void RunEmulation();
void ShutdownEmulation();
const Core::PerfStatsResults& PerfStats();
void ConfigureFilesystemProvider(const std::string& filepath);
void InitializeSystem(bool reload);
void SetAppletId(int applet_id);
Core::SystemResultStatus InitializeEmulation(const std::string& filepath,
const std::size_t program_index,
const bool frontend_initiated);
Core::SystemResultStatus BootOS();
static void OnEmulationStarted();
static u64 GetProgramId(std::string programId);
bool IsInitialized() { return is_initialized; };
bool IsHandheldOnly();
void SetDeviceType([[maybe_unused]] int index, int type);
void OnGamepadConnectEvent([[maybe_unused]] int index);
void OnGamepadDisconnectEvent([[maybe_unused]] int index);
private:
static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
static void OnEmulationStopped(Core::SystemResultStatus result);
static void ChangeProgram(std::size_t program_index);
private:
// Window management
std::unique_ptr<EmulationWindow> m_window;
CA::MetalLayer* m_native_window{};
// Core emulation
Core::System m_system;
InputCommon::InputSubsystem m_input_subsystem;
Common::DetachedTasks m_detached_tasks;
Core::PerfStatsResults m_perf_stats{};
std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
std::atomic<bool> m_is_running = false;
std::atomic<bool> m_is_paused = false;
std::unique_ptr<FileSys::ManualContentProvider> m_manual_provider;
int m_applet_id{1};
// GPU driver parameters
std::shared_ptr<Common::DynamicLibrary> m_vulkan_library;
// Synchronization
std::condition_variable_any m_cv;
mutable std::mutex m_mutex;
bool is_initialized = false;
CGSize m_size;
// Program index for next boot
std::atomic<s32> m_next_program_index = -1;
};

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

@ -0,0 +1,531 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationSession.m - Sudachi
// Created by Jarrod Norwell on 1/20/24.
//
#import "EmulationSession.h"
#include <SDL.h>
#include <codecvt>
#include <locale>
#include <string>
#include <string_view>
#include <dlfcn.h>
#include "common/fs/fs.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/savedata_factory.h"
#include "core/loader/nro.h"
#include "frontend_common/content_manager.h"
#include "common/detached_tasks.h"
#include "common/dynamic_library.h"
#include "common/fs/path_util.h"
#include "common/logging/backend.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/scm_rev.h"
#include "common/scope_exit.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/core.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/fs_filesystem.h"
#include "core/file_sys/submission_package.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_real.h"
#include "core/frontend/applets/cabinet.h"
#include "core/frontend/applets/controller.h"
#include "core/frontend/applets/error.h"
#include "core/frontend/applets/general.h"
#include "core/frontend/applets/mii_edit.h"
#include "core/frontend/applets/profile_select.h"
#include "core/frontend/applets/software_keyboard.h"
#include "core/frontend/applets/web_browser.h"
#include "core/hle/service/am/applet_manager.h"
#include "core/hle/service/am/frontend/applets.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#include "frontend_common/yuzu_config.h"
#include "hid_core/frontend/emulated_controller.h"
#include "hid_core/hid_core.h"
#include "hid_core/hid_types.h"
#include "video_core/renderer_base.h"
#include "video_core/renderer_vulkan/renderer_vulkan.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#define jconst [[maybe_unused]] const auto
#define jauto [[maybe_unused]] auto
static EmulationSession s_instance;
EmulationSession::EmulationSession() {
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
}
EmulationSession& EmulationSession::GetInstance() {
return s_instance;
}
const Core::System& EmulationSession::System() const {
return m_system;
}
Core::System& EmulationSession::System() {
return m_system;
}
FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
return m_manual_provider.get();
}
InputCommon::InputSubsystem& EmulationSession::GetInputSubsystem() {
return m_input_subsystem;
}
const EmulationWindow& EmulationSession::Window() const {
return *m_window;
}
EmulationWindow& EmulationSession::Window() {
return *m_window;
}
CA::MetalLayer* EmulationSession::NativeWindow() const {
return m_native_window;
}
void EmulationSession::SetNativeWindow(CA::MetalLayer* native_window, CGSize size) {
m_native_window = native_window;
m_size = size;
}
void EmulationSession::InitializeGpuDriver() {
m_vulkan_library = std::make_shared<Common::DynamicLibrary>(dlopen("@executable_path/Frameworks/MoltenVK", RTLD_NOW));
}
bool EmulationSession::IsRunning() const {
return m_is_running;
}
bool EmulationSession::IsPaused() const {
return m_is_running && m_is_paused;
}
const Core::PerfStatsResults& EmulationSession::PerfStats() {
m_perf_stats = m_system.GetAndResetPerfStats();
return m_perf_stats;
}
void EmulationSession::SurfaceChanged() {
if (!IsRunning()) {
return;
}
m_window->OnSurfaceChanged(m_native_window, m_size);
}
void EmulationSession::ConfigureFilesystemProvider(const std::string& filepath) {
const auto file = m_system.GetFilesystem()->OpenFile(filepath, FileSys::OpenMode::Read);
if (!file) {
return;
}
auto loader = Loader::GetLoader(m_system, file);
if (!loader) {
return;
}
const auto file_type = loader->GetFileType();
if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
return;
}
u64 program_id = 0;
const auto res2 = loader->ReadProgramId(program_id);
if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
m_manual_provider->AddEntry(FileSys::TitleType::Application,
FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
program_id, file);
} else if (res2 == Loader::ResultStatus::Success &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
const auto nsp = file_type == Loader::FileType::NSP
? std::make_shared<FileSys::NSP>(file)
: FileSys::XCI{file}.GetSecurePartitionNSP();
for (const auto& title : nsp->GetNCAs()) {
for (const auto& entry : title.second) {
m_manual_provider->AddEntry(entry.first.first, entry.first.second, title.first,
entry.second->GetBaseFile());
}
}
}
}
void EmulationSession::InitializeSystem(bool reload) {
if (!reload) {
SDL_SetMainReady();
// Initialize logging system
Common::Log::Initialize();
Common::Log::SetColorConsoleBackendEnabled(true);
Common::Log::Start();
}
// Initialize filesystem.
m_system.SetFilesystem(m_vfs);
m_system.GetUserChannel().clear();
m_manual_provider = std::make_unique<FileSys::ManualContentProvider>();
m_system.SetContentProvider(std::make_unique<FileSys::ContentProviderUnion>());
m_system.RegisterContentProvider(FileSys::ContentProviderUnionSlot::FrontendManual,
m_manual_provider.get());
m_system.GetFileSystemController().CreateFactories(*m_vfs);
is_initialized = true;
}
void EmulationSession::SetAppletId(int applet_id) {
m_applet_id = applet_id;
m_system.GetFrontendAppletHolder().SetCurrentAppletId(
static_cast<Service::AM::AppletId>(m_applet_id));
}
Core::SystemResultStatus EmulationSession::InitializeEmulation(const std::string& filepath,
const std::size_t program_index,
const bool frontend_initiated) {
std::scoped_lock lock(m_mutex);
// Create the render window.
m_window = std::make_unique<EmulationWindow>(&m_input_subsystem, m_native_window, m_size, m_vulkan_library);
// Initialize system.
m_system.SetShuttingDown(false);
m_system.ApplySettings();
Settings::LogSettings();
m_system.HIDCore().ReloadInputDevices();
m_system.SetFrontendAppletSet({
nullptr, // Amiibo Settings
nullptr, // Controller Selector
nullptr, // Error Display
nullptr, // Mii Editor
nullptr, // Parental Controls
nullptr, // Photo Viewer
nullptr, // Profile Selector
nullptr, // std::move(android_keyboard), // Software Keyboard
nullptr, // Web Browser
});
// Initialize filesystem.
ConfigureFilesystemProvider(filepath);
// Load the ROM.
Service::AM::FrontendAppletParameters params{
.applet_id = static_cast<Service::AM::AppletId>(m_applet_id),
.launch_type = frontend_initiated ? Service::AM::LaunchType::FrontendInitiated
: Service::AM::LaunchType::ApplicationInitiated,
.program_index = static_cast<s32>(program_index),
};
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filepath, params);
if (m_load_result != Core::SystemResultStatus::Success) {
return m_load_result;
}
// Complete initialization.
m_system.GPU().Start();
m_system.GetCpuManager().OnGpuReady();
m_system.RegisterExitCallback([&] { HaltEmulation(); });
if (Settings::values.use_disk_shader_cache.GetValue()) {
m_system.Renderer().ReadRasterizer()->LoadDiskResources(
m_system.GetApplicationProcessProgramID(), std::stop_token{},
[](VideoCore::LoadCallbackStage, size_t value, size_t total) {});
}
// Register an ExecuteProgram callback such that Core can execute a sub-program
m_system.RegisterExecuteProgramCallback([&](std::size_t program_index_) {
m_next_program_index = program_index_;
EmulationSession::GetInstance().HaltEmulation();
ChangeProgram(m_next_program_index);
});
OnEmulationStarted();
return Core::SystemResultStatus::Success;
}
Core::SystemResultStatus EmulationSession::BootOS() {
std::scoped_lock lock(m_mutex);
// Create the render window.
m_window = std::make_unique<EmulationWindow>(&m_input_subsystem, m_native_window, m_size, m_vulkan_library);
// Initialize system.
m_system.SetShuttingDown(false);
m_system.ApplySettings();
Settings::LogSettings();
m_system.HIDCore().ReloadInputDevices();
m_system.SetFrontendAppletSet({
nullptr, // Amiibo Settings
nullptr, // Controller Selector
nullptr, // Error Display
nullptr, // Mii Editor
nullptr, // Parental Controls
nullptr, // Photo Viewer
nullptr, // Profile Selector
nullptr, // std::move(android_keyboard), // Software Keyboard
nullptr, // Web Browser
});
constexpr u64 QLaunchId = static_cast<u64>(Service::AM::AppletProgramId::QLaunch);
auto bis_system = m_system.GetFileSystemController().GetSystemNANDContents();
auto qlaunch_applet_nca = bis_system->GetEntry(QLaunchId, FileSys::ContentRecordType::Program);
m_system.GetFrontendAppletHolder().SetCurrentAppletId(Service::AM::AppletId::QLaunch);
const auto filename = qlaunch_applet_nca->GetFullPath();
auto params = Service::AM::FrontendAppletParameters {
.program_id = QLaunchId,
.applet_id = Service::AM::AppletId::QLaunch,
.applet_type = Service::AM::AppletType::LibraryApplet
};
m_load_result = m_system.Load(EmulationSession::GetInstance().Window(), filename, params);
if (m_load_result != Core::SystemResultStatus::Success) {
return m_load_result;
}
// Complete initialization.
m_system.GPU().Start();
m_system.GetCpuManager().OnGpuReady();
m_system.RegisterExitCallback([&] { HaltEmulation(); });
if (Settings::values.use_disk_shader_cache.GetValue()) {
m_system.Renderer().ReadRasterizer()->LoadDiskResources(
m_system.GetApplicationProcessProgramID(), std::stop_token{},
[](VideoCore::LoadCallbackStage, size_t value, size_t total) {});
}
// Register an ExecuteProgram callback such that Core can execute a sub-program
m_system.RegisterExecuteProgramCallback([&](std::size_t program_index_) {
m_next_program_index = program_index_;
EmulationSession::GetInstance().HaltEmulation();
});
OnEmulationStarted();
return Core::SystemResultStatus::Success;
}
void EmulationSession::ShutdownEmulation() {
std::scoped_lock lock(m_mutex);
if (m_next_program_index != -1) {
ChangeProgram(m_next_program_index);
m_next_program_index = -1;
}
m_is_running = false;
// Unload user input.
m_system.HIDCore().UnloadInputDevices();
// Enable all controllers
m_system.HIDCore().SetSupportedStyleTag({Core::HID::NpadStyleSet::All});
// Shutdown the main emulated process
if (m_load_result == Core::SystemResultStatus::Success) {
m_system.DetachDebugger();
m_system.ShutdownMainProcess();
m_detached_tasks.WaitForAllTasks();
m_load_result = Core::SystemResultStatus::ErrorNotInitialized;
m_window.reset();
OnEmulationStopped(Core::SystemResultStatus::Success);
return;
}
// Tear down the render window.
m_window.reset();
}
void EmulationSession::PauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Pause();
m_is_paused = true;
}
void EmulationSession::UnPauseEmulation() {
std::scoped_lock lock(m_mutex);
m_system.Run();
m_is_paused = false;
}
void EmulationSession::HaltEmulation() {
std::scoped_lock lock(m_mutex);
m_is_running = false;
m_cv.notify_one();
}
void EmulationSession::RunEmulation() {
{
std::scoped_lock lock(m_mutex);
m_is_running = true;
}
// Load the disk shader cache.
if (Settings::values.use_disk_shader_cache.GetValue()) {
LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0);
m_system.Renderer().ReadRasterizer()->LoadDiskResources(
m_system.GetApplicationProcessProgramID(), std::stop_token{}, LoadDiskCacheProgress);
LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0);
}
void(m_system.Run());
if (m_system.DebuggerEnabled()) {
m_system.InitializeDebugger();
}
while (true) {
{
[[maybe_unused]] std::unique_lock lock(m_mutex);
if (m_cv.wait_for(lock, std::chrono::milliseconds(800),
[&]() { return !m_is_running; })) {
// Emulation halted.
break;
}
}
}
// Reset current applet ID.
m_applet_id = static_cast<int>(Service::AM::AppletId::Application);
}
void EmulationSession::LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress,
int max) {
}
void EmulationSession::OnEmulationStarted() {
}
void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
}
void EmulationSession::ChangeProgram(std::size_t program_index) {
LOG_INFO(Frontend, "Trying To Switch Program");
// Halt current emulation session
EmulationSession::GetInstance().HaltEmulation();
// Save the current state if necessary
// Shutdown the current emulation session cleanly
// Update the program index
EmulationSession::GetInstance().m_next_program_index = program_index;
// Initialize the new program
// Start the new emulation session
EmulationSession::GetInstance().RunEmulation();
}
u64 EmulationSession::GetProgramId(std::string programId) {
try {
return std::stoull(programId);
} catch (...) {
return 0;
}
}
static Core::SystemResultStatus RunEmulation(const std::string& filepath,
const size_t program_index,
const bool frontend_initiated) {
MicroProfileOnThreadCreate("EmuThread");
SCOPE_EXIT {
MicroProfileShutdown();
};
LOG_INFO(Frontend, "starting");
if (filepath.empty()) {
LOG_CRITICAL(Frontend, "failed to load: filepath empty!");
return Core::SystemResultStatus::ErrorLoader;
}
SCOPE_EXIT {
EmulationSession::GetInstance().ShutdownEmulation();
};
jconst result = EmulationSession::GetInstance().InitializeEmulation(filepath, program_index,
frontend_initiated);
if (result != Core::SystemResultStatus::Success) {
return result;
}
EmulationSession::GetInstance().RunEmulation();
return Core::SystemResultStatus::Success;
}
bool EmulationSession::IsHandheldOnly() {
jconst npad_style_set = m_system.HIDCore().GetSupportedStyleTag();
if (npad_style_set.fullkey == 1) {
return false;
}
if (npad_style_set.handheld == 0) {
return false;
}
return !Settings::IsDockedMode();
}
void EmulationSession::SetDeviceType([[maybe_unused]] int index, int type) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->SetNpadStyleIndex(static_cast<Core::HID::NpadStyleIndex>(type));
}
void EmulationSession::OnGamepadConnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
// Ensure that player1 is configured correctly and handheld disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Player1) {
jauto handheld = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Handheld);
if (controller->GetNpadStyleIndex() == Core::HID::NpadStyleIndex::Handheld) {
handheld->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Fullkey);
handheld->Disconnect();
}
}
// Ensure that handheld is configured correctly and player 1 disconnected
if (controller->GetNpadIdType() == Core::HID::NpadIdType::Handheld) {
jauto player1 = m_system.HIDCore().GetEmulatedController(Core::HID::NpadIdType::Player1);
if (controller->GetNpadStyleIndex() != Core::HID::NpadStyleIndex::Handheld) {
player1->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
controller->SetNpadStyleIndex(Core::HID::NpadStyleIndex::Handheld);
player1->Disconnect();
}
}
if (!controller->IsConnected()) {
controller->Connect();
}
}
void EmulationSession::OnGamepadDisconnectEvent([[maybe_unused]] int index) {
jauto controller = m_system.HIDCore().GetEmulatedControllerByIndex(index);
controller->Disconnect();
}

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

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationWindow.h - Sudachi
// Created by Jarrod Norwell on 1/18/24.
//
#pragma once
#import <Metal/Metal.hpp>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#include <memory>
#include <span>
#include "core/frontend/emu_window.h"
#include "core/frontend/graphics_context.h"
#include "input_common/main.h"
class GraphicsContext_Apple final : public Core::Frontend::GraphicsContext {
public:
explicit GraphicsContext_Apple(std::shared_ptr<Common::DynamicLibrary> driver_library)
: m_driver_library{driver_library} {}
~GraphicsContext_Apple() = default;
std::shared_ptr<Common::DynamicLibrary> GetDriverLibrary() override {
return m_driver_library;
}
private:
std::shared_ptr<Common::DynamicLibrary> m_driver_library;
};
NS_ASSUME_NONNULL_BEGIN
class EmulationWindow final : public Core::Frontend::EmuWindow {
public:
EmulationWindow(InputCommon::InputSubsystem* input_subsystem, CA::MetalLayer* surface, CGSize size,
std::shared_ptr<Common::DynamicLibrary> driver_library);
~EmulationWindow() = default;
void OnSurfaceChanged(CA::MetalLayer* surface, CGSize size);
void OrientationChanged(UIInterfaceOrientation orientation);
void OnFrameDisplayed() override;
void OnTouchPressed(int id, float x, float y);
void OnTouchMoved(int id, float x, float y);
void OnTouchReleased(int id);
void OnGamepadButtonEvent(int player_index, int button_id, bool pressed);
void OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y);
void OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z);
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override {
return {std::make_unique<GraphicsContext_Apple>(m_driver_library)};
}
bool HasFirstFrame() const {
return m_first_frame;
}
bool IsShown() const override {
return true;
};
private:
float m_window_width{};
float m_window_height{};
CGSize m_size;
bool is_portrait = true;
InputCommon::InputSubsystem* m_input_subsystem{};
std::shared_ptr<Common::DynamicLibrary> m_driver_library;
bool m_first_frame = false;
};
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
//
// EmulationWindow.mm - Sudachi
// Created by Jarrod Norwell on 1/18/24.
//
#import "EmulationWindow.h"
#import "EmulationSession/EmulationSession.h"
#include <SDL.h>
#include "common/logging/log.h"
#include "input_common/drivers/touch_screen.h"
#include "input_common/drivers/virtual_amiibo.h"
#include "input_common/drivers/virtual_gamepad.h"
#include "input_common/main.h"
void EmulationWindow::OnSurfaceChanged(CA::MetalLayer* surface, CGSize size) {
m_size = size;
m_window_width = size.width;
m_window_height = size.height;
// Ensures that we emulate with the correct aspect ratio.
// UpdateCurrentFramebufferLayout(m_window_width, m_window_height);
window_info.render_surface = reinterpret_cast<void*>(surface);
window_info.render_surface_scale = [[UIScreen mainScreen] nativeScale];
}
void EmulationWindow::OrientationChanged(UIInterfaceOrientation orientation) {
is_portrait = orientation == UIInterfaceOrientationPortrait;
}
void EmulationWindow::OnTouchPressed(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchPressed(touch_x,
touch_y, id);
}
void EmulationWindow::OnTouchMoved(int id, float x, float y) {
const auto [touch_x, touch_y] = MapToTouchScreen(x, y);
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchMoved(touch_x,
touch_y, id);
}
void EmulationWindow::OnTouchReleased(int id) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id);
}
void EmulationWindow::OnGamepadButtonEvent(int player_index, int button_id, bool pressed) {
m_input_subsystem->GetVirtualGamepad()->SetButtonState(player_index, button_id, pressed);
}
void EmulationWindow::OnGamepadJoystickEvent(int player_index, int stick_id, float x, float y) {
m_input_subsystem->GetVirtualGamepad()->SetStickPosition(player_index, stick_id, x, y);
}
void EmulationWindow::OnGamepadMotionEvent(int player_index, u64 delta_timestamp, float gyro_x, float gyro_y, float gyro_z, float accel_x, float accel_y, float accel_z) {
m_input_subsystem->GetVirtualGamepad()->SetMotionState(player_index, delta_timestamp, gyro_x, gyro_y, gyro_z, accel_x, accel_y, accel_z);
}
void EmulationWindow::OnFrameDisplayed() {
if (!m_first_frame) {
m_first_frame = true;
}
}
EmulationWindow::EmulationWindow(InputCommon::InputSubsystem* input_subsystem, CA::MetalLayer* surface, CGSize size, std::shared_ptr<Common::DynamicLibrary> driver_library) : m_input_subsystem{input_subsystem}, m_size{size}, m_driver_library{driver_library} {
LOG_INFO(Frontend, "initializing");
if (!surface) {
LOG_CRITICAL(Frontend, "surface is nullptr");
return;
}
OnSurfaceChanged(surface, m_size);
window_info.render_surface_scale = [[UIScreen mainScreen] nativeScale];
window_info.type = Core::Frontend::WindowSystemType::Cocoa;
m_input_subsystem->Initialize();
}

View file

@ -425,6 +425,9 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent)
"their resolution, details and supported controllers and depending on this setting.\n" "their resolution, details and supported controllers and depending on this setting.\n"
"Setting to Handheld can help improve performance for low end systems.")); "Setting to Handheld can help improve performance for low end systems."));
INSERT(Settings, current_user, QString(), QString()); INSERT(Settings, current_user, QString(), QString());
INSERT(Settings, serial_unit, tr("Unit Serial"), QString());
INSERT(Settings, serial_battery, tr("Battery Serial"), QString());
INSERT(Settings, debug_knobs, tr("Debug knobs"), QString());
// Controls // Controls

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-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -91,6 +94,17 @@ void AlphaTest(EmitContext& ctx) {
} // Anonymous namespace } // Anonymous namespace
void EmitPrologue(EmitContext& ctx) { void EmitPrologue(EmitContext& ctx) {
if (ctx.stage == Stage::Fragment && ctx.runtime_info.dual_source_blend) {
// Initialize dual-source blending outputs - prevents MoltenVK crash.
const Id zero{ctx.Const(0.0f)};
const Id one{ctx.Const(1.0f)};
const Id default_color{ctx.ConstantComposite(ctx.F32[4], zero, zero, zero, one)};
for (u32 i = 0; i < 2; ++i) {
if (Sirit::ValidId(ctx.frag_color[i])) {
ctx.OpStore(ctx.frag_color[i], default_color);
}
}
}
if (ctx.stage == Stage::VertexB) { if (ctx.stage == Stage::VertexB) {
const Id zero{ctx.Const(0.0f)}; const Id zero{ctx.Const(0.0f)};
const Id one{ctx.Const(1.0f)}; const Id one{ctx.Const(1.0f)};

View file

@ -1670,14 +1670,23 @@ void EmitContext::DefineOutputs(const IR::Program& program) {
break; break;
case Stage::Fragment: case Stage::Fragment:
for (u32 index = 0; index < 8; ++index) { for (u32 index = 0; index < 8; ++index) {
if (!info.stores_frag_color[index] && !profile.need_declared_frag_colors) { const bool need_dual_source = runtime_info.dual_source_blend && index <= 1;
if (!need_dual_source && !info.stores_frag_color[index] &&
!profile.need_declared_frag_colors) {
continue; continue;
} }
const Id type{GetAttributeType(*this, runtime_info.color_output_types[index])}; const Id type{GetAttributeType(*this, runtime_info.color_output_types[index])};
frag_color[index] = DefineOutput(*this, type, std::nullopt); frag_color[index] = DefineOutput(*this, type, std::nullopt);
// Correct mapping for dual-source blending
if (runtime_info.dual_source_blend && index <= 1) {
Decorate(frag_color[index], spv::Decoration::Location, 0u);
Decorate(frag_color[index], spv::Decoration::Index, index);
Name(frag_color[index], index == 0 ? "frag_color0" : "frag_color0_secondary");
} else {
Decorate(frag_color[index], spv::Decoration::Location, index); Decorate(frag_color[index], spv::Decoration::Location, index);
Name(frag_color[index], fmt::format("frag_color{}", index)); Name(frag_color[index], fmt::format("frag_color{}", index));
} }
}
if (info.stores_frag_depth) { if (info.stores_frag_depth) {
frag_depth = DefineOutput(*this, F32[1], std::nullopt); frag_depth = DefineOutput(*this, F32[1], std::nullopt);
Decorate(frag_depth, spv::Decoration::BuiltIn, spv::BuiltIn::FragDepth); Decorate(frag_depth, spv::Decoration::BuiltIn, spv::BuiltIn::FragDepth);

View file

@ -110,6 +110,9 @@ struct RuntimeInfo {
/// Output types for each color attachment /// Output types for each color attachment
std::array<AttributeType, 8> color_output_types{}; std::array<AttributeType, 8> color_output_types{};
/// Dual source blending
bool dual_source_blend{};
}; };
} // namespace Shader } // namespace Shader

View file

@ -14,9 +14,12 @@
#include <mutex> #include <mutex>
#include <numeric> #include <numeric>
#include <span> #include <span>
#include <ankerl/unordered_dense.h>
#include <vector> #include <vector>
#include <ankerl/unordered_dense.h>
#include <boost/container/static_vector.hpp>
#include <boost/container/small_vector.hpp>
#include "common/common_types.h" #include "common/common_types.h"
#include "common/div_ceil.h" #include "common/div_ceil.h"
#include "common/literals.h" #include "common/literals.h"
@ -94,10 +97,10 @@ static constexpr Binding NULL_BINDING{
template <typename Buffer> template <typename Buffer>
struct HostBindings { struct HostBindings {
boost::container::small_vector<Buffer*, NUM_VERTEX_BUFFERS> buffers; boost::container::static_vector<Buffer*, NUM_VERTEX_BUFFERS> buffers;
boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> offsets; boost::container::static_vector<u64, NUM_VERTEX_BUFFERS> offsets;
boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> sizes; boost::container::static_vector<u64, NUM_VERTEX_BUFFERS> sizes;
boost::container::small_vector<u64, NUM_VERTEX_BUFFERS> strides; boost::container::static_vector<u64, NUM_VERTEX_BUFFERS> strides;
u32 min_index{NUM_VERTEX_BUFFERS}; u32 min_index{NUM_VERTEX_BUFFERS};
u32 max_index{0}; u32 max_index{0};
}; };

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project // SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -19,12 +22,12 @@ ChannelState::ChannelState(s32 bind_id_) : bind_id{bind_id_}, initialized{} {}
void ChannelState::Init(Core::System& system, GPU& gpu, u64 program_id_) { void ChannelState::Init(Core::System& system, GPU& gpu, u64 program_id_) {
ASSERT(memory_manager); ASSERT(memory_manager);
program_id = program_id_; program_id = program_id_;
dma_pusher = std::make_unique<Tegra::DmaPusher>(system, gpu, *memory_manager, *this); dma_pusher.emplace(system, gpu, *memory_manager, *this);
maxwell_3d = std::make_unique<Engines::Maxwell3D>(system, *memory_manager); maxwell_3d.emplace(system, *memory_manager);
fermi_2d = std::make_unique<Engines::Fermi2D>(*memory_manager); fermi_2d.emplace(*memory_manager);
kepler_compute = std::make_unique<Engines::KeplerCompute>(system, *memory_manager); kepler_compute.emplace(system, *memory_manager);
maxwell_dma = std::make_unique<Engines::MaxwellDMA>(system, *memory_manager); maxwell_dma.emplace(system, *memory_manager);
kepler_memory = std::make_unique<Engines::KeplerMemory>(system, *memory_manager); kepler_memory.emplace(system, *memory_manager);
initialized = true; initialized = true;
} }

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project // SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -6,6 +9,12 @@
#include <memory> #include <memory>
#include "common/common_types.h" #include "common/common_types.h"
#include "video_core/engines/fermi_2d.h"
#include "video_core/engines/kepler_memory.h"
#include "video_core/engines/kepler_compute.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/engines/maxwell_dma.h"
#include "video_core/dma_pusher.h"
namespace Core { namespace Core {
class System; class System;
@ -18,49 +27,34 @@ class RasterizerInterface;
namespace Tegra { namespace Tegra {
class GPU; class GPU;
namespace Engines {
class Puller;
class Fermi2D;
class Maxwell3D;
class MaxwellDMA;
class KeplerCompute;
class KeplerMemory;
} // namespace Engines
class MemoryManager; class MemoryManager;
class DmaPusher;
namespace Control { namespace Control {
struct ChannelState { struct ChannelState {
explicit ChannelState(s32 bind_id); explicit ChannelState(s32 bind_id);
ChannelState(const ChannelState& state) = delete;
ChannelState& operator=(const ChannelState&) = delete;
ChannelState(ChannelState&& other) noexcept = default;
ChannelState& operator=(ChannelState&& other) noexcept = default;
void Init(Core::System& system, GPU& gpu, u64 program_id); void Init(Core::System& system, GPU& gpu, u64 program_id);
void BindRasterizer(VideoCore::RasterizerInterface* rasterizer); void BindRasterizer(VideoCore::RasterizerInterface* rasterizer);
s32 bind_id = -1;
u64 program_id = 0;
/// 3D engine /// 3D engine
std::unique_ptr<Engines::Maxwell3D> maxwell_3d; std::optional<Engines::Maxwell3D> maxwell_3d;
/// 2D engine /// 2D engine
std::unique_ptr<Engines::Fermi2D> fermi_2d; std::optional<Engines::Fermi2D> fermi_2d;
/// Compute engine /// Compute engine
std::unique_ptr<Engines::KeplerCompute> kepler_compute; std::optional<Engines::KeplerCompute> kepler_compute;
/// DMA engine /// DMA engine
std::unique_ptr<Engines::MaxwellDMA> maxwell_dma; std::optional<Engines::MaxwellDMA> maxwell_dma;
/// Inline memory engine /// Inline memory engine
std::unique_ptr<Engines::KeplerMemory> kepler_memory; std::optional<Engines::KeplerMemory> kepler_memory;
/// NV01 Timer
std::optional<Engines::KeplerMemory> nv01_timer;
std::optional<DmaPusher> dma_pusher;
std::shared_ptr<MemoryManager> memory_manager; std::shared_ptr<MemoryManager> memory_manager;
std::unique_ptr<DmaPusher> dma_pusher; s32 bind_id = -1;
u64 program_id = 0;
bool initialized{}; bool initialized{};
}; };

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@ -15,6 +15,7 @@
namespace Tegra::Engines { namespace Tegra::Engines {
enum class EngineTypes : u32 { enum class EngineTypes : u32 {
Nv01Timer,
KeplerCompute, KeplerCompute,
Maxwell3D, Maxwell3D,
Fermi2D, Fermi2D,

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@ -26,8 +26,15 @@ namespace Tegra::Engines {
constexpr u32 MacroRegistersStart = 0xE00; constexpr u32 MacroRegistersStart = 0xE00;
Maxwell3D::Maxwell3D(Core::System& system_, MemoryManager& memory_manager_) Maxwell3D::Maxwell3D(Core::System& system_, MemoryManager& memory_manager_)
: draw_manager{std::make_unique<DrawManager>(this)}, system{system_}, : draw_manager{std::make_unique<DrawManager>(this)}, system{system_}
memory_manager{memory_manager_}, macro_engine{GetMacroEngine(*this)}, upload_state{memory_manager, regs.upload} { , memory_manager{memory_manager_}
#ifdef ARCHITECTURE_x86_64
, macro_engine(bool(Settings::values.disable_macro_jit))
#else
, macro_engine(true)
#endif
, upload_state{memory_manager, regs.upload}
{
dirty.flags.flip(); dirty.flags.flip();
InitializeRegisterDefaults(); InitializeRegisterDefaults();
execution_mask.reset(); execution_mask.reset();
@ -328,9 +335,9 @@ void Maxwell3D::ProcessMethodCall(u32 method, u32 argument, u32 nonshadow_argume
shadow_state.shadow_ram_control = static_cast<Regs::ShadowRamControl>(nonshadow_argument); shadow_state.shadow_ram_control = static_cast<Regs::ShadowRamControl>(nonshadow_argument);
return; return;
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr): case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
return macro_engine->ClearCode(regs.load_mme.instruction_ptr); return macro_engine.ClearCode(regs.load_mme.instruction_ptr);
case MAXWELL3D_REG_INDEX(load_mme.instruction): case MAXWELL3D_REG_INDEX(load_mme.instruction):
return macro_engine->AddCode(regs.load_mme.instruction_ptr, argument); return macro_engine.AddCode(regs.load_mme.instruction_ptr, argument);
case MAXWELL3D_REG_INDEX(load_mme.start_address): case MAXWELL3D_REG_INDEX(load_mme.start_address):
return ProcessMacroBind(argument); return ProcessMacroBind(argument);
case MAXWELL3D_REG_INDEX(falcon[4]): case MAXWELL3D_REG_INDEX(falcon[4]):
@ -398,7 +405,7 @@ void Maxwell3D::CallMacroMethod(u32 method, const std::vector<u32>& parameters)
((method - MacroRegistersStart) >> 1) % static_cast<u32>(macro_positions.size()); ((method - MacroRegistersStart) >> 1) % static_cast<u32>(macro_positions.size());
// Execute the current macro. // Execute the current macro.
macro_engine->Execute(macro_positions[entry], parameters); macro_engine.Execute(*this, macro_positions[entry], parameters);
draw_manager->DrawDeferred(); draw_manager->DrawDeferred();
} }
@ -464,7 +471,7 @@ void Maxwell3D::CallMultiMethod(u32 method, const u32* base_start, u32 amount,
} }
void Maxwell3D::ProcessMacroUpload(u32 data) { void Maxwell3D::ProcessMacroUpload(u32 data) {
macro_engine->AddCode(regs.load_mme.instruction_ptr++, data); macro_engine.AddCode(regs.load_mme.instruction_ptr++, data);
} }
void Maxwell3D::ProcessMacroBind(u32 data) { void Maxwell3D::ProcessMacroBind(u32 data) {

View file

@ -2258,7 +2258,7 @@ public:
/// Returns whether the vertex array specified by index is supposed to be /// Returns whether the vertex array specified by index is supposed to be
/// accessed per instance or not. /// accessed per instance or not.
bool IsInstancingEnabled(std::size_t index) const { bool IsInstancingEnabled(std::size_t index) const {
return is_instanced[index]; return bool(is_instanced[index]); //FUCK YOU MSVC
} }
}; };
@ -3203,7 +3203,7 @@ private:
std::vector<u32> macro_params; std::vector<u32> macro_params;
/// Interpreter for the macro codes uploaded to the GPU. /// Interpreter for the macro codes uploaded to the GPU.
std::optional<MacroEngine> macro_engine; MacroEngine macro_engine;
Upload::State upload_state; Upload::State upload_state;

View file

@ -0,0 +1,52 @@
// 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
#pragma once
#include <array>
#include <cstddef>
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/logging/log.h"
#include "video_core/engines/engine_interface.h"
#include "video_core/engines/engine_upload.h"
namespace Core {
class System;
}
namespace Tegra {
class MemoryManager;
}
namespace Tegra::Engines {
class Nv01Timer final : public EngineInterface {
public:
explicit Nv01Timer(Core::System& system_, MemoryManager& memory_manager)
: system{system_}
{}
~Nv01Timer() override;
/// Write the value to the register identified by method.
void CallMethod(u32 method, u32 method_argument, bool is_last_call) override {
LOG_DEBUG(HW_GPU, "method={}, argument={}, is_last_call={}", method, method_argument, is_last_call);
}
/// Write multiple values to the register identified by method.
void CallMultiMethod(u32 method, const u32* base_start, u32 amount, u32 methods_pending) override {
LOG_DEBUG(HW_GPU, "method={}, base_start={}, amount={}, pending={}", method, fmt::ptr(base_start), amount, methods_pending);
}
struct Regs {
// No fucking idea
INSERT_PADDING_BYTES_NOINIT(0x48);
} regs{};
private:
void ConsumeSinkImpl() override {}
Core::System& system;
};
}

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project // SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -34,24 +37,22 @@ void Puller::ProcessBindMethod(const MethodCall& method_call) {
bound_engines[method_call.subchannel] = engine_id; bound_engines[method_call.subchannel] = engine_id;
switch (engine_id) { switch (engine_id) {
case EngineID::FERMI_TWOD_A: case EngineID::FERMI_TWOD_A:
dma_pusher.BindSubchannel(channel_state.fermi_2d.get(), method_call.subchannel, dma_pusher.BindSubchannel(&*channel_state.fermi_2d, method_call.subchannel, EngineTypes::Fermi2D);
EngineTypes::Fermi2D);
break; break;
case EngineID::MAXWELL_B: case EngineID::MAXWELL_B:
dma_pusher.BindSubchannel(channel_state.maxwell_3d.get(), method_call.subchannel, dma_pusher.BindSubchannel(&*channel_state.maxwell_3d, method_call.subchannel, EngineTypes::Maxwell3D);
EngineTypes::Maxwell3D);
break; break;
case EngineID::KEPLER_COMPUTE_B: case EngineID::KEPLER_COMPUTE_B:
dma_pusher.BindSubchannel(channel_state.kepler_compute.get(), method_call.subchannel, dma_pusher.BindSubchannel(&*channel_state.kepler_compute, method_call.subchannel, EngineTypes::KeplerCompute);
EngineTypes::KeplerCompute);
break; break;
case EngineID::MAXWELL_DMA_COPY_A: case EngineID::MAXWELL_DMA_COPY_A:
dma_pusher.BindSubchannel(channel_state.maxwell_dma.get(), method_call.subchannel, dma_pusher.BindSubchannel(&*channel_state.maxwell_dma, method_call.subchannel, EngineTypes::MaxwellDMA);
EngineTypes::MaxwellDMA);
break; break;
case EngineID::KEPLER_INLINE_TO_MEMORY_B: case EngineID::KEPLER_INLINE_TO_MEMORY_B:
dma_pusher.BindSubchannel(channel_state.kepler_memory.get(), method_call.subchannel, dma_pusher.BindSubchannel(&*channel_state.kepler_memory, method_call.subchannel, EngineTypes::KeplerMemory);
EngineTypes::KeplerMemory); break;
case EngineID::NV01_TIMER:
dma_pusher.BindSubchannel(&*channel_state.nv01_timer, method_call.subchannel, EngineTypes::Nv01Timer);
break; break;
default: default:
UNIMPLEMENTED_MSG("Unimplemented engine {:04X}", engine_id); UNIMPLEMENTED_MSG("Unimplemented engine {:04X}", engine_id);
@ -209,24 +210,22 @@ void Puller::CallEngineMethod(const MethodCall& method_call) {
switch (engine) { switch (engine) {
case EngineID::FERMI_TWOD_A: case EngineID::FERMI_TWOD_A:
channel_state.fermi_2d->CallMethod(method_call.method, method_call.argument, channel_state.fermi_2d->CallMethod(method_call.method, method_call.argument, method_call.IsLastCall());
method_call.IsLastCall());
break; break;
case EngineID::MAXWELL_B: case EngineID::MAXWELL_B:
channel_state.maxwell_3d->CallMethod(method_call.method, method_call.argument, channel_state.maxwell_3d->CallMethod(method_call.method, method_call.argument, method_call.IsLastCall());
method_call.IsLastCall());
break; break;
case EngineID::KEPLER_COMPUTE_B: case EngineID::KEPLER_COMPUTE_B:
channel_state.kepler_compute->CallMethod(method_call.method, method_call.argument, channel_state.kepler_compute->CallMethod(method_call.method, method_call.argument, method_call.IsLastCall());
method_call.IsLastCall());
break; break;
case EngineID::MAXWELL_DMA_COPY_A: case EngineID::MAXWELL_DMA_COPY_A:
channel_state.maxwell_dma->CallMethod(method_call.method, method_call.argument, channel_state.maxwell_dma->CallMethod(method_call.method, method_call.argument, method_call.IsLastCall());
method_call.IsLastCall());
break; break;
case EngineID::KEPLER_INLINE_TO_MEMORY_B: case EngineID::KEPLER_INLINE_TO_MEMORY_B:
channel_state.kepler_memory->CallMethod(method_call.method, method_call.argument, channel_state.kepler_memory->CallMethod(method_call.method, method_call.argument, method_call.IsLastCall());
method_call.IsLastCall()); break;
case EngineID::NV01_TIMER:
channel_state.nv01_timer->CallMethod(method_call.method, method_call.argument, method_call.IsLastCall());
break; break;
default: default:
UNIMPLEMENTED_MSG("Unimplemented engine"); UNIMPLEMENTED_MSG("Unimplemented engine");
@ -255,6 +254,9 @@ void Puller::CallEngineMultiMethod(u32 method, u32 subchannel, const u32* base_s
case EngineID::KEPLER_INLINE_TO_MEMORY_B: case EngineID::KEPLER_INLINE_TO_MEMORY_B:
channel_state.kepler_memory->CallMultiMethod(method, base_start, amount, methods_pending); channel_state.kepler_memory->CallMultiMethod(method, base_start, amount, methods_pending);
break; break;
case EngineID::NV01_TIMER:
channel_state.nv01_timer->CallMultiMethod(method, base_start, amount, methods_pending);
break;
default: default:
UNIMPLEMENTED_MSG("Unimplemented engine"); UNIMPLEMENTED_MSG("Unimplemented engine");
break; break;

View file

@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project // SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
@ -20,6 +23,7 @@ class MemoryManager;
class DmaPusher; class DmaPusher;
enum class EngineID { enum class EngineID {
NV01_TIMER = 0x0004,
FERMI_TWOD_A = 0x902D, // 2D Engine FERMI_TWOD_A = 0x902D, // 2D Engine
MAXWELL_B = 0xB197, // 3D Engine MAXWELL_B = 0xB197, // 3D Engine
KEPLER_COMPUTE_B = 0xB1C0, KEPLER_COMPUTE_B = 0xB1C0,

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@ -10,6 +10,7 @@
#include <span> #include <span>
#include <fstream> #include <fstream>
#include <variant>
#ifdef ARCHITECTURE_x86_64 #ifdef ARCHITECTURE_x86_64
// xbyak hates human beings // xbyak hates human beings
#ifdef __GNUC__ #ifdef __GNUC__
@ -73,26 +74,12 @@ bool IsTopologySafe(Maxwell3D::Regs::PrimitiveTopology topology) {
} }
} }
class HLEMacroImpl : public CachedMacro { } // Anonymous namespace
public:
explicit HLEMacroImpl(Maxwell3D& maxwell3d_)
: CachedMacro(maxwell3d_)
{}
};
/// @note: these macros have two versions, a normal and extended version, with the extended version void HLE_DrawArraysIndirect::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
/// also assigning the base vertex/instance.
template <bool extended>
class HLE_DrawArraysIndirect final : public HLEMacroImpl {
public:
explicit HLE_DrawArraysIndirect(Maxwell3D& maxwell3d_)
: HLEMacroImpl(maxwell3d_)
{}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0]); auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0]);
if (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology)) { if (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology)) {
Fallback(parameters); Fallback(maxwell3d, parameters);
return; return;
} }
@ -118,9 +105,7 @@ public:
maxwell3d.replace_table.clear(); maxwell3d.replace_table.clear();
} }
} }
void HLE_DrawArraysIndirect::Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters) {
private:
void Fallback(const std::vector<u32>& parameters) {
SCOPE_EXIT { SCOPE_EXIT {
if (extended) { if (extended) {
maxwell3d.engine_state = Maxwell3D::EngineHint::None; maxwell3d.engine_state = Maxwell3D::EngineHint::None;
@ -129,52 +114,35 @@ private:
}; };
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
const u32 instance_count = (maxwell3d.GetRegisterValue(0xD1B) & parameters[2]); const u32 instance_count = (maxwell3d.GetRegisterValue(0xD1B) & parameters[2]);
auto topology = Maxwell3D::Regs::PrimitiveTopology(parameters[0]);
auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0]);
const u32 vertex_first = parameters[3]; const u32 vertex_first = parameters[3];
const u32 vertex_count = parameters[1]; const u32 vertex_count = parameters[1];
if (!IsTopologySafe(topology) && size_t(maxwell3d.GetMaxCurrentVertices()) < size_t(vertex_first) + size_t(vertex_count)) { if (!IsTopologySafe(topology) && size_t(maxwell3d.GetMaxCurrentVertices()) < size_t(vertex_first) + size_t(vertex_count)) {
ASSERT(false && "Faulty draw!"); ASSERT(false && "Faulty draw!");
return; return;
} }
const u32 base_instance = parameters[4]; const u32 base_instance = parameters[4];
if (extended) { if (extended) {
maxwell3d.regs.global_base_instance_index = base_instance; maxwell3d.regs.global_base_instance_index = base_instance;
maxwell3d.engine_state = Maxwell3D::EngineHint::OnHLEMacro; maxwell3d.engine_state = Maxwell3D::EngineHint::OnHLEMacro;
maxwell3d.SetHLEReplacementAttributeType( maxwell3d.SetHLEReplacementAttributeType(0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
} }
maxwell3d.draw_manager->DrawArray(topology, vertex_first, vertex_count, base_instance, instance_count);
maxwell3d.draw_manager->DrawArray(topology, vertex_first, vertex_count, base_instance,
instance_count);
if (extended) { if (extended) {
maxwell3d.regs.global_base_instance_index = 0; maxwell3d.regs.global_base_instance_index = 0;
maxwell3d.engine_state = Maxwell3D::EngineHint::None; maxwell3d.engine_state = Maxwell3D::EngineHint::None;
maxwell3d.replace_table.clear(); maxwell3d.replace_table.clear();
} }
} }
};
/* void HLE_DrawIndexedIndirect::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
* @note: these macros have two versions, a normal and extended version, with the extended version
* also assigning the base vertex/instance.
*/
template <bool extended>
class HLE_DrawIndexedIndirect final : public HLEMacroImpl {
public:
explicit HLE_DrawIndexedIndirect(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0]); auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0]);
if (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology)) { if (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology)) {
Fallback(parameters); Fallback(maxwell3d, parameters);
return; return;
} }
const u32 estimate = static_cast<u32>(maxwell3d.EstimateIndexBufferSize()); const u32 estimate = u32(maxwell3d.EstimateIndexBufferSize());
const u32 element_base = parameters[4]; const u32 element_base = parameters[4];
const u32 base_instance = parameters[5]; const u32 base_instance = parameters[5];
maxwell3d.regs.vertex_id_base = element_base; maxwell3d.regs.vertex_id_base = element_base;
@ -205,9 +173,7 @@ public:
maxwell3d.replace_table.clear(); maxwell3d.replace_table.clear();
} }
} }
void HLE_DrawIndexedIndirect::Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters) {
private:
void Fallback(const std::vector<u32>& parameters) {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
const u32 instance_count = (maxwell3d.GetRegisterValue(0xD1B) & parameters[2]); const u32 instance_count = (maxwell3d.GetRegisterValue(0xD1B) & parameters[2]);
const u32 element_base = parameters[4]; const u32 element_base = parameters[4];
@ -221,9 +187,7 @@ private:
maxwell3d.SetHLEReplacementAttributeType(0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseVertex); maxwell3d.SetHLEReplacementAttributeType(0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseVertex);
maxwell3d.SetHLEReplacementAttributeType(0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance); maxwell3d.SetHLEReplacementAttributeType(0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
} }
maxwell3d.draw_manager->DrawIndex(Tegra::Maxwell3D::Regs::PrimitiveTopology(parameters[0]), parameters[3], parameters[1], element_base, base_instance, instance_count); maxwell3d.draw_manager->DrawIndex(Tegra::Maxwell3D::Regs::PrimitiveTopology(parameters[0]), parameters[3], parameters[1], element_base, base_instance, instance_count);
maxwell3d.regs.vertex_id_base = 0x0; maxwell3d.regs.vertex_id_base = 0x0;
maxwell3d.regs.global_base_vertex_index = 0x0; maxwell3d.regs.global_base_vertex_index = 0x0;
maxwell3d.regs.global_base_instance_index = 0x0; maxwell3d.regs.global_base_instance_index = 0x0;
@ -232,13 +196,7 @@ private:
maxwell3d.replace_table.clear(); maxwell3d.replace_table.clear();
} }
} }
}; void HLE_MultiLayerClear::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_MultiLayerClear final : public HLEMacroImpl {
public:
explicit HLE_MultiLayerClear(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
ASSERT(parameters.size() == 1); ASSERT(parameters.size() == 1);
@ -250,16 +208,10 @@ public:
maxwell3d.regs.clear_surface.raw = clear_params.raw; maxwell3d.regs.clear_surface.raw = clear_params.raw;
maxwell3d.draw_manager->Clear(num_layers); maxwell3d.draw_manager->Clear(num_layers);
} }
}; void HLE_MultiDrawIndexedIndirectCount::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_MultiDrawIndexedIndirectCount final : public HLEMacroImpl {
public:
explicit HLE_MultiDrawIndexedIndirectCount(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
const auto topology = Maxwell3D::Regs::PrimitiveTopology(parameters[2]); const auto topology = Maxwell3D::Regs::PrimitiveTopology(parameters[2]);
if (!IsTopologySafe(topology)) { if (!IsTopologySafe(topology)) {
Fallback(parameters); Fallback(maxwell3d, parameters);
return; return;
} }
@ -289,19 +241,14 @@ public:
params.stride = stride; params.stride = stride;
maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true; maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true;
maxwell3d.engine_state = Maxwell3D::EngineHint::OnHLEMacro; maxwell3d.engine_state = Maxwell3D::EngineHint::OnHLEMacro;
maxwell3d.SetHLEReplacementAttributeType( maxwell3d.SetHLEReplacementAttributeType(0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseVertex);
0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseVertex); maxwell3d.SetHLEReplacementAttributeType(0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
maxwell3d.SetHLEReplacementAttributeType( maxwell3d.SetHLEReplacementAttributeType(0, 0x648, Maxwell3D::HLEReplacementAttributeType::DrawID);
0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
maxwell3d.SetHLEReplacementAttributeType(0, 0x648,
Maxwell3D::HLEReplacementAttributeType::DrawID);
maxwell3d.draw_manager->DrawIndexedIndirect(topology, 0, estimate); maxwell3d.draw_manager->DrawIndexedIndirect(topology, 0, estimate);
maxwell3d.engine_state = Maxwell3D::EngineHint::None; maxwell3d.engine_state = Maxwell3D::EngineHint::None;
maxwell3d.replace_table.clear(); maxwell3d.replace_table.clear();
} }
void HLE_MultiDrawIndexedIndirectCount::Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters) {
private:
void Fallback(const std::vector<u32>& parameters) {
SCOPE_EXIT { SCOPE_EXIT {
// Clean everything. // Clean everything.
maxwell3d.regs.vertex_id_base = 0x0; maxwell3d.regs.vertex_id_base = 0x0;
@ -318,41 +265,29 @@ private:
const auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[2]); const auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[2]);
const u32 padding = parameters[3]; const u32 padding = parameters[3];
const std::size_t max_draws = parameters[4]; const std::size_t max_draws = parameters[4];
const u32 indirect_words = 5 + padding; const u32 indirect_words = 5 + padding;
const std::size_t first_draw = start_indirect; const std::size_t first_draw = start_indirect;
const std::size_t effective_draws = end_indirect - start_indirect; const std::size_t effective_draws = end_indirect - start_indirect;
const std::size_t last_draw = start_indirect + (std::min)(effective_draws, max_draws); const std::size_t last_draw = start_indirect + (std::min)(effective_draws, max_draws);
for (std::size_t index = first_draw; index < last_draw; index++) { for (std::size_t index = first_draw; index < last_draw; index++) {
const std::size_t base = index * indirect_words + 5; const std::size_t base = index * indirect_words + 5;
const u32 base_vertex = parameters[base + 3]; const u32 base_vertex = parameters[base + 3];
const u32 base_instance = parameters[base + 4]; const u32 base_instance = parameters[base + 4];
maxwell3d.regs.vertex_id_base = base_vertex; maxwell3d.regs.vertex_id_base = base_vertex;
maxwell3d.engine_state = Maxwell3D::EngineHint::OnHLEMacro; maxwell3d.engine_state = Maxwell3D::EngineHint::OnHLEMacro;
maxwell3d.SetHLEReplacementAttributeType( maxwell3d.SetHLEReplacementAttributeType(0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseVertex);
0, 0x640, Maxwell3D::HLEReplacementAttributeType::BaseVertex); maxwell3d.SetHLEReplacementAttributeType(0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
maxwell3d.SetHLEReplacementAttributeType(
0, 0x644, Maxwell3D::HLEReplacementAttributeType::BaseInstance);
maxwell3d.CallMethod(0x8e3, 0x648, true); maxwell3d.CallMethod(0x8e3, 0x648, true);
maxwell3d.CallMethod(0x8e4, static_cast<u32>(index), true); maxwell3d.CallMethod(0x8e4, static_cast<u32>(index), true);
maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true; maxwell3d.dirty.flags[VideoCommon::Dirty::IndexBuffer] = true;
maxwell3d.draw_manager->DrawIndex(topology, parameters[base + 2], parameters[base], maxwell3d.draw_manager->DrawIndex(topology, parameters[base + 2], parameters[base], base_vertex, base_instance, parameters[base + 1]);
base_vertex, base_instance, parameters[base + 1]);
} }
} }
}; void HLE_DrawIndirectByteCount::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_DrawIndirectByteCount final : public HLEMacroImpl {
public:
explicit HLE_DrawIndirectByteCount(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
const bool force = maxwell3d.Rasterizer().HasDrawTransformFeedback(); const bool force = maxwell3d.Rasterizer().HasDrawTransformFeedback();
auto topology = Maxwell3D::Regs::PrimitiveTopology(parameters[0] & 0xFFFFU);
auto topology = static_cast<Maxwell3D::Regs::PrimitiveTopology>(parameters[0] & 0xFFFFU);
if (!force && (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology))) { if (!force && (!maxwell3d.AnyParametersDirty() || !IsTopologySafe(topology))) {
Fallback(parameters); Fallback(maxwell3d, parameters);
return; return;
} }
auto& params = maxwell3d.draw_manager->GetIndirectParams(); auto& params = maxwell3d.draw_manager->GetIndirectParams();
@ -367,12 +302,9 @@ public:
maxwell3d.regs.draw.begin = parameters[0]; maxwell3d.regs.draw.begin = parameters[0];
maxwell3d.regs.draw_auto_stride = parameters[1]; maxwell3d.regs.draw_auto_stride = parameters[1];
maxwell3d.regs.draw_auto_byte_count = parameters[2]; maxwell3d.regs.draw_auto_byte_count = parameters[2];
maxwell3d.draw_manager->DrawArrayIndirect(topology); maxwell3d.draw_manager->DrawArrayIndirect(topology);
} }
void HLE_DrawIndirectByteCount::Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters) {
private:
void Fallback(const std::vector<u32>& parameters) {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
maxwell3d.regs.draw.begin = parameters[0]; maxwell3d.regs.draw.begin = parameters[0];
@ -383,13 +315,7 @@ private:
maxwell3d.regs.draw.topology, 0, maxwell3d.regs.draw.topology, 0,
maxwell3d.regs.draw_auto_byte_count / maxwell3d.regs.draw_auto_stride, 0, 1); maxwell3d.regs.draw_auto_byte_count / maxwell3d.regs.draw_auto_stride, 0, 1);
} }
}; void HLE_C713C83D8F63CCF3::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_C713C83D8F63CCF3 final : public HLEMacroImpl {
public:
explicit HLE_C713C83D8F63CCF3(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
const u32 offset = (parameters[0] & 0x3FFFFFFF) << 2; const u32 offset = (parameters[0] & 0x3FFFFFFF) << 2;
const u32 address = maxwell3d.regs.shadow_scratch[24]; const u32 address = maxwell3d.regs.shadow_scratch[24];
@ -399,13 +325,7 @@ public:
const_buffer.address_low = address << 8; const_buffer.address_low = address << 8;
const_buffer.offset = offset; const_buffer.offset = offset;
} }
}; void HLE_D7333D26E0A93EDE::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_D7333D26E0A93EDE final : public HLEMacroImpl {
public:
explicit HLE_D7333D26E0A93EDE(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
const size_t index = parameters[0]; const size_t index = parameters[0];
const u32 address = maxwell3d.regs.shadow_scratch[42 + index]; const u32 address = maxwell3d.regs.shadow_scratch[42 + index];
@ -415,13 +335,7 @@ public:
const_buffer.address_high = (address >> 24) & 0xFF; const_buffer.address_high = (address >> 24) & 0xFF;
const_buffer.address_low = address << 8; const_buffer.address_low = address << 8;
} }
}; void HLE_BindShader::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_BindShader final : public HLEMacroImpl {
public:
explicit HLE_BindShader(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
auto& regs = maxwell3d.regs; auto& regs = maxwell3d.regs;
const u32 index = parameters[0]; const u32 index = parameters[0];
@ -445,13 +359,7 @@ public:
bind_group.raw_config = 0x11; bind_group.raw_config = 0x11;
maxwell3d.ProcessCBBind(bind_group_id); maxwell3d.ProcessCBBind(bind_group_id);
} }
}; void HLE_SetRasterBoundingBox::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_SetRasterBoundingBox final : public HLEMacroImpl {
public:
explicit HLE_SetRasterBoundingBox(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
const u32 raster_mode = parameters[0]; const u32 raster_mode = parameters[0];
auto& regs = maxwell3d.regs; auto& regs = maxwell3d.regs;
@ -460,16 +368,9 @@ public:
regs.raster_bounding_box.raw = raster_mode & 0xFFFFF00F; regs.raster_bounding_box.raw = raster_mode & 0xFFFFF00F;
regs.raster_bounding_box.pad.Assign(scratch_data & raster_enabled); regs.raster_bounding_box.pad.Assign(scratch_data & raster_enabled);
} }
}; void HLE_ClearConstBuffer::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
static constexpr std::array<u32, 0x7000> zeroes{}; //must be bigger than either 7000 or 5F00
template <size_t base_size>
class HLE_ClearConstBuffer final : public HLEMacroImpl {
public:
explicit HLE_ClearConstBuffer(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
static constexpr std::array<u32, base_size> zeroes{};
auto& regs = maxwell3d.regs; auto& regs = maxwell3d.regs;
regs.const_buffer.size = u32(base_size); regs.const_buffer.size = u32(base_size);
regs.const_buffer.address_high = parameters[0]; regs.const_buffer.address_high = parameters[0];
@ -477,15 +378,8 @@ public:
regs.const_buffer.offset = 0; regs.const_buffer.offset = 0;
maxwell3d.ProcessCBMultiData(zeroes.data(), parameters[2] * 4); maxwell3d.ProcessCBMultiData(zeroes.data(), parameters[2] * 4);
} }
}; void HLE_ClearMemory::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
class HLE_ClearMemory final : public HLEMacroImpl {
public:
explicit HLE_ClearMemory(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
const u32 needed_memory = parameters[2] / sizeof(u32); const u32 needed_memory = parameters[2] / sizeof(u32);
if (needed_memory > zero_memory.size()) { if (needed_memory > zero_memory.size()) {
zero_memory.resize(needed_memory, 0); zero_memory.resize(needed_memory, 0);
@ -498,176 +392,93 @@ public:
maxwell3d.CallMethod(size_t(MAXWELL3D_REG_INDEX(launch_dma)), 0x1011, true); maxwell3d.CallMethod(size_t(MAXWELL3D_REG_INDEX(launch_dma)), 0x1011, true);
maxwell3d.CallMultiMethod(size_t(MAXWELL3D_REG_INDEX(inline_data)), zero_memory.data(), needed_memory, needed_memory); maxwell3d.CallMultiMethod(size_t(MAXWELL3D_REG_INDEX(inline_data)), zero_memory.data(), needed_memory, needed_memory);
} }
void HLE_TransformFeedbackSetup::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method) {
private:
std::vector<u32> zero_memory;
};
class HLE_TransformFeedbackSetup final : public HLEMacroImpl {
public:
explicit HLE_TransformFeedbackSetup(Maxwell3D& maxwell3d_) : HLEMacroImpl(maxwell3d_) {}
void Execute(const std::vector<u32>& parameters, [[maybe_unused]] u32 method) override {
maxwell3d.RefreshParameters(); maxwell3d.RefreshParameters();
auto& regs = maxwell3d.regs; auto& regs = maxwell3d.regs;
regs.transform_feedback_enabled = 1; regs.transform_feedback_enabled = 1;
regs.transform_feedback.buffers[0].start_offset = 0; regs.transform_feedback.buffers[0].start_offset = 0;
regs.transform_feedback.buffers[1].start_offset = 0; regs.transform_feedback.buffers[1].start_offset = 0;
regs.transform_feedback.buffers[2].start_offset = 0; regs.transform_feedback.buffers[2].start_offset = 0;
regs.transform_feedback.buffers[3].start_offset = 0; regs.transform_feedback.buffers[3].start_offset = 0;
regs.upload.line_length_in = 4; regs.upload.line_length_in = 4;
regs.upload.line_count = 1; regs.upload.line_count = 1;
regs.upload.dest.address_high = parameters[0]; regs.upload.dest.address_high = parameters[0];
regs.upload.dest.address_low = parameters[1]; regs.upload.dest.address_low = parameters[1];
maxwell3d.CallMethod(size_t(MAXWELL3D_REG_INDEX(launch_dma)), 0x1011, true); maxwell3d.CallMethod(size_t(MAXWELL3D_REG_INDEX(launch_dma)), 0x1011, true);
maxwell3d.CallMethod(size_t(MAXWELL3D_REG_INDEX(inline_data)), regs.transform_feedback.controls[0].stride, true); maxwell3d.CallMethod(size_t(MAXWELL3D_REG_INDEX(inline_data)), regs.transform_feedback.controls[0].stride, true);
maxwell3d.Rasterizer().RegisterTransformFeedback(regs.upload.dest.Address()); maxwell3d.Rasterizer().RegisterTransformFeedback(regs.upload.dest.Address());
} }
};
} // Anonymous namespace #define HLE_MACRO_LIST \
HLE_MACRO_ELEM(0x0D61FC9FAAC9FCADULL, HLE_DrawArraysIndirect, (false)) \
HLE_MACRO_ELEM(0x8A4D173EB99A8603ULL, HLE_DrawArraysIndirect, (true)) \
HLE_MACRO_ELEM(0x771BB18C62444DA0ULL, HLE_DrawIndexedIndirect, (false)) \
HLE_MACRO_ELEM(0x0217920100488FF7ULL, HLE_DrawIndexedIndirect, (true)) \
HLE_MACRO_ELEM(0x3F5E74B9C9A50164ULL, HLE_MultiDrawIndexedIndirectCount, ()) \
HLE_MACRO_ELEM(0xEAD26C3E2109B06BULL, HLE_MultiLayerClear, ()) \
HLE_MACRO_ELEM(0xC713C83D8F63CCF3ULL, HLE_C713C83D8F63CCF3, ()) \
HLE_MACRO_ELEM(0xD7333D26E0A93EDEULL, HLE_D7333D26E0A93EDE, ()) \
HLE_MACRO_ELEM(0xEB29B2A09AA06D38ULL, HLE_BindShader, ()) \
HLE_MACRO_ELEM(0xDB1341DBEB4C8AF7ULL, HLE_SetRasterBoundingBox, ()) \
HLE_MACRO_ELEM(0x6C97861D891EDf7EULL, HLE_ClearConstBuffer, (0x5F00)) \
HLE_MACRO_ELEM(0xD246FDDF3A6173D7ULL, HLE_ClearConstBuffer, (0x7000)) \
HLE_MACRO_ELEM(0xEE4D0004BEC8ECF4ULL, HLE_ClearMemory, ()) \
HLE_MACRO_ELEM(0xFC0CF27F5FFAA661ULL, HLE_TransformFeedbackSetup, ()) \
HLE_MACRO_ELEM(0xB5F74EDB717278ECULL, HLE_DrawIndirectByteCount, ()) \
HLEMacro::HLEMacro(Maxwell3D& maxwell3d_) : maxwell3d{maxwell3d_} {} // Allocates and returns a cached macro if the hash matches a known function.
[[nodiscard]] inline AnyCachedMacro GetHLEProgram(u64 hash) noexcept {
HLEMacro::~HLEMacro() = default;
std::unique_ptr<CachedMacro> HLEMacro::GetHLEProgram(u64 hash) const {
// Compiler will make you a GREAT job at making an ad-hoc hash table :) // Compiler will make you a GREAT job at making an ad-hoc hash table :)
switch (hash) { switch (hash) {
case 0x0D61FC9FAAC9FCADULL: return std::make_unique<HLE_DrawArraysIndirect<false>>(maxwell3d); #define HLE_MACRO_ELEM(HASH, TY, VAL) case HASH: return TY VAL;
case 0x8A4D173EB99A8603ULL: return std::make_unique<HLE_DrawArraysIndirect<true>>(maxwell3d); HLE_MACRO_LIST
case 0x771BB18C62444DA0ULL: return std::make_unique<HLE_DrawIndexedIndirect<false>>(maxwell3d); #undef HLE_MACRO_ELEM
case 0x0217920100488FF7ULL: return std::make_unique<HLE_DrawIndexedIndirect<true>>(maxwell3d); default: return std::monostate{};
case 0x3F5E74B9C9A50164ULL: return std::make_unique<HLE_MultiDrawIndexedIndirectCount>(maxwell3d); }
case 0xEAD26C3E2109B06BULL: return std::make_unique<HLE_MultiLayerClear>(maxwell3d); }
case 0xC713C83D8F63CCF3ULL: return std::make_unique<HLE_C713C83D8F63CCF3>(maxwell3d); [[nodiscard]] inline bool CanBeHLEProgram(u64 hash) noexcept {
case 0xD7333D26E0A93EDEULL: return std::make_unique<HLE_D7333D26E0A93EDE>(maxwell3d); switch (hash) {
case 0xEB29B2A09AA06D38ULL: return std::make_unique<HLE_BindShader>(maxwell3d); #define HLE_MACRO_ELEM(HASH, TY, VAL) case HASH: return true;
case 0xDB1341DBEB4C8AF7ULL: return std::make_unique<HLE_SetRasterBoundingBox>(maxwell3d); HLE_MACRO_LIST
case 0x6C97861D891EDf7EULL: return std::make_unique<HLE_ClearConstBuffer<0x5F00>>(maxwell3d); #undef HLE_MACRO_ELEM
case 0xD246FDDF3A6173D7ULL: return std::make_unique<HLE_ClearConstBuffer<0x7000>>(maxwell3d); default: return false;
case 0xEE4D0004BEC8ECF4ULL: return std::make_unique<HLE_ClearMemory>(maxwell3d);
case 0xFC0CF27F5FFAA661ULL: return std::make_unique<HLE_TransformFeedbackSetup>(maxwell3d);
case 0xB5F74EDB717278ECULL: return std::make_unique<HLE_DrawIndirectByteCount>(maxwell3d);
default:
return nullptr;
} }
} }
namespace { void MacroInterpreterImpl::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> params, u32 method) {
class MacroInterpreterImpl final : public CachedMacro {
public:
explicit MacroInterpreterImpl(Engines::Maxwell3D& maxwell3d_, const std::vector<u32>& code_)
: CachedMacro(maxwell3d_)
, code{code_}
{}
void Execute(const std::vector<u32>& params, u32 method) override;
private:
/// Resets the execution engine state, zeroing registers, etc.
void Reset();
/**
* Executes a single macro instruction located at the current program counter. Returns whether
* the interpreter should keep running.
*
* @param is_delay_slot Whether the current step is being executed due to a delay slot in a
* previous instruction.
*/
bool Step(bool is_delay_slot);
/// Calculates the result of an ALU operation. src_a OP src_b;
u32 GetALUResult(Macro::ALUOperation operation, u32 src_a, u32 src_b);
/// Performs the result operation on the input result and stores it in the specified register
/// (if necessary).
void ProcessResult(Macro::ResultOperation operation, u32 reg, u32 result);
/// Evaluates the branch condition and returns whether the branch should be taken or not.
bool EvaluateBranchCondition(Macro::BranchCondition cond, u32 value) const;
/// Reads an opcode at the current program counter location.
Macro::Opcode GetOpcode() const;
/// Returns the specified register's value. Register 0 is hardcoded to always return 0.
u32 GetRegister(u32 register_id) const;
/// Sets the register to the input value.
void SetRegister(u32 register_id, u32 value);
/// Sets the method address to use for the next Send instruction.
void SetMethodAddress(u32 address);
/// Calls a GPU Engine method with the input parameter.
void Send(u32 value);
/// Reads a GPU register located at the method address.
u32 Read(u32 method) const;
/// Returns the next parameter in the parameter queue.
u32 FetchParameter();
/// Current program counter
u32 pc{};
/// Program counter to execute at after the delay slot is executed.
std::optional<u32> delayed_pc;
/// General purpose macro registers.
std::array<u32, Macro::NUM_MACRO_REGISTERS> registers = {};
/// Method address to use for the next Send instruction.
Macro::MethodAddress method_address = {};
/// Input parameters of the current macro.
std::unique_ptr<u32[]> parameters;
std::size_t num_parameters = 0;
std::size_t parameters_capacity = 0;
/// Index of the next parameter that will be fetched by the 'parm' instruction.
u32 next_parameter_index = 0;
bool carry_flag = false;
const std::vector<u32>& code;
};
void MacroInterpreterImpl::Execute(const std::vector<u32>& params, u32 method) {
Reset(); Reset();
registers[1] = params[0]; registers[1] = params[0];
num_parameters = params.size(); parameters.resize(params.size());
std::memcpy(parameters.data(), params.data(), params.size() * sizeof(u32));
if (num_parameters > parameters_capacity) {
parameters_capacity = num_parameters;
parameters = std::make_unique<u32[]>(num_parameters);
}
std::memcpy(parameters.get(), params.data(), num_parameters * sizeof(u32));
// Execute the code until we hit an exit condition. // Execute the code until we hit an exit condition.
bool keep_executing = true; bool keep_executing = true;
while (keep_executing) { while (keep_executing) {
keep_executing = Step(false); keep_executing = Step(maxwell3d, false);
} }
// Assert the the macro used all the input parameters // Assert the the macro used all the input parameters
ASSERT(next_parameter_index == num_parameters); ASSERT(next_parameter_index == parameters.size());
} }
/// Resets the execution engine state, zeroing registers, etc.
void MacroInterpreterImpl::Reset() { void MacroInterpreterImpl::Reset() {
registers = {}; registers = {};
pc = 0; pc = 0;
delayed_pc = {}; delayed_pc = {};
method_address.raw = 0; method_address.raw = 0;
num_parameters = 0; // Vector must hold its last indices otherwise wonky shit will happen
// The next parameter index starts at 1, because $r1 already has the value of the first // The next parameter index starts at 1, because $r1 already has the value of the first
// parameter. // parameter.
next_parameter_index = 1; next_parameter_index = 1;
carry_flag = false; carry_flag = false;
} }
bool MacroInterpreterImpl::Step(bool is_delay_slot) { /// @brief Executes a single macro instruction located at the current program counter. Returns whether
/// the interpreter should keep running.
/// @param is_delay_slot Whether the current step is being executed due to a delay slot in a previous instruction.
bool MacroInterpreterImpl::Step(Engines::Maxwell3D& maxwell3d, bool is_delay_slot) {
u32 base_address = pc; u32 base_address = pc;
Macro::Opcode opcode = GetOpcode(); Macro::Opcode opcode = GetOpcode();
@ -682,14 +493,12 @@ bool MacroInterpreterImpl::Step(bool is_delay_slot) {
switch (opcode.operation) { switch (opcode.operation) {
case Macro::Operation::ALU: { case Macro::Operation::ALU: {
u32 result = GetALUResult(opcode.alu_operation, GetRegister(opcode.src_a), u32 result = GetALUResult(opcode.alu_operation, GetRegister(opcode.src_a), GetRegister(opcode.src_b));
GetRegister(opcode.src_b)); ProcessResult(maxwell3d, opcode.result_operation, opcode.dst, result);
ProcessResult(opcode.result_operation, opcode.dst, result);
break; break;
} }
case Macro::Operation::AddImmediate: { case Macro::Operation::AddImmediate: {
ProcessResult(opcode.result_operation, opcode.dst, ProcessResult(maxwell3d, opcode.result_operation, opcode.dst, GetRegister(opcode.src_a) + opcode.immediate);
GetRegister(opcode.src_a) + opcode.immediate);
break; break;
} }
case Macro::Operation::ExtractInsert: { case Macro::Operation::ExtractInsert: {
@ -699,7 +508,7 @@ bool MacroInterpreterImpl::Step(bool is_delay_slot) {
src = (src >> opcode.bf_src_bit) & opcode.GetBitfieldMask(); src = (src >> opcode.bf_src_bit) & opcode.GetBitfieldMask();
dst &= ~(opcode.GetBitfieldMask() << opcode.bf_dst_bit); dst &= ~(opcode.GetBitfieldMask() << opcode.bf_dst_bit);
dst |= src << opcode.bf_dst_bit; dst |= src << opcode.bf_dst_bit;
ProcessResult(opcode.result_operation, opcode.dst, dst); ProcessResult(maxwell3d, opcode.result_operation, opcode.dst, dst);
break; break;
} }
case Macro::Operation::ExtractShiftLeftImmediate: { case Macro::Operation::ExtractShiftLeftImmediate: {
@ -708,7 +517,7 @@ bool MacroInterpreterImpl::Step(bool is_delay_slot) {
u32 result = ((src >> dst) & opcode.GetBitfieldMask()) << opcode.bf_dst_bit; u32 result = ((src >> dst) & opcode.GetBitfieldMask()) << opcode.bf_dst_bit;
ProcessResult(opcode.result_operation, opcode.dst, result); ProcessResult(maxwell3d, opcode.result_operation, opcode.dst, result);
break; break;
} }
case Macro::Operation::ExtractShiftLeftRegister: { case Macro::Operation::ExtractShiftLeftRegister: {
@ -717,12 +526,12 @@ bool MacroInterpreterImpl::Step(bool is_delay_slot) {
u32 result = ((src >> opcode.bf_src_bit) & opcode.GetBitfieldMask()) << dst; u32 result = ((src >> opcode.bf_src_bit) & opcode.GetBitfieldMask()) << dst;
ProcessResult(opcode.result_operation, opcode.dst, result); ProcessResult(maxwell3d, opcode.result_operation, opcode.dst, result);
break; break;
} }
case Macro::Operation::Read: { case Macro::Operation::Read: {
u32 result = Read(GetRegister(opcode.src_a) + opcode.immediate); u32 result = Read(maxwell3d, GetRegister(opcode.src_a) + opcode.immediate);
ProcessResult(opcode.result_operation, opcode.dst, result); ProcessResult(maxwell3d, opcode.result_operation, opcode.dst, result);
break; break;
} }
case Macro::Operation::Branch: { case Macro::Operation::Branch: {
@ -738,7 +547,7 @@ bool MacroInterpreterImpl::Step(bool is_delay_slot) {
delayed_pc = base_address + opcode.GetBranchTarget(); delayed_pc = base_address + opcode.GetBranchTarget();
// Execute one more instruction due to the delay slot. // Execute one more instruction due to the delay slot.
return Step(true); return Step(maxwell3d, true);
} }
break; break;
} }
@ -751,13 +560,13 @@ bool MacroInterpreterImpl::Step(bool is_delay_slot) {
// cause an exit if it's executed inside a delay slot. // cause an exit if it's executed inside a delay slot.
if (opcode.is_exit && !is_delay_slot) { if (opcode.is_exit && !is_delay_slot) {
// Exit has a delay slot, execute the next instruction // Exit has a delay slot, execute the next instruction
Step(true); Step(maxwell3d, true);
return false; return false;
} }
return true; return true;
} }
/// Calculates the result of an ALU operation. src_a OP src_b;
u32 MacroInterpreterImpl::GetALUResult(Macro::ALUOperation operation, u32 src_a, u32 src_b) { u32 MacroInterpreterImpl::GetALUResult(Macro::ALUOperation operation, u32 src_a, u32 src_b) {
switch (operation) { switch (operation) {
case Macro::ALUOperation::Add: { case Macro::ALUOperation::Add: {
@ -797,7 +606,8 @@ u32 MacroInterpreterImpl::GetALUResult(Macro::ALUOperation operation, u32 src_a,
} }
} }
void MacroInterpreterImpl::ProcessResult(Macro::ResultOperation operation, u32 reg, u32 result) { /// Performs the result operation on the input result and stores it in the specified register (if necessary).
void MacroInterpreterImpl::ProcessResult(Engines::Maxwell3D& maxwell3d, Macro::ResultOperation operation, u32 reg, u32 result) {
switch (operation) { switch (operation) {
case Macro::ResultOperation::IgnoreAndFetch: case Macro::ResultOperation::IgnoreAndFetch:
// Fetch parameter and ignore result. // Fetch parameter and ignore result.
@ -815,12 +625,12 @@ void MacroInterpreterImpl::ProcessResult(Macro::ResultOperation operation, u32 r
case Macro::ResultOperation::FetchAndSend: case Macro::ResultOperation::FetchAndSend:
// Fetch parameter and send result. // Fetch parameter and send result.
SetRegister(reg, FetchParameter()); SetRegister(reg, FetchParameter());
Send(result); Send(maxwell3d, result);
break; break;
case Macro::ResultOperation::MoveAndSend: case Macro::ResultOperation::MoveAndSend:
// Move and send result. // Move and send result.
SetRegister(reg, result); SetRegister(reg, result);
Send(result); Send(maxwell3d, result);
break; break;
case Macro::ResultOperation::FetchAndSetMethod: case Macro::ResultOperation::FetchAndSetMethod:
// Fetch parameter and use result as Method Address. // Fetch parameter and use result as Method Address.
@ -831,13 +641,13 @@ void MacroInterpreterImpl::ProcessResult(Macro::ResultOperation operation, u32 r
// Move result and use as Method Address, then fetch and send parameter. // Move result and use as Method Address, then fetch and send parameter.
SetRegister(reg, result); SetRegister(reg, result);
SetMethodAddress(result); SetMethodAddress(result);
Send(FetchParameter()); Send(maxwell3d, FetchParameter());
break; break;
case Macro::ResultOperation::MoveAndSetMethodSend: case Macro::ResultOperation::MoveAndSetMethodSend:
// Move result and use as Method Address, then send bits 12:17 of result. // Move result and use as Method Address, then send bits 12:17 of result.
SetRegister(reg, result); SetRegister(reg, result);
SetMethodAddress(result); SetMethodAddress(result);
Send((result >> 12) & 0b111111); Send(maxwell3d, (result >> 12) & 0b111111);
break; break;
default: default:
UNIMPLEMENTED_MSG("Unimplemented result operation {}", operation); UNIMPLEMENTED_MSG("Unimplemented result operation {}", operation);
@ -845,6 +655,7 @@ void MacroInterpreterImpl::ProcessResult(Macro::ResultOperation operation, u32 r
} }
} }
/// Evaluates the branch condition and returns whether the branch should be taken or not.
bool MacroInterpreterImpl::EvaluateBranchCondition(Macro::BranchCondition cond, u32 value) const { bool MacroInterpreterImpl::EvaluateBranchCondition(Macro::BranchCondition cond, u32 value) const {
switch (cond) { switch (cond) {
case Macro::BranchCondition::Zero: case Macro::BranchCondition::Zero:
@ -855,46 +666,44 @@ bool MacroInterpreterImpl::EvaluateBranchCondition(Macro::BranchCondition cond,
UNREACHABLE(); UNREACHABLE();
} }
/// Reads an opcode at the current program counter location.
Macro::Opcode MacroInterpreterImpl::GetOpcode() const { Macro::Opcode MacroInterpreterImpl::GetOpcode() const {
ASSERT((pc % sizeof(u32)) == 0); ASSERT((pc % sizeof(u32)) == 0);
ASSERT(pc < code.size() * sizeof(u32)); ASSERT(pc < code.size() * sizeof(u32));
return {code[pc / sizeof(u32)]}; return {code[pc / sizeof(u32)]};
} }
/// Returns the specified register's value. Register 0 is hardcoded to always return 0.
u32 MacroInterpreterImpl::GetRegister(u32 register_id) const { u32 MacroInterpreterImpl::GetRegister(u32 register_id) const {
return registers.at(register_id); return registers[register_id];
} }
/// Sets the register to the input value.
void MacroInterpreterImpl::SetRegister(u32 register_id, u32 value) { void MacroInterpreterImpl::SetRegister(u32 register_id, u32 value) {
// Register 0 is hardwired as the zero register. // Register 0 is hardwired as the zero register.
// Ensure no writes to it actually occur. // Ensure no writes to it actually occur.
if (register_id == 0) { if (register_id == 0)
return; return;
registers[register_id] = value;
} }
registers.at(register_id) = value; /// Calls a GPU Engine method with the input parameter.
} void MacroInterpreterImpl::Send(Engines::Maxwell3D& maxwell3d, u32 value) {
void MacroInterpreterImpl::SetMethodAddress(u32 address) {
method_address.raw = address;
}
void MacroInterpreterImpl::Send(u32 value) {
maxwell3d.CallMethod(method_address.address, value, true); maxwell3d.CallMethod(method_address.address, value, true);
// Increment the method address by the method increment. // Increment the method address by the method increment.
method_address.address.Assign(method_address.address.Value() + method_address.address.Assign(method_address.address.Value() + method_address.increment.Value());
method_address.increment.Value());
} }
u32 MacroInterpreterImpl::Read(u32 method) const { /// Reads a GPU register located at the method address.
u32 MacroInterpreterImpl::Read(Engines::Maxwell3D& maxwell3d, u32 method) const {
return maxwell3d.GetRegisterValue(method); return maxwell3d.GetRegisterValue(method);
} }
/// Returns the next parameter in the parameter queue.
u32 MacroInterpreterImpl::FetchParameter() { u32 MacroInterpreterImpl::FetchParameter() {
ASSERT(next_parameter_index < num_parameters); ASSERT(next_parameter_index < parameters.size());
return parameters[next_parameter_index++]; return parameters[next_parameter_index++];
} }
} // Anonymous namespace
#ifdef ARCHITECTURE_x86_64 #ifdef ARCHITECTURE_x86_64
namespace { namespace {
@ -930,17 +739,15 @@ static const auto default_cg_mode = Xbyak::DontSetProtectRWE;
static const auto default_cg_mode = nullptr; //Allow RWE static const auto default_cg_mode = nullptr; //Allow RWE
#endif #endif
class MacroJITx64Impl final : public Xbyak::CodeGenerator, public CachedMacro { struct MacroJITx64Impl final : public Xbyak::CodeGenerator, public DynamicCachedMacro {
public: explicit MacroJITx64Impl(std::span<const u32> code_)
explicit MacroJITx64Impl(Engines::Maxwell3D& maxwell3d_, const std::vector<u32>& code_)
: Xbyak::CodeGenerator(MAX_CODE_SIZE, default_cg_mode) : Xbyak::CodeGenerator(MAX_CODE_SIZE, default_cg_mode)
, CachedMacro(maxwell3d_)
, code{code_} , code{code_}
{ {
Compile(); Compile();
} }
void Execute(const std::vector<u32>& parameters, u32 method) override; void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, u32 method) override;
void Compile_ALU(Macro::Opcode opcode); void Compile_ALU(Macro::Opcode opcode);
void Compile_AddImmediate(Macro::Opcode opcode); void Compile_AddImmediate(Macro::Opcode opcode);
@ -950,18 +757,13 @@ public:
void Compile_Read(Macro::Opcode opcode); void Compile_Read(Macro::Opcode opcode);
void Compile_Branch(Macro::Opcode opcode); void Compile_Branch(Macro::Opcode opcode);
private:
void Optimizer_ScanFlags(); void Optimizer_ScanFlags();
void Compile(); void Compile();
bool Compile_NextInstruction(); bool Compile_NextInstruction();
Xbyak::Reg32 Compile_FetchParameter(); Xbyak::Reg32 Compile_FetchParameter();
Xbyak::Reg32 Compile_GetRegister(u32 index, Xbyak::Reg32 dst); Xbyak::Reg32 Compile_GetRegister(u32 index, Xbyak::Reg32 dst);
void Compile_ProcessResult(Macro::ResultOperation operation, u32 reg); void Compile_ProcessResult(Macro::ResultOperation operation, u32 reg);
void Compile_Send(Xbyak::Reg32 value); void Compile_Send(Xbyak::Reg32 value);
Macro::Opcode GetOpCode() const; Macro::Opcode GetOpCode() const;
struct JITState { struct JITState {
@ -981,21 +783,17 @@ private:
bool enable_asserts{}; bool enable_asserts{};
}; };
OptimizerState optimizer{}; OptimizerState optimizer{};
std::optional<Macro::Opcode> next_opcode{}; std::optional<Macro::Opcode> next_opcode{};
ProgramType program{nullptr}; ProgramType program{nullptr};
std::array<Xbyak::Label, MAX_CODE_SIZE> labels; std::array<Xbyak::Label, MAX_CODE_SIZE> labels;
std::array<Xbyak::Label, MAX_CODE_SIZE> delay_skip; std::array<Xbyak::Label, MAX_CODE_SIZE> delay_skip;
Xbyak::Label end_of_code{}; Xbyak::Label end_of_code{};
bool is_delay_slot{}; bool is_delay_slot{};
u32 pc{}; u32 pc{};
std::span<const u32> code;
const std::vector<u32>& code;
}; };
void MacroJITx64Impl::Execute(const std::vector<u32>& parameters, u32 method) { void MacroJITx64Impl::Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, u32 method) {
ASSERT_OR_EXECUTE(program != nullptr, { return; }); ASSERT_OR_EXECUTE(program != nullptr, { return; });
JITState state{}; JITState state{};
state.maxwell3d = &maxwell3d; state.maxwell3d = &maxwell3d;
@ -1231,7 +1029,7 @@ void MacroJITx64Impl::Compile_Read(Macro::Opcode opcode) {
Compile_ProcessResult(opcode.result_operation, opcode.dst); Compile_ProcessResult(opcode.result_operation, opcode.dst);
} }
void Send(Engines::Maxwell3D* maxwell3d, Macro::MethodAddress method_address, u32 value) { static void MacroJIT_SendThunk(Engines::Maxwell3D* maxwell3d, Macro::MethodAddress method_address, u32 value) {
maxwell3d->CallMethod(method_address.address, value, true); maxwell3d->CallMethod(method_address.address, value, true);
} }
@ -1240,7 +1038,7 @@ void MacroJITx64Impl::Compile_Send(Xbyak::Reg32 value) {
mov(Common::X64::ABI_PARAM1, qword[STATE]); mov(Common::X64::ABI_PARAM1, qword[STATE]);
mov(Common::X64::ABI_PARAM2.cvt32(), METHOD_ADDRESS); mov(Common::X64::ABI_PARAM2.cvt32(), METHOD_ADDRESS);
mov(Common::X64::ABI_PARAM3.cvt32(), value); mov(Common::X64::ABI_PARAM3.cvt32(), value);
Common::X64::CallFarFunction(*this, &Send); Common::X64::CallFarFunction(*this, &MacroJIT_SendThunk);
Common::X64::ABI_PopRegistersAndAdjustStack(*this, PersistentCallerSavedRegs(), 0); Common::X64::ABI_PopRegistersAndAdjustStack(*this, PersistentCallerSavedRegs(), 0);
Xbyak::Label dont_process{}; Xbyak::Label dont_process{};
@ -1452,10 +1250,8 @@ bool MacroJITx64Impl::Compile_NextInstruction() {
return true; return true;
} }
static void WarnInvalidParameter(uintptr_t parameter, uintptr_t max_parameter) { static void MacroJIT_ErrorThunk(uintptr_t parameter, uintptr_t max_parameter) {
LOG_CRITICAL(HW_GPU, LOG_CRITICAL(HW_GPU, "Macro JIT: invalid parameter access 0x{:x} (0x{:x} is the last parameter)", parameter, max_parameter - sizeof(u32));
"Macro JIT: invalid parameter access 0x{:x} (0x{:x} is the last parameter)",
parameter, max_parameter - sizeof(u32));
} }
Xbyak::Reg32 MacroJITx64Impl::Compile_FetchParameter() { Xbyak::Reg32 MacroJITx64Impl::Compile_FetchParameter() {
@ -1465,7 +1261,7 @@ Xbyak::Reg32 MacroJITx64Impl::Compile_FetchParameter() {
Common::X64::ABI_PushRegistersAndAdjustStack(*this, PersistentCallerSavedRegs(), 0); Common::X64::ABI_PushRegistersAndAdjustStack(*this, PersistentCallerSavedRegs(), 0);
mov(Common::X64::ABI_PARAM1, PARAMETERS); mov(Common::X64::ABI_PARAM1, PARAMETERS);
mov(Common::X64::ABI_PARAM2, MAX_PARAMETER); mov(Common::X64::ABI_PARAM2, MAX_PARAMETER);
Common::X64::CallFarFunction(*this, &WarnInvalidParameter); Common::X64::CallFarFunction(*this, &MacroJIT_ErrorThunk);
Common::X64::ABI_PopRegistersAndAdjustStack(*this, PersistentCallerSavedRegs(), 0); Common::X64::ABI_PopRegistersAndAdjustStack(*this, PersistentCallerSavedRegs(), 0);
L(parameter_ok); L(parameter_ok);
mov(eax, dword[PARAMETERS]); mov(eax, dword[PARAMETERS]);
@ -1574,33 +1370,42 @@ static void Dump(u64 hash, std::span<const u32> code, bool decompiled = false) {
macro_file.write(reinterpret_cast<const char*>(code.data()), code.size_bytes()); macro_file.write(reinterpret_cast<const char*>(code.data()), code.size_bytes());
} }
MacroEngine::MacroEngine(Engines::Maxwell3D& maxwell3d_, bool is_interpreted_) void MacroEngine::Execute(Engines::Maxwell3D& maxwell3d, u32 method, std::span<const u32> parameters) {
: hle_macros{std::make_optional<Tegra::HLEMacro>(maxwell3d_)} auto const execute_variant = [&maxwell3d, &parameters, method](AnyCachedMacro& acm) {
, maxwell3d{maxwell3d_} if (auto a = std::get_if<HLE_DrawArraysIndirect>(&acm))
, is_interpreted{is_interpreted_} a->Execute(maxwell3d, parameters, method);
{} if (auto a = std::get_if<HLE_DrawIndexedIndirect>(&acm))
a->Execute(maxwell3d, parameters, method);
MacroEngine::~MacroEngine() = default; if (auto a = std::get_if<HLE_MultiDrawIndexedIndirectCount>(&acm))
a->Execute(maxwell3d, parameters, method);
void MacroEngine::AddCode(u32 method, u32 data) { if (auto a = std::get_if<HLE_MultiLayerClear>(&acm))
uploaded_macro_code[method].push_back(data); a->Execute(maxwell3d, parameters, method);
} if (auto a = std::get_if<HLE_C713C83D8F63CCF3>(&acm))
a->Execute(maxwell3d, parameters, method);
void MacroEngine::ClearCode(u32 method) { if (auto a = std::get_if<HLE_D7333D26E0A93EDE>(&acm))
macro_cache.erase(method); a->Execute(maxwell3d, parameters, method);
uploaded_macro_code.erase(method); if (auto a = std::get_if<HLE_BindShader>(&acm))
} a->Execute(maxwell3d, parameters, method);
if (auto a = std::get_if<HLE_SetRasterBoundingBox>(&acm))
void MacroEngine::Execute(u32 method, const std::vector<u32>& parameters) { a->Execute(maxwell3d, parameters, method);
auto compiled_macro = macro_cache.find(method); if (auto a = std::get_if<HLE_ClearConstBuffer>(&acm))
if (compiled_macro != macro_cache.end()) { a->Execute(maxwell3d, parameters, method);
const auto& cache_info = compiled_macro->second; if (auto a = std::get_if<HLE_ClearMemory>(&acm))
if (cache_info.has_hle_program) { a->Execute(maxwell3d, parameters, method);
cache_info.hle_program->Execute(parameters, method); if (auto a = std::get_if<HLE_TransformFeedbackSetup>(&acm))
} else { a->Execute(maxwell3d, parameters, method);
maxwell3d.RefreshParameters(); if (auto a = std::get_if<HLE_DrawIndirectByteCount>(&acm))
cache_info.lle_program->Execute(parameters, method); a->Execute(maxwell3d, parameters, method);
} if (auto a = std::get_if<MacroInterpreterImpl>(&acm))
a->Execute(maxwell3d, parameters, method);
if (auto a = std::get_if<std::unique_ptr<DynamicCachedMacro>>(&acm))
a->get()->Execute(maxwell3d, parameters, method);
};
if (auto const it = macro_cache.find(method); it != macro_cache.end()) {
auto& ci = it->second;
if (!CanBeHLEProgram(ci.hash) || Settings::values.disable_macro_hle)
maxwell3d.RefreshParameters(); //LLE must reload parameters
execute_variant(ci.program);
} else { } else {
// Macro not compiled, check if it's uploaded and if so, compile it // Macro not compiled, check if it's uploaded and if so, compile it
std::optional<u32> mid_method; std::optional<u32> mid_method;
@ -1617,51 +1422,37 @@ void MacroEngine::Execute(u32 method, const std::vector<u32>& parameters) {
return; return;
} }
} }
auto& cache_info = macro_cache[method]; auto& ci = macro_cache[method];
if (mid_method) {
if (!mid_method.has_value()) {
cache_info.lle_program = Compile(macro_code->second);
cache_info.hash = Common::HashValue(macro_code->second);
} else {
const auto& macro_cached = uploaded_macro_code[mid_method.value()]; const auto& macro_cached = uploaded_macro_code[mid_method.value()];
const auto rebased_method = method - mid_method.value(); const auto rebased_method = method - mid_method.value();
auto& code = uploaded_macro_code[method]; auto& code = uploaded_macro_code[method];
code.resize(macro_cached.size() - rebased_method); code.resize(macro_cached.size() - rebased_method);
std::memcpy(code.data(), macro_cached.data() + rebased_method, code.size() * sizeof(u32)); std::memcpy(code.data(), macro_cached.data() + rebased_method, code.size() * sizeof(u32));
cache_info.hash = Common::HashValue(code); ci.hash = Common::HashValue(code);
cache_info.lle_program = Compile(code); ci.program = Compile(maxwell3d, code);
}
auto hle_program = hle_macros->GetHLEProgram(cache_info.hash);
if (!hle_program || Settings::values.disable_macro_hle) {
maxwell3d.RefreshParameters();
cache_info.lle_program->Execute(parameters, method);
} else { } else {
cache_info.has_hle_program = true; ci.program = Compile(maxwell3d, macro_code->second);
cache_info.hle_program = std::move(hle_program); ci.hash = Common::HashValue(macro_code->second);
cache_info.hle_program->Execute(parameters, method);
} }
if (CanBeHLEProgram(ci.hash) && !Settings::values.disable_macro_hle) {
ci.program = GetHLEProgram(ci.hash);
} else {
maxwell3d.RefreshParameters();
}
execute_variant(ci.program);
if (Settings::values.dump_macros) { if (Settings::values.dump_macros) {
Dump(cache_info.hash, macro_code->second, cache_info.has_hle_program); Dump(ci.hash, macro_code->second, !std::holds_alternative<std::monostate>(ci.program));
} }
} }
} }
std::unique_ptr<CachedMacro> MacroEngine::Compile(const std::vector<u32>& code) { AnyCachedMacro MacroEngine::Compile(Engines::Maxwell3D& maxwell3d, std::span<const u32> code) {
#ifdef ARCHITECTURE_x86_64 #ifdef ARCHITECTURE_x86_64
if (!is_interpreted) if (!is_interpreted)
return std::make_unique<MacroJITx64Impl>(maxwell3d, code); return std::make_unique<MacroJITx64Impl>(code);
#endif
return std::make_unique<MacroInterpreterImpl>(maxwell3d, code);
}
std::optional<MacroEngine> GetMacroEngine(Engines::Maxwell3D& maxwell3d) {
#ifdef ARCHITECTURE_x86_64
return std::make_optional<MacroEngine>(maxwell3d, bool(Settings::values.disable_macro_jit));
#else
return std::make_optional<MacroEngine>(maxwell3d, true);
#endif #endif
return MacroInterpreterImpl(code);
} }
} // namespace Tegra } // namespace Tegra

View file

@ -7,8 +7,10 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <ankerl/unordered_dense.h> #include <span>
#include <variant>
#include <vector> #include <vector>
#include <ankerl/unordered_dense.h>
#include "common/bit_field.h" #include "common/bit_field.h"
#include "common/common_types.h" #include "common/common_types.h"
@ -98,62 +100,142 @@ union MethodAddress {
} // namespace Macro } // namespace Macro
class CachedMacro { struct HLEMacro {
public: };
CachedMacro(Engines::Maxwell3D& maxwell3d_) /// @note: these macros have two versions, a normal and extended version, with the extended version
: maxwell3d{maxwell3d_} /// also assigning the base vertex/instance.
{} struct HLE_DrawArraysIndirect final {
virtual ~CachedMacro() = default; HLE_DrawArraysIndirect(bool extended_) noexcept : extended{extended_} {}
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
void Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters);
bool extended;
};
/// @note: these macros have two versions, a normal and extended version, with the extended version
/// also assigning the base vertex/instance.
struct HLE_DrawIndexedIndirect final {
explicit HLE_DrawIndexedIndirect(bool extended_) noexcept : extended{extended_} {}
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
void Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters);
bool extended;
};
struct HLE_MultiLayerClear final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
};
struct HLE_MultiDrawIndexedIndirectCount final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
void Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters);
};
struct HLE_DrawIndirectByteCount final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
void Fallback(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters);
};
struct HLE_C713C83D8F63CCF3 final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
};
struct HLE_D7333D26E0A93EDE final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
};
struct HLE_BindShader final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
};
struct HLE_SetRasterBoundingBox final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
};
struct HLE_ClearConstBuffer final {
HLE_ClearConstBuffer(size_t base_size_) noexcept : base_size{base_size_} {}
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
size_t base_size;
};
struct HLE_ClearMemory final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
std::vector<u32> zero_memory;
};
struct HLE_TransformFeedbackSetup final {
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, [[maybe_unused]] u32 method);
};
struct MacroInterpreterImpl final {
MacroInterpreterImpl() {}
MacroInterpreterImpl(std::span<const u32> code_) : code{code_} {}
void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> params, u32 method);
void Reset();
bool Step(Engines::Maxwell3D& maxwell3d, bool is_delay_slot);
u32 GetALUResult(Macro::ALUOperation operation, u32 src_a, u32 src_b);
void ProcessResult(Engines::Maxwell3D& maxwell3d, Macro::ResultOperation operation, u32 reg, u32 result);
bool EvaluateBranchCondition(Macro::BranchCondition cond, u32 value) const;
Macro::Opcode GetOpcode() const;
u32 GetRegister(u32 register_id) const;
void SetRegister(u32 register_id, u32 value);
/// Sets the method address to use for the next Send instruction.
[[nodiscard]] inline void SetMethodAddress(u32 address) noexcept {
method_address.raw = address;
}
void Send(Engines::Maxwell3D& maxwell3d, u32 value);
u32 Read(Engines::Maxwell3D& maxwell3d, u32 method) const;
u32 FetchParameter();
/// General purpose macro registers.
std::array<u32, Macro::NUM_MACRO_REGISTERS> registers = {};
/// Input parameters of the current macro.
std::vector<u32> parameters;
std::span<const u32> code;
/// Program counter to execute at after the delay slot is executed.
std::optional<u32> delayed_pc;
/// Method address to use for the next Send instruction.
Macro::MethodAddress method_address = {};
/// Current program counter
u32 pc{};
/// Index of the next parameter that will be fetched by the 'parm' instruction.
u32 next_parameter_index = 0;
bool carry_flag = false;
};
struct DynamicCachedMacro {
virtual ~DynamicCachedMacro() = default;
/// Executes the macro code with the specified input parameters. /// Executes the macro code with the specified input parameters.
/// @param parameters The parameters of the macro /// @param parameters The parameters of the macro
/// @param method The method to execute /// @param method The method to execute
virtual void Execute(const std::vector<u32>& parameters, u32 method) = 0; virtual void Execute(Engines::Maxwell3D& maxwell3d, std::span<const u32> parameters, u32 method) = 0;
Engines::Maxwell3D& maxwell3d;
}; };
class HLEMacro { using AnyCachedMacro = std::variant<
public: std::monostate,
explicit HLEMacro(Engines::Maxwell3D& maxwell3d_); HLEMacro,
~HLEMacro(); HLE_DrawArraysIndirect,
// Allocates and returns a cached macro if the hash matches a known function. HLE_DrawIndexedIndirect,
// Returns nullptr otherwise. HLE_MultiDrawIndexedIndirectCount,
[[nodiscard]] std::unique_ptr<CachedMacro> GetHLEProgram(u64 hash) const; HLE_MultiLayerClear,
private: HLE_C713C83D8F63CCF3,
Engines::Maxwell3D& maxwell3d; HLE_D7333D26E0A93EDE,
}; HLE_BindShader,
HLE_SetRasterBoundingBox,
class MacroEngine { HLE_ClearConstBuffer,
public: HLE_ClearMemory,
explicit MacroEngine(Engines::Maxwell3D& maxwell3d, bool is_interpreted); HLE_TransformFeedbackSetup,
~MacroEngine(); HLE_DrawIndirectByteCount,
MacroInterpreterImpl,
// Used for JIT x86 macro
std::unique_ptr<DynamicCachedMacro>
>;
struct MacroEngine {
MacroEngine(bool is_interpreted_) noexcept : is_interpreted{is_interpreted_} {}
// Store the uploaded macro code to compile them when they're called. // Store the uploaded macro code to compile them when they're called.
void AddCode(u32 method, u32 data); inline void AddCode(u32 method, u32 data) noexcept {
uploaded_macro_code[method].push_back(data);
}
// Clear the code associated with a method. // Clear the code associated with a method.
void ClearCode(u32 method); inline void ClearCode(u32 method) noexcept {
macro_cache.erase(method);
uploaded_macro_code.erase(method);
}
// Compiles the macro if its not in the cache, and executes the compiled macro // Compiles the macro if its not in the cache, and executes the compiled macro
void Execute(u32 method, const std::vector<u32>& parameters); void Execute(Engines::Maxwell3D& maxwell3d, u32 method, std::span<const u32> parameters);
AnyCachedMacro Compile(Engines::Maxwell3D& maxwell3d, std::span<const u32> code);
protected:
std::unique_ptr<CachedMacro> Compile(const std::vector<u32>& code);
private:
struct CacheInfo { struct CacheInfo {
std::unique_ptr<CachedMacro> lle_program{}; AnyCachedMacro program;
std::unique_ptr<CachedMacro> hle_program{};
u64 hash{}; u64 hash{};
bool has_hle_program{};
}; };
ankerl::unordered_dense::map<u32, CacheInfo> macro_cache; ankerl::unordered_dense::map<u32, CacheInfo> macro_cache;
ankerl::unordered_dense::map<u32, std::vector<u32>> uploaded_macro_code; ankerl::unordered_dense::map<u32, std::vector<u32>> uploaded_macro_code;
std::optional<HLEMacro> hle_macros;
Engines::Maxwell3D& maxwell3d;
bool is_interpreted; bool is_interpreted;
}; };
std::optional<MacroEngine> GetMacroEngine(Engines::Maxwell3D& maxwell3d);
} // namespace Tegra } // namespace Tegra

View file

@ -1032,7 +1032,7 @@ void BlitImageHelper::ConvertDepthToColorPipeline(vk::Pipeline& pipeline, VkRend
VkShaderModule frag_shader = *convert_float_to_depth_frag; VkShaderModule frag_shader = *convert_float_to_depth_frag;
const std::array stages = MakeStages(*full_screen_vert, frag_shader); const std::array stages = MakeStages(*full_screen_vert, frag_shader);
const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device); const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device);
pipeline = device.GetLogical().CreateGraphicsPipeline({ pipeline = device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = nullptr, .pNext = nullptr,
.flags = 0, .flags = 0,
@ -1062,7 +1062,7 @@ void BlitImageHelper::ConvertColorToDepthPipeline(vk::Pipeline& pipeline, VkRend
VkShaderModule frag_shader = *convert_depth_to_float_frag; VkShaderModule frag_shader = *convert_depth_to_float_frag;
const std::array stages = MakeStages(*full_screen_vert, frag_shader); const std::array stages = MakeStages(*full_screen_vert, frag_shader);
const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device); const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device);
pipeline = device.GetLogical().CreateGraphicsPipeline({ pipeline = device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = nullptr, .pNext = nullptr,
.flags = 0, .flags = 0,
@ -1093,7 +1093,7 @@ void BlitImageHelper::ConvertPipelineEx(vk::Pipeline& pipeline, VkRenderPass ren
} }
const std::array stages = MakeStages(*full_screen_vert, *module); const std::array stages = MakeStages(*full_screen_vert, *module);
const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device); const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device);
pipeline = device.GetLogical().CreateGraphicsPipeline({ pipeline = device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = nullptr, .pNext = nullptr,
.flags = 0, .flags = 0,
@ -1135,7 +1135,7 @@ void BlitImageHelper::ConvertPipeline(vk::Pipeline& pipeline, VkRenderPass rende
is_target_depth ? *convert_float_to_depth_frag : *convert_depth_to_float_frag; is_target_depth ? *convert_float_to_depth_frag : *convert_depth_to_float_frag;
const std::array stages = MakeStages(*full_screen_vert, frag_shader); const std::array stages = MakeStages(*full_screen_vert, frag_shader);
const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device); const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device);
pipeline = device.GetLogical().CreateGraphicsPipeline({ pipeline = device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
.pNext = nullptr, .pNext = nullptr,
.flags = 0, .flags = 0,

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-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -61,7 +64,8 @@ public:
.pDescriptorUpdateEntries = entries.data(), .pDescriptorUpdateEntries = entries.data(),
.templateType = type, .templateType = type,
.descriptorSetLayout = descriptor_set_layout, .descriptorSetLayout = descriptor_set_layout,
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, .pipelineBindPoint =
is_compute ? VK_PIPELINE_BIND_POINT_COMPUTE : VK_PIPELINE_BIND_POINT_GRAPHICS,
.pipelineLayout = pipeline_layout, .pipelineLayout = pipeline_layout,
.set = 0, .set = 0,
}); });
@ -122,7 +126,7 @@ private:
}); });
++binding; ++binding;
num_descriptors += descriptors[i].count; num_descriptors += descriptors[i].count;
offset += sizeof(DescriptorUpdateEntry); offset += sizeof(DescriptorUpdateEntry) * descriptors[i].count;
} }
} }

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
@ -6,6 +6,7 @@
#include "common/assert.h" #include "common/assert.h"
#include <ranges> #include <ranges>
#include <vulkan/vulkan_core.h>
#include "video_core/renderer_vulkan/present/util.h" #include "video_core/renderer_vulkan/present/util.h"
namespace Vulkan { namespace Vulkan {
@ -629,8 +630,8 @@ vk::Sampler CreateCubicSampler(const Device& device, VkCubicFilterWeightsQCOM qc
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
.pNext = nullptr, .pNext = nullptr,
.flags = 0, .flags = 0,
.magFilter = VK_FILTER_CUBIC_EXT, .magFilter = device.IsExtFilterCubicSupported() ? VK_FILTER_CUBIC_EXT : VK_FILTER_LINEAR,
.minFilter = VK_FILTER_CUBIC_EXT, .minFilter = device.IsExtFilterCubicSupported() ? VK_FILTER_CUBIC_EXT : VK_FILTER_LINEAR,
.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER,

View file

@ -137,14 +137,8 @@ try
memory_allocator, memory_allocator,
scheduler, scheduler,
swapchain, swapchain,
#ifdef ANDROID
surface) surface)
, , blit_swapchain(device_memory,
#else
*surface)
,
#endif
blit_swapchain(device_memory,
device, device,
memory_allocator, memory_allocator,
present_manager, present_manager,

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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2024 Torzu Emulator Project // SPDX-FileCopyrightText: Copyright 2024 Torzu Emulator Project
@ -22,7 +22,8 @@ BlitScreen::BlitScreen(Tegra::MaxwellDeviceMemoryManager& device_memory_, const
MemoryAllocator& memory_allocator_, PresentManager& present_manager_, MemoryAllocator& memory_allocator_, PresentManager& present_manager_,
Scheduler& scheduler_, const PresentFilters& filters_) Scheduler& scheduler_, const PresentFilters& filters_)
: device_memory{device_memory_}, device{device_}, memory_allocator{memory_allocator_}, : device_memory{device_memory_}, device{device_}, memory_allocator{memory_allocator_},
present_manager{present_manager_}, scheduler{scheduler_}, filters{filters_}, image_count{1}, present_manager{present_manager_}, scheduler{scheduler_}, filters{filters_},
image_count{1}, image_index{0},
swapchain_view_format{VK_FORMAT_B8G8R8A8_UNORM} {} swapchain_view_format{VK_FORMAT_B8G8R8A8_UNORM} {}
BlitScreen::~BlitScreen() = default; BlitScreen::~BlitScreen() = default;
@ -87,57 +88,49 @@ void BlitScreen::DrawToFrame(RasterizerVulkan& rasterizer, Frame* frame,
bool resource_update_required = false; bool resource_update_required = false;
bool presentation_recreate_required = false; bool presentation_recreate_required = false;
// Recreate dynamic resources if the adapting filter changed
if (!window_adapt || scaling_filter != filters.get_scaling_filter()) { if (!window_adapt || scaling_filter != filters.get_scaling_filter()) {
resource_update_required = true; resource_update_required = true;
} }
// Recreate dynamic resources if the image count changed if (image_count != current_swapchain_image_count) {
const size_t old_swapchain_image_count =
std::exchange(image_count, current_swapchain_image_count);
if (old_swapchain_image_count != current_swapchain_image_count) {
resource_update_required = true; resource_update_required = true;
image_count = current_swapchain_image_count;
} }
// Recreate the presentation frame if the format or dimensions of the window changed if (swapchain_view_format != current_swapchain_view_format ||
const VkFormat old_swapchain_view_format =
std::exchange(swapchain_view_format, current_swapchain_view_format);
if (old_swapchain_view_format != current_swapchain_view_format ||
layout.width != frame->width || layout.height != frame->height) { layout.width != frame->width || layout.height != frame->height) {
resource_update_required = true; resource_update_required = true;
presentation_recreate_required = true; presentation_recreate_required = true;
swapchain_view_format = current_swapchain_view_format;
} }
// If we have a pending resource update, perform it
if (resource_update_required) { if (resource_update_required) {
// Wait for idle to ensure no resources are in use
WaitIdle(); WaitIdle();
// Update window adapt pass
SetWindowAdaptPass(); SetWindowAdaptPass();
// Update frame format if needed
if (presentation_recreate_required) { if (presentation_recreate_required) {
present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format, present_manager.RecreateFrame(frame, layout.width, layout.height, swapchain_view_format,
window_adapt->GetRenderPass()); window_adapt->GetRenderPass());
} }
image_index = 0;
} }
// Add additional layers if needed
const VkExtent2D window_size{ const VkExtent2D window_size{
.width = layout.screen.GetWidth(), .width = layout.screen.GetWidth(),
.height = layout.screen.GetHeight(), .height = layout.screen.GetHeight(),
}; };
while (layers.size() < framebuffers.size()) { if (layers.size() != framebuffers.size()) {
layers.clear();
for (size_t i = 0; i < framebuffers.size(); ++i) {
layers.emplace_back(device, memory_allocator, scheduler, device_memory, image_count, layers.emplace_back(device, memory_allocator, scheduler, device_memory, image_count,
window_size, window_adapt->GetDescriptorSetLayout(), filters); window_size, window_adapt->GetDescriptorSetLayout(), filters);
} }
}
// Perform the draw
window_adapt->Draw(rasterizer, scheduler, image_index, layers, framebuffers, layout, frame); window_adapt->Draw(rasterizer, scheduler, image_index, layers, framebuffers, layout, frame);
// Advance to next image
if (++image_index >= image_count) { if (++image_index >= image_count) {
image_index = 0; image_index = 0;
} }
@ -146,16 +139,20 @@ void BlitScreen::DrawToFrame(RasterizerVulkan& rasterizer, Frame* frame,
vk::Framebuffer BlitScreen::CreateFramebuffer(const Layout::FramebufferLayout& layout, vk::Framebuffer BlitScreen::CreateFramebuffer(const Layout::FramebufferLayout& layout,
VkImageView image_view, VkImageView image_view,
VkFormat current_view_format) { VkFormat current_view_format) {
const bool format_updated = bool format_updated = swapchain_view_format != current_view_format;
std::exchange(swapchain_view_format, current_view_format) != current_view_format; swapchain_view_format = current_view_format;
if (!window_adapt || scaling_filter != filters.get_scaling_filter() || format_updated) { if (!window_adapt || scaling_filter != filters.get_scaling_filter() || format_updated) {
WaitIdle(); WaitIdle();
SetWindowAdaptPass(); SetWindowAdaptPass();
image_index = 0;
} }
const VkExtent2D extent{ const VkExtent2D extent{
.width = layout.width, .width = layout.width,
.height = layout.height, .height = layout.height,
}; };
return CreateFramebuffer(image_view, extent, window_adapt->GetRenderPass()); return CreateFramebuffer(image_view, extent, window_adapt->GetRenderPass());
} }

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