Compare commits

..

2 Commits

Author SHA1 Message Date
maufeat 469c690f5f headers 2026-02-12 17:17:15 +01:00
maufeat 0aacd0fadb initial rework of the launcher, using kotlin compose 2026-02-12 17:12:32 +01:00
248 changed files with 7488 additions and 4808 deletions
+35
View File
@@ -0,0 +1,35 @@
diff --git a/library/aesni.h b/library/aesni.h
index 754c984c79..59e27afd3e 100644
--- a/library/aesni.h
+++ b/library/aesni.h
@@ -35,7 +35,7 @@
/* GCC-like compilers: currently, we only support intrinsics if the requisite
* target flag is enabled when building the library (e.g. `gcc -mpclmul -msse2`
* or `clang -maes -mpclmul`). */
-#if (defined(__GNUC__) || defined(__clang__)) && defined(__AES__) && defined(__PCLMUL__)
+#if defined(__GNUC__) || defined(__clang__)
#define MBEDTLS_AESNI_HAVE_INTRINSICS
#endif
/* For 32-bit, we only support intrinsics */
diff --git a/library/aesni.c b/library/aesni.c
index 2857068..3e104ab 100644
--- a/library/aesni.c
+++ b/library/aesni.c
@@ -31,16 +31,14 @@
#include <immintrin.h>
#endif
-#if defined(MBEDTLS_ARCH_IS_X86)
#if defined(MBEDTLS_COMPILER_IS_GCC)
#pragma GCC push_options
#pragma GCC target ("pclmul,sse2,aes")
#define MBEDTLS_POP_TARGET_PRAGMA
-#elif defined(__clang__) && (__clang_major__ >= 5)
+#elif defined(__clang__)
#pragma clang attribute push (__attribute__((target("pclmul,sse2,aes"))), apply_to=function)
#define MBEDTLS_POP_TARGET_PRAGMA
#endif
-#endif
#if !defined(MBEDTLS_AES_USE_HARDWARE_ONLY)
/*
+20
View File
@@ -0,0 +1,20 @@
diff --git a/library/common.h b/library/common.h
index 50f2a29..c60d9dc 100644
--- a/library/common.h
+++ b/library/common.h
@@ -19,11 +19,11 @@
#include <stdint.h>
#include <stddef.h>
-#if defined(__ARM_NEON)
-#include <arm_neon.h>
+#if defined(MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64)
+#include <arm64_neon.h>
#define MBEDTLS_HAVE_NEON_INTRINSICS
-#elif defined(MBEDTLS_PLATFORM_IS_WINDOWS_ON_ARM64)
-#include <arm64_neon.h>
+#elif (defined(__ANDROID__) && defined(__ARM_FP)) || defined(__ARM_NEON)
+#include <arm_neon.h>
#define MBEDTLS_HAVE_NEON_INTRINSICS
#endif
+149 -88
View File
@@ -55,23 +55,29 @@ if (YUZU_STATIC_ROOM)
set(ENABLE_UPDATE_CHECKER OFF)
set(USE_DISCORD_PRESENCE OFF)
set(BUILD_TESTING OFF)
set(ENABLE_OPENSSL OFF)
set(ENABLE_WEB_SERVICE OFF)
set(ENABLE_LIBUSB OFF)
# allow static libs for boost though
# allow static libs for boost and mbedtls though
set(Boost_USE_STATIC_LIBS ON)
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
set(OPENSSL_USE_STATIC_LIBS ON)
set(MBEDTLS_LIB_SUFFIX "_static")
set(YUZU_USE_CPM ON)
set(zstd_FORCE_BUNDLED ON)
set(fmt_FORCE_BUNDLED ON)
endif()
# common network mbedtls
# common: xbyak? booost fmt lz4 zstd
# network: enet boost
# qt stuff
option(ENABLE_QT "Enable the Qt frontend" ON)
option(ENABLE_QT_TRANSLATION "Enable translations for the Qt frontend" OFF)
option(ENABLE_UPDATE_CHECKER "Enable update checker (for Qt and Android)" OFF)
# option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
option(YUZU_USE_QT_MULTIMEDIA "Use QtMultimedia for Camera" OFF)
cmake_dependent_option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF "NOT YUZU_USE_BUNDLED_QT" OFF)
set(YUZU_QT_MIRROR "" CACHE STRING "What mirror to use for downloading the bundled Qt libraries")
cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSVC}" "ENABLE_QT" OFF)
@@ -124,7 +130,12 @@ if (YUZU_STATIC_BUILD)
set(YUZU_USE_BUNDLED_OPENSSL ON)
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF)
## some libraries define a Library::Name_static alternative ##
set(MBEDTLS_LIB_SUFFIX _static)
elseif(APPLE)
set(YUZU_USE_CPM ON)
set(YUZU_USE_BUNDLED_FFMPEG ON)
set(YUZU_USE_BUNDLED_SDL2 ON)
set(YUZU_USE_BUNDLED_OPENSSL ON)
@@ -134,6 +145,7 @@ if (YUZU_STATIC_BUILD)
set(SPIRV-Tools_FORCE_BUNDLED ON)
set(SPIRV-Headers_FORCE_BUNDLED ON)
set(zstd_FORCE_BUNDLED ON)
set(MbedTLS_FORCE_BUNDLED ON)
endif()
endif()
@@ -186,6 +198,8 @@ if (MSVC OR ANDROID)
set(EXT_DEFAULT ON)
endif()
option(YUZU_USE_CPM "Use CPM to fetch system dependencies (fmt, boost, etc) if needed. Externals will still be fetched." ${EXT_DEFAULT})
# ffmpeg
option(YUZU_USE_BUNDLED_FFMPEG "Download bundled FFmpeg" ${EXT_DEFAULT})
cmake_dependent_option(YUZU_USE_EXTERNAL_FFMPEG "Build FFmpeg from external source" "${PLATFORM_SUN}" "NOT WIN32 AND NOT ANDROID" OFF)
@@ -231,12 +245,30 @@ cmake_dependent_option(YUZU_USE_BUNDLED_MOLTENVK "Download bundled MoltenVK lib"
option(YUZU_DISABLE_LLVM "Disable LLVM (useful for CI)" OFF)
set(DEFAULT_ENABLE_OPENSSL ON)
if (ANDROID OR WIN32 OR APPLE OR PLATFORM_SUN OR PLATFORM_OPENBSD)
# - Windows defaults to the Schannel backend.
# - macOS defaults to the SecureTransport backend.
# - Android currently has no SSL backend as the NDK doesn't include any SSL
# library; a proper 'native' backend would have to go through Java.
# - Solaris and OpenBSD have too old backends
# But you can force builds for those platforms to use OpenSSL if you have
# your own copy of it.
set(DEFAULT_ENABLE_OPENSSL OFF)
endif()
if (ENABLE_WEB_SERVICE OR USE_DISCORD_PRESENCE)
set(DEFAULT_ENABLE_OPENSSL ON)
endif()
option(ENABLE_OPENSSL "Enable OpenSSL backend for ISslConnection" ${DEFAULT_ENABLE_OPENSSL})
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL OFF)
if (EXT_DEFAULT OR PLATFORM_SUN OR PLATFORM_OPENBSD)
set(DEFAULT_YUZU_USE_BUNDLED_OPENSSL ON)
endif()
option(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" ${DEFAULT_YUZU_USE_BUNDLED_OPENSSL})
cmake_dependent_option(YUZU_USE_BUNDLED_OPENSSL "Download bundled OpenSSL build" ${DEFAULT_YUZU_USE_BUNDLED_OPENSSL} "ENABLE_OPENSSL" OFF)
if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL)
AddJsonPackage(vulkan-validation-layers)
@@ -364,94 +396,123 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
find_package(RenderDoc MODULE)
# openssl funniness
if (YUZU_USE_BUNDLED_OPENSSL)
if (ENABLE_OPENSSL)
if (YUZU_USE_BUNDLED_OPENSSL)
set(BUILD_SHARED_LIBS OFF)
AddJsonPackage(openssl)
if (OpenSSL_ADDED)
add_compile_definitions(YUZU_BUNDLED_OPENSSL)
endif()
endif()
find_package(OpenSSL 1.1.1 REQUIRED)
endif()
if (YUZU_USE_CPM)
message(STATUS "Fetching needed dependencies with CPM")
set(BUILD_SHARED_LIBS OFF)
AddJsonPackage(openssl)
if (OpenSSL_ADDED)
add_compile_definitions(YUZU_BUNDLED_OPENSSL)
endif()
endif()
set(BUILD_TESTING OFF)
set(ENABLE_TESTING OFF)
find_package(OpenSSL 3 REQUIRED)
# TODO(crueter): renderdoc?
message(STATUS "Fetching needed dependencies with CPM")
# boost
set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers process filesystem crc variant)
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
set(ENABLE_TESTING OFF)
AddJsonPackage(boost)
# boost
set(BOOST_INCLUDE_LIBRARIES algorithm icl pool container heap asio headers process filesystem crc variant)
# really annoying thing where boost::headers doesn't work with cpm
# TODO(crueter) investigate
set(BOOST_NO_HEADERS ${Boost_ADDED})
AddJsonPackage(boost)
# really annoying thing where boost::headers doesn't work with cpm
# TODO(crueter) investigate
set(BOOST_NO_HEADERS ${Boost_ADDED})
if (Boost_ADDED)
if (MSVC OR ANDROID)
add_compile_definitions(YUZU_BOOST_v1)
endif()
if (NOT MSVC OR CXX_CLANG)
# boost sucks
if (PLATFORM_SUN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthreads")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthreads")
if (Boost_ADDED)
if (MSVC OR ANDROID)
add_compile_definitions(YUZU_BOOST_v1)
endif()
target_compile_options(boost_heap INTERFACE -Wno-shadow)
target_compile_options(boost_icl INTERFACE -Wno-shadow)
target_compile_options(boost_asio INTERFACE -Wno-conversion -Wno-implicit-fallthrough)
endif()
endif()
if (NOT MSVC OR CXX_CLANG)
# boost sucks
if (PLATFORM_SUN)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthreads")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthreads")
endif()
# fmt
AddJsonPackage(fmt)
# lz4
AddJsonPackage(lz4)
if (lz4_ADDED)
add_library(lz4::lz4 ALIAS lz4_static)
endif()
# zstd
AddJsonPackage(zstd)
if (zstd_ADDED)
add_library(zstd::zstd ALIAS libzstd_static)
add_library(zstd::libzstd ALIAS libzstd_static)
endif()
if (NOT YUZU_STATIC_ROOM)
# nlohmann
AddJsonPackage(nlohmann)
# zlib
AddJsonPackage(zlib)
if (ZLIB_ADDED)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
endif()
# Opus
AddJsonPackage(opus)
if (Opus_ADDED)
if (MSVC AND CXX_CLANG)
target_compile_options(opus PRIVATE
-Wno-implicit-function-declaration
)
target_compile_options(boost_heap INTERFACE -Wno-shadow)
target_compile_options(boost_icl INTERFACE -Wno-shadow)
target_compile_options(boost_asio INTERFACE -Wno-conversion -Wno-implicit-fallthrough)
endif()
endif()
if (NOT TARGET Opus::opus)
add_library(Opus::opus ALIAS opus)
# fmt
AddJsonPackage(fmt)
# lz4
AddJsonPackage(lz4)
if (lz4_ADDED)
add_library(lz4::lz4 ALIAS lz4_static)
endif()
# zstd
AddJsonPackage(zstd)
if (zstd_ADDED)
add_library(zstd::zstd ALIAS libzstd_static)
add_library(zstd::libzstd ALIAS libzstd_static)
endif()
if (NOT YUZU_STATIC_ROOM)
# nlohmann
AddJsonPackage(nlohmann)
# zlib
AddJsonPackage(zlib)
if (ZLIB_ADDED)
add_library(ZLIB::ZLIB ALIAS zlibstatic)
endif()
# Opus
AddJsonPackage(opus)
if (Opus_ADDED)
if (MSVC AND CXX_CLANG)
target_compile_options(opus PRIVATE
-Wno-implicit-function-declaration
)
endif()
endif()
if (NOT TARGET Opus::opus)
add_library(Opus::opus ALIAS opus)
endif()
endif()
else()
# TODO: we can probably just use CPM for this... right?
# Enforce the search mode of non-required packages for better and shorter failure messages
find_package(fmt 8 REQUIRED)
if (NOT YUZU_DISABLE_LLVM)
find_package(LLVM MODULE COMPONENTS Demangle)
endif()
find_package(nlohmann_json 3.8 REQUIRED)
find_package(lz4 REQUIRED)
find_package(RenderDoc MODULE)
find_package(stb MODULE)
find_package(Opus 1.3 MODULE REQUIRED)
find_package(ZLIB 1.2 REQUIRED)
find_package(zstd 1.5 REQUIRED MODULE)
# wow
find_package(Boost 1.57.0 CONFIG REQUIRED OPTIONAL_COMPONENTS headers context system fiber filesystem)
if (ENABLE_OPENSSL)
find_package(OpenSSL 1.1.1 REQUIRED)
endif()
endif()
@@ -513,8 +574,8 @@ message(STATUS "Platform Libraries: ${PLATFORM_LIBRARIES}")
add_subdirectory(externals)
# pass targets from externals
# TODO(crueter): CPMUtil Propagate func?
find_package(enet)
find_package(MbedTLS)
find_package(unordered_dense REQUIRED)
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
@@ -580,11 +641,11 @@ if (ENABLE_QT)
list(APPEND CMAKE_PREFIX_PATH "${Qt6_DIR}")
endif()
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Charts Concurrent)
find_package(Qt6 CONFIG REQUIRED COMPONENTS Widgets Concurrent)
# if (YUZU_USE_QT_MULTIMEDIA)
# find_package(Qt6 REQUIRED COMPONENTS Multimedia)
# endif()
if (YUZU_USE_QT_MULTIMEDIA)
find_package(Qt6 REQUIRED COMPONENTS Multimedia)
endif()
if (PLATFORM_LINUX OR PLATFORM_FREEBSD)
# yes Qt, we get it
@@ -619,13 +680,13 @@ if (ENABLE_QT)
## Components ##
# Best practice is to ask for all components at once, so they are from the same version
set(YUZU_QT_COMPONENTS Core Widgets Charts Concurrent)
set(YUZU_QT_COMPONENTS Core Widgets Concurrent)
if (PLATFORM_LINUX)
list(APPEND YUZU_QT_COMPONENTS DBus)
endif()
# if (YUZU_USE_QT_MULTIMEDIA)
# list(APPEND YUZU_QT_COMPONENTS Multimedia)
# endif()
if (YUZU_USE_QT_MULTIMEDIA)
list(APPEND YUZU_QT_COMPONENTS Multimedia)
endif()
if (YUZU_USE_QT_WEB_ENGINE)
list(APPEND YUZU_QT_COMPONENTS WebEngineCore WebEngineWidgets)
endif()
+1 -5
View File
@@ -84,11 +84,7 @@ function(SystemPackageViable JSON_NAME)
parse_object(${object})
string(REPLACE " " ";" find_args "${find_args}")
if (${package}_FORCE_BUNDLED)
set(${package}_FOUND OFF)
else()
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
endif()
find_package(${package} ${version} ${find_args} QUIET NO_POLICY_SCOPE)
set(${pkg}_VIABLE ${${package}_FOUND} PARENT_SCOPE)
set(${pkg}_PACKAGE ${package} PARENT_SCOPE)
+4 -7
View File
@@ -36,17 +36,14 @@ set(GIT_DESC ${BUILD_VERSION})
# Also if this is a CI build, add the build name (ie: Nightly, Canary) to the scm_rev file as well
# Auto-updater metadata! Must somewhat mirror GitHub API endpoint
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
set(BUILD_AUTO_UPDATE_API "http://api.github.com")
if (NIGHTLY_BUILD)
set(BUILD_AUTO_UPDATE_WEBSITE "https://github.com")
set(BUILD_AUTO_UPDATE_API "api.github.com")
set(BUILD_AUTO_UPDATE_API_PATH "/repos/")
set(BUILD_AUTO_UPDATE_REPO "Eden-CI/Nightly")
set(REPO_NAME "Eden Nightly")
else()
set(BUILD_AUTO_UPDATE_WEBSITE "https://git.eden-emu.dev")
set(BUILD_AUTO_UPDATE_API "git.eden-emu.dev")
set(BUILD_AUTO_UPDATE_API_PATH "/api/v1/repos/")
set(BUILD_AUTO_UPDATE_REPO "eden-emu/eden")
set(BUILD_AUTO_UPDATE_REPO "eden-emulator/Releases")
set(REPO_NAME "Eden")
endif()
+1 -1
View File
@@ -5,7 +5,7 @@
"name": "openssl",
"repo": "crueter-ci/OpenSSL",
"version": "3.6.0-1cb0d36b39",
"min_version": "3"
"min_version": "1.1.1"
},
"boost": {
"package": "Boost",
-211
View File
@@ -1,211 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
fill="none"
viewBox="0 0 512 512"
version="1.1"
id="svg7"
sodipodi:docname="EdenLogoLoveWhiteV3.svg"
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
xml:space="preserve"
inkscape:export-filename="dev.eden_emu.eden.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata1">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:creator>
<cc:Agent>
<dc:title>Madeline_Dev</dc:title>
<dc:identifier>mailto:madelvidel@gmail.com</dc:identifier>
</cc:Agent>
</dc:creator>
<dc:date>2025</dc:date>
<dc:license
rdf:resource="https://www.gnu.org/licenses/gpl-3.0.html" />
<dc:rights>2025 Eden Emulator Project</dc:rights>
<dc:source>https://git.eden-emu.dev</dc:source>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7"><linearGradient
id="swatch14"
inkscape:swatch="solid"><stop
style="stop-color:#66003b;stop-opacity:1;"
offset="0"
id="stop14" /></linearGradient><linearGradient
id="linearGradient1"
inkscape:collect="always"><stop
style="stop-color:#ffffff;stop-opacity:0.50980395;"
offset="0"
id="stop1" /><stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop2" /></linearGradient><linearGradient
id="linearGradient11"
inkscape:collect="always"><stop
style="stop-color:#9523a7;stop-opacity:0.50980395;"
offset="0"
id="stop11" /><stop
style="stop-color:#ffabad;stop-opacity:1;"
offset="0.99898213"
id="stop20" /><stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="0.99898213"
id="stop12" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient12"
x1="109.74531"
y1="106.54533"
x2="431.05463"
y2="427.85461"
gradientUnits="userSpaceOnUse"
spreadMethod="reflect"
gradientTransform="matrix(1.0945321,0,0,1.0945321,-39.661525,-35.159057)" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="125.40197"
y1="271.834"
x2="431.02424"
y2="271.834"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.0964243,0,0,1.0253208,-40.187969,-20.060025)" /><filter
inkscape:label="Light Contour"
inkscape:menu="Image Paint and Draw"
inkscape:menu-tooltip="Uses vertical specular light to draw lines"
style="color-interpolation-filters:sRGB"
id="filter11"
x="-0.01907517"
y="-0.054959154"
width="1.0379885"
height="1.1092314"><feGaussianBlur
in="SourceGraphic"
stdDeviation="0.38250006"
result="result3"
id="feGaussianBlur9" /><feComponentTransfer
result="result1"
in="result3"
id="feComponentTransfer9"><feFuncR
type="discrete"
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
id="feFuncR9" /><feFuncG
type="discrete"
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
id="feFuncG9" /><feFuncB
type="discrete"
tableValues="0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1"
id="feFuncB9" /></feComponentTransfer><feGaussianBlur
result="result5"
stdDeviation="0.01"
id="feGaussianBlur10" /><feBlend
in2="result5"
result="result6"
mode="lighten"
in="result5"
id="feBlend10" /><feColorMatrix
in="result6"
type="luminanceToAlpha"
result="result2"
id="feColorMatrix10" /><feSpecularLighting
surfaceScale="5"
result="result9"
specularExponent="20"
in="result2"
specularConstant="1"
id="feSpecularLighting10"><feDistantLight
azimuth="180"
elevation="90"
id="feDistantLight10" /></feSpecularLighting><feComposite
in2="result6"
operator="arithmetic"
in="result9"
k1="0.4"
k3="0.7"
result="result3"
id="feComposite10"
k2="0"
k4="0" /><feBlend
in2="result1"
in="result3"
mode="normal"
result="result8"
id="feBlend11" /><feComposite
in2="SourceGraphic"
in="result8"
operator="in"
result="result7"
id="feComposite11" /></filter></defs><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.35"
inkscape:cx="254.81481"
inkscape:cy="260.37037"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg7"
showguides="false" /><circle
style="fill:url(#linearGradient12);fill-opacity:1;stroke:#e4e4e4;stroke-width:14.0448;stroke-opacity:1;paint-order:stroke fill markers"
id="path8"
cx="256.2999"
cy="257.2999"
r="248.67769" /><path
id="path15"
style="fill:url(#linearGradient2);fill-opacity:1;stroke:#ffffff;stroke-width:16.9642;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.52156866;paint-order:stroke fill markers"
d="m 306.72111,24.233031 c 0,0 -11.98397,40.08696 -18.0546,60.431848 -12.96613,9.503601 -21.49377,18.397701 -21.49377,18.397701 0,0 -23.41313,-31.029398 -45.74145,-43.934598 -22.32833,-12.905201 -52.42065,-11.242483 -52.42065,-11.242483 0,0 -12.50052,4e-4 -27.63117,5.537132 -15.13066,5.536732 -21.27107,9.227888 -21.27107,9.227888 0,0 15.35165,-0.410529 37.93799,6.766716 22.58635,7.177243 32.45374,11.484796 32.45374,11.484796 l 31.02752,-6.562453 -8.44161,11.074275 c 0,0 14.80259,8.920284 22.80648,16.917787 8.0039,7.9975 11.73088,12.50812 11.73088,12.50812 0,0 -13.92373,-5.43341 -56.68427,-1.74226 -42.76055,3.69116 -84.86368,56.39265 -84.86368,56.39265 0,0 41.22428,-15.9958 65.5649,-21.32747 24.34062,-5.33166 47.58524,-7.9983 47.58524,-7.9983 0,0 -18.41865,7.3827 -38.15428,38.3474 -19.73564,30.96468 -14.0351,80.18128 -14.0351,80.18128 0,0 31.35774,-59.05848 61.39977,-78.94969 30.04203,-19.89124 36.182,-20.50642 36.182,-20.50642 0,0 -24.1209,48.39514 -33.55015,126.11445 -9.42924,77.71931 30.26089,207.72959 30.26089,207.72959 l 47.80367,-6.97298 c 0,0 -45.61041,-42.24281 -49.55753,-151.33695 -3.94713,-109.09413 16.66479,-174.30453 16.66479,-174.30453 0,0 17.10508,1.23039 42.54213,32.81027 25.43704,31.57987 40.34713,57.41795 40.34713,57.41795 0,0 9.65024,-51.26683 -11.4011,-74.64415 -21.05135,-23.3773 -53.50637,-33.62931 -53.50637,-33.62931 0,0 13.70573,-6.66419 42.10313,-4.71609 28.39738,1.94813 87.30278,27.12897 87.30278,27.12896 0,0 -15.8158,-25.5214 -52.87463,-43.21167 -37.05881,-17.69028 -81.35597,0.29238 -81.35597,0.29238 0,0 4.13589,-15.37277 29.10021,-32.628037 24.96432,-17.25527 56.907,-34.36427 56.907,-34.36427 0,0 -26.3595,1.160335 -55.20025,12.035504 -7.176,2.7059 -14.064,6.299542 -20.36309,10.173104 l 17.40574,-51.051762 z" /><path
id="path4-2-6"
style="fill:#ffffff;fill-opacity:0.509804;stroke:#ffffff;stroke-width:8.49614;stroke-dasharray:none;stroke-opacity:0.521569"
d="m 105.64857,363.95957 a 27.851731,25.309928 0 0 0 -7.115581,35.20471 27.851731,25.309928 0 0 0 5.656921,5.48617 c 7.16052,5.83605 21.58543,14.65841 45.88391,17.847 37.57172,4.93039 41.7105,10.38005 41.71122,10.381 -4.5e-4,-6e-4 -4.13529,-5.45309 4.14459,-39.11953 5.35491,-21.77281 1.40624,-37.16078 -2.05217,-45.31698 a 27.851731,25.309928 0 0 0 -3.63281,-6.75471 27.851731,25.309928 0 0 0 -38.74027,-6.4662 27.851731,25.309928 0 0 0 -7.11556,35.20472 27.851731,25.309928 0 0 0 -38.74025,-6.46618 z" /><path
id="path4-2-6-1"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:6.89965;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 351.29524,378.32915 a 20.862787,22.283365 87.929759 0 0 -28.94663,11.7882 20.862787,22.283365 87.929759 0 0 -1.50914,6.04125 c -1.09835,7.13663 -1.16134,20.16231 7.28339,36.72931 13.0577,25.61689 10.90177,30.69793 10.90138,30.69882 2.4e-4,-5.6e-4 2.16,-5.08045 30.49632,-15.43688 18.32576,-6.69765 27.84648,-16.23659 32.287,-22.14109 a 20.862787,22.283365 87.929759 0 0 3.33646,-5.36755 20.862787,22.283365 87.929759 0 0 -12.45108,-27.05014 20.862787,22.283365 87.929759 0 0 -28.94664,11.78822 20.862787,22.283365 87.929759 0 0 -12.45106,-27.05014 z" /><path
id="path4-2-6-1-5"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:6.89965;stroke-dasharray:none;stroke-opacity:0.52156866"
d="m 83.024352,249.4105 a 22.283365,20.862787 0 0 0 -29.353581,10.73482 22.283365,20.862787 0 0 0 -1.72639,5.98278 c -1.35544,7.0923 -1.88894,20.10721 5.951801,36.96846 12.12378,26.07187 9.7857,31.07171 9.78528,31.07259 2.6e-4,-5.5e-4 2.34212,-4.99911 31.03406,-14.32513 18.555748,-6.03127 28.414858,-15.22005 33.065768,-20.96029 a 22.283365,20.862787 0 0 0 3.52818,-5.24352 22.283365,20.862787 0 0 0 -11.46577,-27.48227 22.283365,20.862787 0 0 0 -29.353588,10.73483 22.283365,20.862787 0 0 0 -11.46576,-27.48227 z" /><path
id="path4-2-6-1-9"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:5.01078;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 320.97602,291.36362 a 16.625347,14.748181 0 0 0 -21.90036,7.58859 16.625347,14.748181 0 0 0 -1.28804,4.2293 c -1.01128,5.01364 -1.40932,14.21406 4.44057,26.1335 9.0454,18.43055 7.30098,21.965 7.30067,21.96562 2e-4,-3.9e-4 1.74743,-3.53394 23.15415,-10.12663 13.84423,-4.26358 21.19999,-10.75925 24.66998,-14.8171 a 16.625347,14.748181 0 0 0 2.63233,-3.70671 16.625347,14.748181 0 0 0 -8.55447,-19.42758 16.625347,14.748181 0 0 0 -21.90037,7.58859 16.625347,14.748181 0 0 0 -8.55446,-19.42758 z" /><path
id="path4-2-6-1-9-8"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:4.22313;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 416.90426,193.13811 a 12.322476,14.134158 82.30909 0 0 -17.56469,8.83612 12.322476,14.134158 82.30909 0 0 -0.59553,3.65007 c -0.27175,4.26676 0.45709,11.92642 6.76182,21.10665 9.74867,14.19503 8.68847,17.32338 8.68827,17.3239 1.2e-4,-3.3e-4 1.06282,-3.12827 18.32699,-11.08266 11.16516,-5.14424 16.60821,-11.37802 19.06101,-15.14093 a 12.322476,14.134158 82.30909 0 0 1.788,-3.37453 12.322476,14.134158 82.30909 0 0 -9.45059,-15.07737 12.322476,14.134158 82.30909 0 0 -17.5647,8.83612 12.322476,14.134158 82.30909 0 0 -9.45058,-15.07737 z" /><path
id="path4-2-6-1-9-8-4"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:3.48342;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 96.169666,99.754795 a 10.296362,11.508677 62.365085 0 0 -7.330189,13.841365 10.296362,11.508677 62.365085 0 0 1.473186,2.6805 c 2.008889,2.93989 6.437543,7.66269 15.415927,10.84633 13.88288,4.92276 14.77391,7.47086 14.77407,7.4713 -1e-4,-2.8e-4 -0.88925,-2.54921 6.69094,-15.67164 4.90232,-8.48655 5.37758,-15.08279 5.10162,-18.6847 a 10.296362,11.508677 62.365085 0 0 -0.5257,-3.042454 10.296362,11.508677 62.365085 0 0 -14.1348,-5.641051 10.296362,11.508677 62.365085 0 0 -7.33021,13.841385 10.296362,11.508677 62.365085 0 0 -14.134844,-5.641035 z" /><path
id="path4-2-6-1-9-8-4-9-2"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:3.05139;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 296.64657,233.26347 a 8.8114666,10.319195 80.445391 0 0 -11.58972,8.06989 8.8114666,10.319195 80.445391 0 0 0.0366,2.62543 c 0.35019,3.02392 1.86017,8.32306 7.60386,14.09672 8.8812,8.92759 8.51432,11.23624 8.51426,11.23663 5e-5,-2.5e-4 0.36876,-2.30885 11.85385,-9.72788 7.4277,-4.79799 10.57103,-9.75243 11.86523,-12.65455 a 8.8114666,10.319195 80.445391 0 0 0.86244,-2.55882 8.8114666,10.319195 80.445391 0 0 -8.77837,-9.57865 8.8114666,10.319195 80.445391 0 0 -11.58975,8.06989 8.8114666,10.319195 80.445391 0 0 -8.77842,-9.57866 z" /><path
id="path4-2-6-1-9-8-4-9-2-6"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:2.49738;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 403.95446,91.44744 a 7.0950375,8.5844443 82.142743 0 0 -9.64888,6.492875 7.0950375,8.5844443 82.142743 0 0 0.0305,2.112375 c 0.29155,2.43298 1.54866,6.69657 6.33049,11.34195 7.39392,7.18298 7.08848,9.04046 7.08843,9.04078 4e-5,-2.1e-4 0.307,-1.85765 9.86876,-7.82686 6.18383,-3.86038 8.80077,-7.84663 9.87824,-10.18161 a 7.0950375,8.5844443 82.142743 0 0 0.718,-2.05879 7.0950375,8.5844443 82.142743 0 0 -7.30831,-7.706798 7.0950375,8.5844443 82.142743 0 0 -9.64889,6.492875 7.0950375,8.5844443 82.142743 0 0 -7.30834,-7.706797 z" /><path
id="path4-2-6-1-9-8-4-9-5"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:7.54176;stroke-dasharray:none;stroke-opacity:0.521569"
d="m 410.46352,265.86621 a 24.802737,22.39469 2.0523303 0 0 -30.17074,15.95063 24.802737,22.39469 2.0523303 0 0 -0.74427,6.61415 c -0.12337,7.7333 1.79614,21.62064 13.65297,38.28529 18.33368,25.76802 16.72835,31.4347 16.72811,31.43572 1.4e-4,-6.3e-4 1.60998,-5.66652 31.32518,-20.02007 19.21759,-9.28266 28.27643,-20.56192 32.27921,-27.37361 a 24.802737,22.39469 2.0523303 0 0 2.8654,-6.11021 24.802737,22.39469 2.0523303 0 0 -17.88248,-27.36631 24.802737,22.39469 2.0523303 0 0 -30.17081,15.95066 24.802737,22.39469 2.0523303 0 0 -17.88257,-27.36625 z" /><path
id="path4-2-6-1-9-8-5"
style="fill:#ffffff;fill-opacity:0.50980395;stroke:#ffffff;stroke-width:3.6405;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 114.43099,189.33754 a 10.333135,12.525309 79.004057 0 0 -14.00848,9.75499 10.333135,12.525309 79.004057 0 0 0.0547,3.08382 c 0.43602,3.54457 2.28588,9.73496 9.26636,16.38295 10.79359,10.27954 10.35819,13.00063 10.3581,13.00111 6e-5,-3e-4 0.4377,-2.72141 14.32197,-11.70923 8.9793,-5.8126 12.76786,-11.70785 14.32433,-15.14812 a 10.333135,12.525309 79.004057 0 0 1.03481,-3.02677 10.333135,12.525309 79.004057 0 0 -10.67161,-11.04688 10.333135,12.525309 79.004057 0 0 -14.00848,9.75501 10.333135,12.525309 79.004057 0 0 -10.6716,-11.04688 z" /><path
id="path4-2-6-1-9-8-4-9-2-2"
style="fill:#ffffff;fill-opacity:0.5117;stroke:#ffffff;stroke-width:3.62674;stroke-dasharray:none;stroke-opacity:0.519962"
d="m 178.56055,273.32338 a 10.656883,12.053184 66.089768 0 0 -7.97821,14.16205 10.656883,12.053184 66.089768 0 0 1.49012,2.79504 c 2.04675,3.07265 6.5954,8.03049 15.94965,11.48124 14.46412,5.33577 15.34612,7.98353 15.34628,7.98398 -8e-5,-2.8e-4 -0.88012,-2.64887 7.3455,-16.0639 5.31973,-8.67576 5.95524,-15.47956 5.74045,-19.20452 a 10.656883,12.053184 66.089768 0 0 -0.48843,-3.15169 10.656883,12.053184 66.089768 0 0 -14.7135,-6.08217 10.656883,12.053184 66.089768 0 0 -7.97826,14.1621 10.656883,12.053184 66.089768 0 0 -14.71355,-6.08215 z" /></svg>

Before

Width:  |  Height:  |  Size: 15 KiB

-1
View File
@@ -1 +0,0 @@
#e48cc9ff
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

+3 -11
View File
@@ -22,25 +22,17 @@ Debug logs can be found in General -> Debug -> Open Log Location on desktop, and
Ignoring SIGSEGV when debugging in host:
- **gdb**: `handle SIGSEGV nostop pass`.
- **gdb**: `handle all nostop pass`.
- **lldb**: `pro hand -p true -s false -n false SIGSEGV`.
## Debugging (guest code)
### gdb
You must have GDB installed for aarch64 to debug the target. Install it through your package manager, e.g.:
* On Arch:
* `sudo pacman -Syu aarch64-linux-gnu-gdb`
* On Gentoo:
* `sudo emerge --ask crossdev`
* `sudo crossdev -t aarch64-unknown-linux-gnu --ex-gdb`
Run `./build/bin/eden-cli -c <path to your config file (see logs where you run eden normally to see where it is)> -d -g <path to game>`
Or `Enable GDB Stub` at General > Debug, then hook up an aarch64-gdb:
* `target remote localhost:6543`
Type `c` (for continue) and then if it crashes just do a `bt` (backtrace) and `layout asm`
Then hook up an aarch64-gdb (use `yay aarch64-gdb` or `sudo pkg in arch64-gdb` to install)
Then type `target remote localhost:1234` and type `c` (for continue) - and then if it crashes just do a `bt` (backtrace) and `layout asm`.
### gdb cheatsheet
+16 -14
View File
@@ -51,11 +51,12 @@ All other dependencies will be downloaded and built by [CPM](https://github.com/
* [fmt](https://fmt.dev/) 8.0.1+
* [lz4](http://www.lz4.org)
* [nlohmann\_json](https://github.com/nlohmann/json) 3.8+
* [OpenSSL](https://www.openssl.org/source/) 3+
* [OpenSSL](https://www.openssl.org/source/) 1.1.1+
* [ZLIB](https://www.zlib.net/) 1.2+
* [zstd](https://facebook.github.io/zstd/) 1.5+
* [enet](http://enet.bespin.org/) 1.3+
* [Opus](https://opus-codec.org/) 1.3+
* [MbedTLS](https://github.com/Mbed-TLS/mbedtls) 3+
Vulkan 1.3.274+ is also needed:
@@ -120,7 +121,7 @@ sudo emerge -a \
dev-util/vulkan-utility-libraries dev-util/glslang \
media-gfx/renderdoc media-libs/libva media-libs/opus media-video/ffmpeg \
media-libs/VulkanMemoryAllocator media-libs/libsdl2 media-libs/cubeb \
net-libs/enet \
net-libs/enet net-libs/mbedtls \
sys-libs/zlib \
dev-cpp/nlohmann_json dev-cpp/simpleini dev-cpp/cpp-httplib dev-cpp/cpp-jwt \
games-util/gamemode \
@@ -138,6 +139,7 @@ Required USE flags:
* `dev-qt/qtbase network concurrent dbus gui widgets`
* `dev-libs/quazip qt6`
* `net-libs/mbedtls cmac`
* `media-libs/libsdl2 haptic joystick sound video`
* `dev-cpp/cpp-httplib ssl`
@@ -149,7 +151,7 @@ Required USE flags:
<summary>Arch Linux</summary>
```sh
sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 ninja nlohmann-json openssl opus qt6-base qt6-multimedia qt6-charts sdl2 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers
sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glslang libzip lz4 mbedtls ninja nlohmann-json openssl opus qt6-base qt6-multimedia sdl2 zlib zstd zip unzip vulkan-headers vulkan-utility-libraries libusb spirv-tools spirv-headers
```
* Building with QT Web Engine requires `qt6-webengine` as well.
@@ -162,7 +164,7 @@ sudo pacman -Syu --needed base-devel boost catch2 cmake enet ffmpeg fmt git glsl
<summary>Ubuntu, Debian, Mint Linux</summary>
```sh
sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev qt6-charts-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl2-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev
sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev libhidapi-dev libpulse-dev libtool libudev-dev libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-xkb1 libxext-dev libxkbcommon-x11-0 mesa-common-dev nasm ninja-build qt6-base-private-dev libmbedtls-dev catch2 libfmt-dev liblz4-dev nlohmann-json3-dev libzstd-dev libssl-dev libavfilter-dev libavcodec-dev libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev qt6-tools-dev libvulkan-dev spirv-tools spirv-headers libusb-1.0-0-dev libxbyak-dev libboost-dev libboost-fiber-dev libboost-context-dev libsdl2-dev libopus-dev libasound2t64 vulkan-utility-libraries-dev
```
* Ubuntu 22.04, Linux Mint 20, or Debian 12 or later is required.
@@ -176,7 +178,7 @@ sudo apt-get install autoconf cmake g++ gcc git glslang-tools libglu1-mesa-dev l
Fedora:
```sh
sudo dnf install autoconf cmake fmt-devel gcc{,-c++} glslang hidapi-devel json-devel libtool libusb1-devel libzstd-devel lz4-devel nasm ninja-build openssl-devel pulseaudio-libs-devel qt6-linguist qt6-qtbase{-private,}-devel qt6-qtwebengine-devel qt6-qtmultimedia-devel qt6-charts-devel speexdsp-devel wayland-devel zlib-devel ffmpeg-devel libXext-devel boost jq
sudo dnf install autoconf cmake fmt-devel gcc{,-c++} glslang hidapi-devel json-devel libtool libusb1-devel libzstd-devel lz4-devel nasm ninja-build openssl-devel pulseaudio-libs-devel qt6-linguist qt6-qtbase{-private,}-devel qt6-qtwebengine-devel qt6-qtmultimedia-devel speexdsp-devel wayland-devel zlib-devel ffmpeg-devel libXext-devel boost jq
```
AlmaLinux (use `YUZU_USE_CPM=ON`):
@@ -209,15 +211,17 @@ First, enable the community repository; [see here](https://wiki.alpinelinux.org/
# Enable the community repository
setup-apkrepos -c
# Install
apk add g++ git cmake make mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev qt6-charts-dev libusb-dev libtool boost-dev sdl2-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev opus-dev jq patch
apk add g++ git cmake make mbedtls-dev mbedtls-static mesa-dev qt6-qtbase-dev qt6-qtbase-private-dev libquazip1-qt6 ffmpeg-dev libusb-dev libtool boost-dev sdl2-dev zstd-dev vulkan-utility-libraries spirv-tools-dev openssl-dev nlohmann-json lz4-dev opus-dev jq patch
```
`mbedtls-static` has to be specified otherwise `libeverest.a` and `libp256m.a` will fail to be found.
</details>
<details>
<summary>Void Linux</summary>
```sh
xbps-install -Su git make cmake clang pkg-config patch SPIRV-Tools-devel SPIRV-Headers lz4 liblz4-devel boost-devel ffmpeg6-devel catch2 Vulkan-Utility-Libraries Vulkan-Headers glslang openssl-devel SDL2-devel quazip-qt6-devel qt6-base-devel qt6-qt5compat-devel qt6-charts-devel fmt-devel json-c++ libenet-devel libusb-devel
xbps-install -Su git make cmake clang pkg-config patch mbedtls-devel SPIRV-Tools-devel SPIRV-Headers lz4 liblz4-devel boost-devel ffmpeg6-devel catch2 Vulkan-Utility-Libraries Vulkan-Headers glslang openssl-devel SDL2-devel quazip-qt6-devel qt6-base-devel qt6-qt5compat-devel fmt-devel json-c++ libenet-devel libusb-devel
```
Yes, `nlohmann-json` is just named `json-c++`. Why?
@@ -229,8 +233,6 @@ Yes, `nlohmann-json` is just named `json-c++`. Why?
A convenience script is provided on the root of this project [shell.nix](../shell.nix). Run the usual `nix-shell`.
If you're going for a pure build (i.e no downloaded deps), use `-DYUZU_USE_CPM=ON -DCPMUTIL_FORCE_SYSTEM=ON`.
</details>
<details>
<summary>macOS</summary>
@@ -255,7 +257,7 @@ brew install molten-vk
<details>
<summary>FreeBSD</summary>
As root run: `pkg install devel/cmake devel/sdl20 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base devel/qt6-charts devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib devel/unordered-dense vulkan-headers quazip-qt6`
As root run: `pkg install devel/cmake devel/sdl20 devel/boost-libs devel/catch2 devel/libfmt devel/nlohmann-json devel/ninja devel/nasm devel/autoconf devel/pkgconf devel/qt6-base devel/simpleini net/enet multimedia/ffnvcodec-headers multimedia/ffmpeg audio/opus archivers/liblz4 lang/gcc12 graphics/glslang graphics/vulkan-utility-libraries graphics/spirv-tools www/cpp-httplib devel/unordered-dense mbedtls3 vulkan-headers quazip-qt6`
If using FreeBSD 12 or prior, use `devel/pkg-config` instead.
@@ -265,7 +267,7 @@ If using FreeBSD 12 or prior, use `devel/pkg-config` instead.
<details>
<summary>NetBSD</summary>
For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq libopus qt6 cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx`.
For NetBSD +10.1: `pkgin install git cmake boost fmtlib SDL2 catch2 libjwt spirv-headers spirv-tools ffmpeg7 libva nlohmann-json jq libopus qt6 mbedtls3 cpp-httplib lz4 vulkan-headers nasm autoconf enet pkg-config libusb1 libcxx`.
[Caveats](./Caveats.md#netbsd).
@@ -285,7 +287,7 @@ pkg_add cmake nasm git boost unzip--iconv autoconf-2.72p0 bash ffmpeg glslang gm
<summary>DragonFlyBSD</summary>
```sh
pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmake jq nlohmann-json enet spirv-tools sdl2 vulkan-utility-libraries vulkan-headers catch2 libfmt openssl liblz4 boost-libs cpp-httplib qt6-base qt6-charts quazip-qt6 unordered-dense libva-vdpau-driver libva-utils libva-intel-driver
pkg install gcc14 git cmake unzip nasm autoconf bash pkgconf ffmpeg glslang gmake jq nlohmann-json enet spirv-tools sdl2 vulkan-utility-libraries vulkan-headers catch2 libfmt openssl liblz4 boost-libs cpp-httplib qt6-base quazip-qt6 unordered-dense libva-vdpau-driver libva-utils libva-intel-driver
```
[Caveats](./Caveats.md#dragonflybsd).
@@ -309,7 +311,7 @@ sudo pkg install qt6 boost glslang libzip library/lz4 libusb-1 nlohmann-json ope
```sh
BASE="git make autoconf libtool automake-wrapper jq patch"
MINGW="qt6-base qt6-charts qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus libusb unordered_dense openssl SDL2"
MINGW="qt6-base qt6-tools qt6-translations qt6-svg cmake toolchain clang python-pip openssl vulkan-memory-allocator vulkan-devel glslang boost fmt lz4 nlohmann-json zlib zstd enet opus mbedtls libusb unordered_dense openssl SDL2"
# Either x86_64 or clang-aarch64 (Windows on ARM)
packages="$BASE"
for pkg in $MINGW; do
@@ -335,7 +337,7 @@ pacman -Syuu --needed --noconfirm $packages
<summary>HaikuOS</summary>
```sh
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.89_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl2_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel glslang qt6_devel qt6_charts_devel
pkgman install git cmake patch libfmt_devel nlohmann_json lz4_devel opus_devel boost1.89_devel vulkan_devel qt6_base_devel qt6_declarative_devel libsdl2_devel ffmpeg7_devel libx11_devel enet_devel catch2_devel quazip1_qt5_devel qt6_5compat_devel mbedtls3_devel glslang qt6_devel
```
[Caveats](./Caveats.md#haikuos).
+9
View File
@@ -286,6 +286,15 @@ Exclusive OR (i.e.: XOR)
Memory access.
### Terminal: Interpret
```c++
SetTerm(IR::Term::Interpret{next})
```
This terminal instruction calls the interpreter, starting at `next`.
The interpreter must interpret exactly one instruction.
### Terminal: ReturnToDispatch
```c++
+5
View File
@@ -117,6 +117,11 @@ public:
MemoryWrite32(vaddr + 4, u32(value >> 32));
}
void InterpreterFallback(u32 pc, size_t num_instructions) override {
// This is never called in practice.
std::terminate();
}
void CallSVC(u32 swi) override {
// Do something.
}
+2 -41
View File
@@ -12,13 +12,12 @@ The setting ranges from 0 to 65535 (0x0000 to 0xFFFF), where each bit represents
* [Accessing Debug Knobs (dev side)](#accessing-debug-knobs-dev-side)
* [Setting Debug Knobs (user side)](#setting-debug-knobs-user-side)
* [Bit Manipulation Examples](#bit-manipulation-examples)
3. [Terminology and user communication](#terminology-and-user-communication)
4. [Examples](#examples)
3. [Examples](#examples)
* [Example 1: Conditional Debug Logging](#example-1-conditional-debug-logging)
* [Example 2: Performance Tuning](#example-2-performance-tuning)
* [Example 3: Feature Gating](#example-3-feature-gating)
5. [Best Practices](#best-practices)
4. [Best Practices](#best-practices)
---
@@ -78,44 +77,6 @@ To enable specific features, calculate the decimal value by setting the appropri
* **Enable bits 0 and 1**: Value = 3 (2^0 + 2^1)
* **Enable bit 15**: Value = 32768 (2^15)
## Terminology and user communication
There are two main confusions when talking about knobs:
### Whether it's zero-based or one-based
Sometimes when an user reports: knobs 1 and 2 gets better performance, dev may get confuse whether he means the knobs 1 and 2 literally, or the 1st and 2nd knobs (knobs 0 and 1).
Debug knobs are **zero-based**, which means:
* The first knob is the knob(0) (or knob0 henceforth), and the last one is the 15 (knob15, likewise)
* You can talk: "knob0 is enabled/disabled", "In this video i was using only knobs 0 and 2", etc.
### Whether one is talking about the knob itself or about the entire parameter value (which represents all knobs)
Sometimes when an user reports: knob 3 results, it's unclear whether he's referring to knob setting with value 3 (which means both knob 0 and 1 are enabled), or to knob(3) specifically.
Whenever you're instructing tests or reporting results, be precise about whether one you're talking to avoid confusion:
### Setting based terminology
ALWAYS use the word in PLURAL (knobs), without mentioning which one, to refer to the setting, aka multiple knobs at once:
Examples:
- **knobs=0**: no knobs enabled
- **knobs=1**: knob0 enabled, others disabled
- **knobs=2**: knob1 enabled, others disabled
- **knobs=3**: knobs 0 and 1 enabled, others disabled
...
### Knob based terminology
Use the word in SINGULAR (knob), or in plural but referring which ones, when meaning multiple knobs at once:
Examples:
- **knob0**: knob 0 enabled, others disabled
- **knob1**: knob 1 enabled, others disabled
- **knobs 0 and 1**: knobs 0 and 1 enabled, others disabled
...
## Examples
### Example 1: Conditional Debug Logging
+4 -1
View File
@@ -6,4 +6,7 @@ Debugging on physical hardware can get tedious and time consuming. Users are emp
**Standard key prefix**: Allows to redirect the key manager to a file other than `prod.keys` (for example `other` would redirect to `other.keys`). This is useful for testing multiple keysets. Default is `prod`.
**Changing serial**: Very basic way to set debug values for the serial (and battery number). Developers do not need to write the full serial as only the first digits (excluding the last) will be accoutned for. Region settings will affect the generated serial. The serial corresponds to a non-OLED/Lite console.
**Changing serial**: Very basic way to set debug values for the serial (and battery number). Developers do not need to write the full serial as it will be writen in-place (that is, it will be filled with the default serial and then overwrite the serial from the beginning).
- Battery serial: `YUZU0EMULATOR14022024`
- Board serial: `YUZ10000000001`
If the user were to set their board serial as `ABC`, then it will be written in-place and the resulting serial would be `ABC10000000001`. There are no underlying checks to ensure correctness of serials other than a hard limit of 16-characters for both.
+14 -3
View File
@@ -27,7 +27,11 @@ set_directory_properties(PROPERTIES EXCLUDE_FROM_ALL ON)
# Xbyak
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
AddJsonPackage(xbyak)
if (PLATFORM_SUN OR PLATFORM_OPENBSD OR PLATFORM_NETBSD OR PLATFORM_DRAGONFLY)
AddJsonPackage(xbyak_sun)
else()
AddJsonPackage(xbyak)
endif()
endif()
# enet
@@ -41,6 +45,9 @@ if (NOT TARGET enet::enet)
add_library(enet::enet ALIAS enet)
endif()
# mbedtls
AddJsonPackage(mbedtls)
# stb
add_library(stb stb/stb_dxt.cpp)
target_include_directories(stb PUBLIC ./stb)
@@ -231,7 +238,9 @@ if (VulkanMemoryAllocator_ADDED)
endif()
# httplib
AddJsonPackage(httplib)
if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER OR USE_DISCORD_PRESENCE OR ENABLE_OPENSSL)
AddJsonPackage(httplib)
endif()
# cpp-jwt
if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
@@ -402,10 +411,12 @@ if (APPLE)
# moltenvk
if (NOT YUZU_USE_BUNDLED_MOLTENVK)
find_library(MOLTENVK_LIBRARY MoltenVK)
else()
unset(MOLTENVK_LIBRARY)
endif()
# TODO: kosmickrisp?
if (NOT MOLTENVK_LIBRARY OR YUZU_USE_BUNDLED_MOLTENVK)
if (NOT MOLTENVK_LIBRARY)
AddJsonPackage(moltenvk)
set(MOLTENVK_LIBRARY "${moltenvk_SOURCE_DIR}/MoltenVK/dylib/macOS/libMoltenVK.dylib" CACHE STRING "" FORCE)
+33 -9
View File
@@ -33,9 +33,6 @@
"find_args": "MODULE GLOBAL",
"patches": [
"0001-mingw.patch"
],
"options": [
"HTTPLIB_REQUIRE_OPENSSL ON"
]
},
"cpp-jwt": {
@@ -51,12 +48,23 @@
"0001-fix-missing-decl.patch"
]
},
"xbyak_sun": {
"package": "xbyak",
"repo": "herumi/xbyak",
"tag": "v%VERSION%",
"hash": "b40dade90fb0e46a2bd52934f7ce461e37be931b571e58cbe2203bc08ed5b54c7ff1a29026c74c7f9805e4e3f6c9636deca528e6b4a8093ce7eae145218599f1",
"git_version": "7.29",
"bundled": true,
"skip_updates": true
},
"xbyak": {
"package": "xbyak",
"repo": "herumi/xbyak",
"tag": "v%VERSION%",
"hash": "ac333d7bea1d61865bebebb116201a58db431946aa2f11aa042ef5795c390ff30af4d6c90ed3b3d24443a1d430703b08f14fc13b2fa405c155a241456ed78a47",
"git_version": "7.33.2"
"hash": "1042090405c426e339506c179d53e91d4d545ce9c9f53d8f797caa092d589f913a9bcb9c8f31c4c60870acb954c556e305fb6732c66bc3c8f1cd924f9172def9",
"git_version": "7.22",
"bundled": true,
"skip_updates": true
},
"oaknut": {
"repo": "eden-emulator/oaknut",
@@ -91,6 +99,20 @@
"0001-avoid-memset-when-clearing-an-empty-table.patch"
]
},
"mbedtls": {
"package": "MbedTLS",
"repo": "Mbed-TLS/mbedtls",
"tag": "mbedtls-%VERSION%",
"hash": "6671fb8fcaa832e5b115dfdce8f78baa6a4aea71f5c89a640583634cdee27aefe3bf4be075744da91f7c3ae5ea4e0c765c8fc3937b5cfd9ea73d87ef496524da",
"version": "3",
"git_version": "3.6.4",
"artifact": "%TAG%.tar.bz2",
"skip_updates": true,
"patches": [
"0001-aesni-fix.patch",
"0002-arm64-aes-fix.patch"
]
},
"enet": {
"repo": "lsalzman/enet",
"tag": "v%VERSION%",
@@ -139,7 +161,7 @@
"package": "SDL2",
"name": "SDL2",
"repo": "crueter-ci/SDL2",
"version": "2.32.10-cf5dabd6ea",
"version": "2.32.10-a65111bd2d",
"min_version": "2.26.4"
},
"catch2": {
@@ -173,6 +195,7 @@
"repo": "libsdl-org/SDL",
"tag": "release-%VERSION%",
"hash": "d5622d6bb7266f7942a7b8ad43e8a22524893bf0c2ea1af91204838d9b78d32768843f6faa248757427b8404b8c6443776d4afa6b672cd8571a4e0c03a829383",
"key": "generic",
"bundled": true,
"git_version": "2.32.10",
"skip_updates": true
@@ -182,6 +205,7 @@
"repo": "libsdl-org/SDL",
"sha": "cc016b0046",
"hash": "b8d9873446cdb922387471df9968e078714683046674ef0d0edddf8e25da65a539a3bae83d635496b970237f90b07b36a69f8d7855d450de59311d6d6e8c3dbc",
"key": "steamdeck",
"bundled": true,
"skip_updates": "true"
},
@@ -231,8 +255,8 @@
},
"ffmpeg": {
"repo": "FFmpeg/FFmpeg",
"sha": "c7b5f1537d",
"hash": "ed177621176b3961bdcaa339187d3a7688c1c8b060b79c4bb0257cbc67ad7021ae5d5adca5303b45625abbbe3d9aafdd87ce777b8690ac295290d744c875489a",
"sha": "5e56937b74",
"hash": "9ab0457dcd6ce6359b5053c1662f57910d332f68ca0cca9d4134d858464840917027374de3d97e0863c3a7daaea2fe4f4cd17d1c6d8e7f740f4ad91e71c2932b",
"bundled": true
},
"ffmpeg-ci": {
@@ -240,7 +264,7 @@
"package": "FFmpeg",
"name": "ffmpeg",
"repo": "crueter-ci/FFmpeg",
"version": "8.0.1-c7b5f1537d",
"version": "8.0.1-5e56937b74",
"min_version": "4.1"
},
"tzdb": {
+3 -6
View File
@@ -94,10 +94,6 @@ if (MSVC AND NOT CXX_CLANG)
/wd4324 # 'struct_name': structure was padded due to __declspec(align())
/wd4201 # nonstandard extension used : nameless struct/union
/wd4702 # unreachable code (when used with LTO)
$<$<CONFIG:Release>:/GS-> # No stack buffer overflow checks
/Gy # Enable function level linking
/GR- # Disable run time type information
)
if (NOT CXX_CLANG)
@@ -112,14 +108,15 @@ if (MSVC AND NOT CXX_CLANG)
add_compile_options(/QIntel-jcc-erratum)
endif()
# /GS- - No stack buffer overflow checks
add_compile_options("$<$<CONFIG:Release>:/GS->")
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
else()
if (NOT MSVC)
add_compile_options(
-fwrapv
-fno-rtti # Disable RTTI
-pipe
)
endif()
+45 -26
View File
@@ -5,8 +5,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// import android.annotation.SuppressLint
import com.android.build.gradle.api.ApplicationVariant
import kotlin.collections.setOf
import org.jlleitschuh.gradle.ktlint.reporter.ReporterType
import com.github.triplet.gradle.androidpublisher.ReleaseStatus
import org.gradle.api.tasks.Copy
@@ -15,7 +13,8 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.9.20"
id("org.jetbrains.kotlin.plugin.compose")
kotlin("plugin.serialization") version "2.3.0"
id("androidx.navigation.safeargs.kotlin")
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
id("com.github.triplet.play") version "3.8.6"
@@ -43,6 +42,7 @@ android {
buildFeatures {
viewBinding = true
compose = true
}
compileOptions {
@@ -50,8 +50,10 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
packaging {
@@ -65,7 +67,7 @@ android {
defaultConfig {
applicationId = "dev.eden.eden_emulator"
minSdk = 24
minSdk = 26
targetSdk = 36
versionName = getGitVersion()
versionCode = autoVersion
@@ -267,6 +269,10 @@ android {
}
}
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
}
idea {
module {
// Inclusion to exclude build/ dir from non-Android
@@ -292,7 +298,7 @@ tasks.getByPath("ktlintMainSourceSetCheck").doFirst { showFormatHelp.invoke() }
tasks.getByPath("loadKtlintReporters").dependsOn("ktlintReset")
ktlint {
version.set("0.47.1")
version.set("0.50.0")
android.set(true)
ignoreFailures.set(false)
disabledRules.set(
@@ -317,29 +323,42 @@ play {
}
dependencies {
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.compose:compose-bom:2026.01.01")
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.4.0")
implementation("androidx.activity:activity-compose:1.12.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.10.0")
implementation("androidx.navigation:navigation-compose:2.9.7")
implementation("io.coil-kt:coil-compose:2.7.0")
implementation("io.coil-kt:coil-svg:2.7.0")
debugImplementation("androidx.compose.ui:ui-tooling")
implementation("androidx.core:core-ktx:1.17.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.fragment:fragment-ktx:1.8.6")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.fragment:fragment-ktx:1.8.9")
implementation("androidx.documentfile:documentfile:1.1.0")
implementation("com.google.android.material:material:1.13.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
implementation("com.squareup.okhttp3:okhttp:4.12.0")
implementation("io.coil-kt:coil:2.2.2")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
implementation("androidx.window:window:1.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("org.commonmark:commonmark:0.22.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.8.9")
implementation("androidx.navigation:navigation-ui-ktx:2.8.9")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0")
implementation("com.squareup.okhttp3:okhttp:5.3.2")
implementation("io.coil-kt:coil:2.7.0")
implementation("androidx.core:core-splashscreen:1.2.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.21.0")
implementation("androidx.window:window:1.5.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0")
implementation("org.commonmark:commonmark:0.27.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.9.7")
implementation("androidx.navigation:navigation-ui-ktx:2.9.7")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
implementation("androidx.compose.ui:ui-graphics-android:1.7.8")
implementation("androidx.compose.ui:ui-text-android:1.7.8")
implementation("net.swiftzer.semver:semver:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
implementation("androidx.compose.ui:ui-graphics-android:1.10.2")
implementation("androidx.compose.ui:ui-text-android:1.10.2")
implementation("net.swiftzer.semver:semver:2.1.0")
}
fun runGitCommand(command: List<String>): String {
+230
View File
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
fill="none"
viewBox="0 0 512 512"
version="1.1"
id="svg7"
sodipodi:docname="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:version="1.4.2 (ebf0e94, 2025-05-08)"
inkscape:export-filename="base.svg.2026_01_12_14_43_47.0.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs7">
<linearGradient
id="linearGradient1"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:0.5;"
offset="0"
id="stop3" />
<stop
style="stop-color:#bf42f6;stop-opacity:0.5;"
offset="0.44631511"
id="stop4" />
<stop
style="stop-color:#5da5ed;stop-opacity:0.5;"
offset="0.90088946"
id="stop2" />
</linearGradient>
<linearGradient
id="linearGradient138"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:1;"
offset="0"
id="stop152" />
<stop
style="stop-color:#bf42f6;stop-opacity:1;"
offset="0.44971901"
id="stop137" />
<stop
style="stop-color:#5da5ed;stop-opacity:1;"
offset="0.89793283"
id="stop138" />
</linearGradient>
<linearGradient
id="swatch37"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop37" />
</linearGradient>
<linearGradient
id="swatch28"
inkscape:swatch="solid">
<stop
style="stop-color:#252525;stop-opacity:1;"
offset="0"
id="stop28" />
</linearGradient>
<linearGradient
id="swatch27"
inkscape:swatch="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop27" />
</linearGradient>
<linearGradient
id="swatch15"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop16" />
</linearGradient>
<linearGradient
id="linearGradient14"
inkscape:swatch="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop14" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop15" />
</linearGradient>
<linearGradient
id="swatch9"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop10" />
</linearGradient>
<linearGradient
id="swatch8"
inkscape:swatch="solid">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop9" />
</linearGradient>
<rect
x="22.627417"
y="402.76802"
width="521.34025"
height="248.94868"
id="rect24" />
<linearGradient
id="linearGradient11"
inkscape:collect="always">
<stop
style="stop-color:#ff2e88;stop-opacity:1;"
offset="0"
id="stop11" />
<stop
style="stop-color:#bf42f6;stop-opacity:1;"
offset="0.44971901"
id="stop154" />
<stop
style="stop-color:#5da5ed;stop-opacity:1;"
offset="0.89793283"
id="stop12" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient138"
id="linearGradient6"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.118028,0,0,1.116699,-46.314723,-42.388667)"
x1="270.39996"
y1="40.000019"
x2="270.39996"
y2="494.39996"
spreadMethod="pad" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<circle
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
id="circle18"
cx="-246.8315"
cy="246.8338"
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath22">
<circle
style="opacity:1;mix-blend-mode:normal;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:10.8382;stroke-opacity:0.566238;paint-order:stroke fill markers"
id="circle22"
cx="256"
cy="256"
inkscape:label="Circle"
r="191.89999" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient11"
id="linearGradient27"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-6.9401139e-5,-2.8678628)"
x1="256.00012"
y1="102.94693"
x2="256.00012"
y2="409.05307" />
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath128">
<circle
style="fill:none;fill-opacity:1;stroke:#03ffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
id="circle128"
cx="256"
cy="256"
r="192" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1"
id="linearGradient2"
x1="256"
y1="64"
x2="256"
y2="448"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3229974,0,0,1.3214002,-82.687336,-82.290326)" />
</defs>
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.4142136"
inkscape:cx="261.62951"
inkscape:cy="230.87036"
inkscape:window-width="1920"
inkscape:window-height="1008"
inkscape:window-x="1080"
inkscape:window-y="351"
inkscape:window-maximized="1"
inkscape:current-layer="svg7" />
<path
id="path8-7"
style="display:inline;mix-blend-mode:multiply;fill:url(#linearGradient6);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2);stroke-width:3.9666;stroke-dasharray:none;stroke-opacity:0.566238;paint-order:stroke fill markers"
inkscape:label="Circle"
d="M 256,2.2792898 A 254.0155,253.71401 0 0 0 150.68475,25.115202 c 19.54414,1.070775 38.74692,5.250294 51.56848,11.647658 14.14361,7.056691 28.63804,19.185961 39.4212,29.347551 h 40.60981 c 1.03847,-0.68139 2.10297,-1.36938 3.1938,-2.05957 5.45602,-15.78533 14.79164,-43.183497 19.49612,-57.0097682 A 254.0155,253.71401 0 0 0 256,2.2792898 Z m 61.57106,7.567234 -18.26098,46.1544672 c 7.79702,-4.13918 16.35655,-7.87447 25.20671,-10.87081 23.1229,-7.828433 43.96931,-10.170904 54.94058,-10.868226 A 254.0155,253.71401 0 0 0 317.57106,9.8465238 Z m 65.39277,26.4001532 c -9.68256,4.806644 -33.05532,16.642034 -55.68217,29.863734 H 424.4677 A 254.0155,253.71401 0 0 0 382.96383,36.246677 Z M 113.90698,45.690231 A 254.0155,253.71401 0 0 0 87.532302,66.110411 H 194.2739 c -1.47402,-0.80231 -2.35141,-1.25949 -2.35141,-1.25949 l 10.4496,-11.83348 -38.40568,7.01234 c 0,1e-5 -12.21537,-4.60266 -40.17313,-12.27223 -3.45336,-0.94731 -6.75329,-1.61824 -9.8863,-2.06732 z m -36.803618,30.18635 a 254.0155,253.71401 0 0 0 -34.88372,43.090929 h 59.976738 c 18.11461,-12.04145 40.14252,-22.882149 62.31266,-24.534159 52.93006,-3.9444 70.16538,1.86342 70.16538,1.86342 0,0 -4.612,-4.8206 -14.51938,-13.36656 -2.72366,-2.34942 -6.0844,-4.77373 -9.52455,-7.05363 z m 174.472868,0 c 4.57322,4.7186 7.29716,7.83565 7.29716,7.83565 0,0 3.53501,-3.18484 9.62532,-7.83565 z m 60.27649,0 c -21.56573,15.45339 -25.4703,27.979669 -25.4703,27.979669 0,0 54.83326,-19.215729 100.70543,-0.31228 11.63986,4.79661 21.58481,10.13159 29.94832,15.42354 h 52.74419 A 254.0155,253.71401 0 0 0 434.89664,75.876581 Z M 36.250648,128.73367 A 254.0155,253.71401 0 0 0 16.372095,171.82459 H 147.45478 c 1.45695,-2.5815 3.06539,-5.08648 4.83979,-7.48982 14.23694,-19.28301 27.92088,-30.0088 36.86047,-35.6011 h -30.25323 c -5.87346,0.93472 -12.04945,1.99094 -18.28166,3.16937 -30.12936,5.69727 -81.157618,22.78945 -81.157618,22.78945 0,0 11.47125,-12.39249 29.11369,-25.95882 z m 265.630492,0 c 33.48676,11.2434 52.42799,26.78443 62.7752,43.09092 h 130.97157 a 254.0155,253.71401 0 0 0 -19.87856,-43.09092 h -44.81136 c 14.85233,11.5863 21.59948,20.9854 21.59948,20.9854 0,0 -33.5226,-12.37087 -66.0646,-20.9854 z m -45.96641,16.27007 c -1.00419,0.0106 -10.12705,0.72026 -44.98966,20.64729 -3.12132,1.78406 -6.25434,3.86182 -9.37468,6.17356 h 41.81911 c 7.17181,-17.34774 12.64083,-26.82085 12.64083,-26.82085 0,0 -0.0287,-7.1e-4 -0.0957,0 z m 14.18088,0.0465 c 0,0 -3.31228,9.32762 -7.30492,26.77438 h 51.78554 C 287.6577,146.14158 270.09561,145.0502 270.09561,145.0502 Z M 13.152456,181.59075 A 254.0155,253.71401 0 0 0 3.927651,224.68167 H 134.1447 c 0.56161,-12.72411 2.67825,-28.50188 8.61499,-43.09092 z m 176.661504,0 c -14.27121,13.10564 -27.60733,29.58761 -37.56073,43.09092 h 73.3721 c 4.47018,-16.79061 9.35068,-31.26371 13.86562,-43.09092 z m 70.85787,0 c -2.41384,11.76417 -4.9032,26.20707 -6.94831,43.09092 H 360.4832 c -8.32133,-10.88917 -20.66988,-26.17008 -36.35141,-43.09092 z m 109.17313,0 c 6.63611,15.24089 6.92441,30.5373 5.57882,43.09092 h 132.64857 a 254.0155,253.71401 0 0 0 -9.22481,-43.09092 z M 2.90181,234.44783 A 254.0155,253.71401 0 0 0 1.984498,255.9933 254.0155,253.71401 0 0 0 2.90181,277.53876 h 211.89923 c 2.25762,-15.52555 5.14325,-29.93448 8.3385,-43.09093 h -77.8863 c -6.46396,9.27617 -10.33076,15.56549 -10.33076,15.56549 0,0 -0.82623,-6.14945 -0.9354,-15.56549 z m 249.72093,0 c -1.3692,13.09684 -2.4456,27.49209 -3.02068,43.09093 h 259.49613 a 254.0155,253.71401 0 0 0 0.91731,-21.54546 254.0155,253.71401 0 0 0 -0.91731,-21.54547 H 374.02584 c -0.445,2.5469 -0.90878,4.89768 -1.32817,7.01751 0,0 -1.69726,-2.53821 -4.94056,-7.01751 z M 3.927651,287.30493 a 254.0155,253.71401 0 0 0 9.224805,43.09091 H 214.04393 c -1.29238,-15.40742 -1.57503,-30.04388 -0.41861,-43.09091 z m 245.385009,0 c -0.30355,13.54349 -0.22032,27.92598 0.36951,43.09091 h 249.16537 a 254.0155,253.71401 0 0 0 9.22481,-43.09091 z M 16.369511,340.16201 a 254.0155,253.71401 0 0 0 19.878554,43.09091 H 221.4677 c -2.69781,-14.4523 -4.96108,-29.01285 -6.4832,-43.09091 z m 233.842379,0 c 1.15864,15.47765 3.81286,29.83979 7.51679,43.09091 h 218.02325 a 254.0155,253.71401 0 0 0 19.87856,-43.09091 z M 42.217052,393.01909 a 254.0155,253.71401 0 0 0 34.88372,43.09093 H 233.09561 c -3.40902,-13.67281 -6.76794,-28.2531 -9.73902,-43.09093 z m 218.490958,0 c 5.34985,16.15926 12.22007,30.51982 19.68733,43.09093 h 154.50389 a 254.0155,253.71401 0 0 0 34.88371,-43.09093 z M 87.529722,445.87618 a 254.0155,253.71401 0 0 0 166.229968,63.8208 c -3.67805,-12.0825 -10.85464,-35.49828 -18.18088,-63.8208 z m 199.010328,0 c 17.5887,26.43772 36.99259,43.60598 47.33592,51.61309 a 254.0155,253.71401 0 0 0 90.59431,-51.61309 z" />
<path
id="path27"
style="display:inline;mix-blend-mode:multiply;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient27);stroke-width:3;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
d="m 318.98012,441.7375 c -9.87518,-6.73978 -64.39137,-49.0272 -67.68975,-127.81978 -3.69298,-88.21893 15.36468,-141.91029 15.36468,-141.91029 0,0 16.00378,0.99513 39.80316,26.53195 23.79939,25.53753 37.74965,46.43102 37.74965,46.43102 3.91262,-19.79992 12.84563,-66.32402 -60.72865,-87.55523 0,0 12.82326,-5.38883 39.3925,-3.81382 26.56907,1.57572 81.6822,21.93799 81.6822,21.93799 0,0 -14.79766,-20.63773 -49.47063,-34.94295 -34.67291,-14.30533 -76.1182,0.23644 -76.1182,0.23644 0,0 3.86959,-12.43127 27.22669,-26.38478 23.35718,-13.9537 49.27409,-26.501533 49.27409,-26.501533 0,0 -21.97854,-0.26548 -47.67725,8.44535 -6.68948,2.267506 -13.15863,5.094213 -19.05208,8.226563 l 16.05803,-40.634103 -4.4617,-1.89059 -5.1305,-0.95965 c 0,0 -11.24072,33.12428 -16.92051,49.576513 -12.13137,7.68489 -20.11005,14.87735 -20.11005,14.87735 0,0 -21.90573,-25.09227 -42.79668,-35.527803 -26.03412,-13.00525 -86.88249,-13.90359 -94.0044,10.401173 0,0 13.56804,-7.884703 34.70032,-2.080917 21.13214,5.803997 30.3644,9.287307 30.3644,9.287307 l 29.02989,-5.30681 -7.89811,8.95527 c 0,0 13.8496,7.21324 21.33822,13.68063 7.48859,6.46722 10.9757,10.11472 10.9757,10.11472 0,0 -13.02739,-4.39388 -53.03507,-1.40893 -40.00771,2.98473 -79.40016,45.60209 -79.40016,45.60209 0,0 38.57037,-12.93531 61.34393,-17.24677 22.77354,-4.31126 44.52166,-6.46757 44.52166,-6.46757 0,0 -17.23298,5.97003 -35.69792,31.00932 -18.46522,25.03987 -13.13146,64.83866 -13.13146,64.83866 0,0 29.33874,-47.7577 57.44675,-63.84249 28.10798,-16.08527 34.0799,-15.6238 34.0799,-15.6238 0,0 -22.56785,39.13486 -31.39017,101.98268 -8.03005,57.2039 26.77689,163.75449 31.1572,178.89699"
sodipodi:nodetypes="cscsccscscscsccccccscscccscscscscscsc"
inkscape:label="MainOutline"
clip-path="url(#clipPath128)"
transform="matrix(1.3229974,0,0,1.3214002,-82.687282,-82.278451)" />
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,184 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.background
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.decode.SvgDecoder
import coil.request.ImageRequest
import dev.eden.emu.ui.theme.EdenTheme
import kotlin.math.pow
@Composable
fun RetroGridBackground(
modifier: Modifier = Modifier,
lineColor: Color = EdenTheme.colors.primary.copy(alpha = 0.2f),
spacing: Dp = 40.dp,
lineWidth: Dp = 2.dp,
speedDpPerSecond: Float = 12f,
fadeColor: Color = EdenTheme.colors.background,
logoScale: Float = 0.35f,
logoAlpha: Float = 0.25f,
horizonRatio: Float = 0.45f,
logoAssetPath: String = "file:///android_asset/base.svg", // correct way?
logoWidth: Dp? = null,
logoHeight: Dp? = null,
logoCropBottomRatio: Float = 0.33f,
) {
val context = LocalContext.current
val density = LocalDensity.current
val durationMs = remember(spacing, speedDpPerSecond, density) {
val spacingPx = spacing.value * density.density
val speedPxPerSecond = speedDpPerSecond * density.density
((spacingPx / speedPxPerSecond) * 1000f).toInt().coerceAtLeast(300)
}
val transition = rememberInfiniteTransition(label = "grid")
val phase by transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = durationMs, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "gridPhase",
)
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val baseSize = maxWidth * logoScale
val resolvedWidth = logoWidth ?: baseSize
val resolvedHeight = logoHeight ?: baseSize
val cropRatio = logoCropBottomRatio.coerceIn(0f, 0.9f)
val visibleHeight = resolvedHeight * (1f - cropRatio)
val visibleHeightPx = with(density) { visibleHeight.toPx() }
val horizonY = maxHeight * horizonRatio
val logoOffsetY = horizonY - (resolvedHeight * 0.5f) - 50.dp
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier = Modifier
.fillMaxSize()
.drawWithCache {
val spacingPx = spacing.toPx().coerceAtLeast(8f)
val strokePx = lineWidth.toPx().coerceAtLeast(1f)
val horizonScale = 0.18f
val overscan = 5.0f
val perspectivePower = 2.2f
onDrawBehind {
val width = size.width
val height = size.height
val centerX = width * 0.5f
val horizonYpx = height * horizonRatio
val depth = (height - horizonYpx).coerceAtLeast(1f)
val offset = (1f - phase) * spacingPx
val baseHalf = centerX * overscan
val horizonHalf = baseHalf * horizonScale
val lineCount = (depth / (spacingPx * 0.65f))
.toInt()
.coerceAtLeast(20)
var x = centerX - baseHalf - spacingPx * 6f
while (x <= centerX + baseHalf + spacingPx * 6f) {
val endX = centerX + (x - centerX) * horizonScale
drawLine(
color = lineColor,
start = Offset(x, height),
end = Offset(endX, horizonYpx),
strokeWidth = strokePx,
)
x += spacingPx
}
var i = 0
while (i <= lineCount) {
val z = ((i + (offset / spacingPx)) / lineCount.toFloat())
.coerceIn(0f, 1f)
val zCurve = z.toDouble().pow(perspectivePower.toDouble()).toFloat()
val y = horizonYpx + depth * zCurve
val halfWidth = horizonHalf + (baseHalf - horizonHalf) * z
drawLine(
color = lineColor,
start = Offset(centerX - halfWidth, y),
end = Offset(centerX + halfWidth, y),
strokeWidth = strokePx,
)
i += 1
}
val fadeHeight = height * 0.12f
val solidHeight = (horizonYpx - fadeHeight).coerceAtLeast(0f)
if (solidHeight > 0f) {
drawRect(
color = fadeColor,
topLeft = Offset(0f, 0f),
size = Size(width, solidHeight),
)
}
drawRect(
brush = Brush.verticalGradient(
colors = listOf(fadeColor, Color.Transparent),
startY = horizonYpx - fadeHeight,
endY = horizonYpx + fadeHeight,
),
topLeft = Offset(0f, horizonYpx - fadeHeight),
size = Size(width, fadeHeight * 2f),
)
}
},
)
AsyncImage(
model = ImageRequest.Builder(context)
.data(logoAssetPath)
.decoderFactory(SvgDecoder.Factory())
.build(),
contentDescription = null,
modifier = Modifier
.align(Alignment.TopCenter)
.requiredWidth(resolvedWidth)
.requiredHeight(resolvedHeight)
.offset(y = logoOffsetY)
.drawWithContent {
clipRect(
left = 0f,
top = 0f,
right = size.width,
bottom = visibleHeightPx,
) {
this@drawWithContent.drawContent()
}
}
.graphicsLayer(alpha = logoAlpha),
)
}
}
}
@@ -0,0 +1,157 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.GameIconUtils
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.ConfirmKeys
import dev.eden.emu.ui.utils.MenuKeys
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun EdenTile(
title: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onLongClick: (() -> Unit)? = null,
imageUri: String? = null,
iconSize: Dp = 140.dp,
tileHeight: Dp = 168.dp,
useLargerFont: Boolean = false,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused = interactionSource.collectIsFocusedAsState().value
val borderColor = if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface
Column(
modifier = modifier
.width(iconSize)
.height(tileHeight)
.semantics { role = Role.Button }
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyUp) {
when {
e.key in ConfirmKeys -> { onClick(); true }
e.key in MenuKeys -> { onLongClick?.invoke(); true }
else -> false
}
} else false
}
.focusable(interactionSource = interactionSource)
.combinedClickable(
interactionSource = interactionSource,
indication = null,
onClick = onClick,
onLongClick = onLongClick,
),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// Game icon
Box(
modifier = Modifier
.size(iconSize)
.background(EdenTheme.colors.surface, Shapes.medium)
.border(BorderStroke(3.dp, borderColor), Shapes.medium)
.clip(Shapes.medium),
contentAlignment = Alignment.Center,
) {
if (imageUri.isNullOrBlank()) {
BasicText(
text = title.take(1).uppercase(Locale.getDefault()),
style = EdenTheme.typography.title.copy(color = EdenTheme.colors.onSurface),
)
} else {
AndroidView(
factory = { ctx ->
android.widget.ImageView(ctx).apply {
layoutParams = android.view.ViewGroup.LayoutParams(-1, -1)
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
setImageResource(R.drawable.default_icon)
GameIconUtils.loadGameIcon(Game(title, imageUri, "0", "", "", false), this)
}
},
modifier = Modifier.fillMaxSize()
)
}
}
// Game title with marquee
MarqueeText(title, isFocused, 32.dp, useLargerFont, Modifier.width(iconSize))
}
}
@Composable
private fun MarqueeText(
text: String,
isAnimating: Boolean,
height: Dp,
useLargerFont: Boolean,
modifier: Modifier = Modifier,
) {
val style = if (useLargerFont) {
EdenTheme.typography.body.copy(Color.White, 18.sp, fontWeight = FontWeight.Bold)
} else {
EdenTheme.typography.label.copy(Color.White)
}
Box(modifier.height(height).clipToBounds(), Alignment.Center) {
BasicText(
text = text,
style = style,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
modifier = if (isAnimating) {
Modifier.basicMarquee(
iterations = Int.MAX_VALUE,
animationMode = MarqueeAnimationMode.Immediately,
initialDelayMillis = 150, // does not seem to take effect
repeatDelayMillis = 1000,
velocity = 30.dp,
)
} else Modifier,
)
}
}
@@ -0,0 +1,259 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.utils.ConfirmKeys
/**
* Expandable search bar for controller-based navigation.
* ________________________________________________________________
* Note: Focus Manager on this doesn't work good. Need a hide impl.
* when it looses focus for better Controller Experience
* ________________________________________________________________
*/
@Composable
fun ExpandableSearchBar(
query: String,
onQueryChange: (String) -> Unit,
isExpanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val searchHint = context.getString(R.string.home_search_games)
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val iconInteractionSource = remember { MutableInteractionSource() }
val isIconFocused by iconInteractionSource.collectIsFocusedAsState()
// Track if we have an active filter (submitted search, if keyboard just hides, it kinda breaks)
val hasActiveFilter = query.isNotEmpty() && !isExpanded
val textFieldFocusRequester = remember { FocusRequester() }
// Focus text field when expanded
LaunchedEffect(isExpanded) {
if (isExpanded) {
delay(100)
try {
textFieldFocusRequester.requestFocus()
keyboardController?.show()
} catch (_: Exception) {}
}
}
// Use a fixed width container to prevent layout shift.
Box(
modifier = modifier.width(if (isExpanded) 300.dp else 48.dp),
contentAlignment = Alignment.CenterEnd,
) {
if (isExpanded) {
Row(
modifier = Modifier
.width(300.dp)
.clip(RoundedCornerShape(24.dp))
.background(EdenTheme.colors.surface)
.border(
width = 2.dp,
color = EdenTheme.colors.primary,
shape = RoundedCornerShape(24.dp)
),
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = Modifier.width(16.dp))
// Text input - NO onFocusChanged to prevent auto-close issues
// (Needs to be "submitted", hiding keyboard breaks focus flow)
BasicTextField(
value = query,
onValueChange = onQueryChange,
modifier = Modifier
.weight(1f)
.focusRequester(textFieldFocusRequester)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyUp) {
when (keyEvent.key) {
Key.Escape, Key.Back -> {
onQueryChange("")
onExpandedChange(false)
keyboardController?.hide()
focusManager.clearFocus()
true
}
Key.Enter, Key.NumPadEnter -> {
keyboardController?.hide()
onExpandedChange(false)
focusManager.clearFocus()
true
}
else -> false
}
} else false
},
textStyle = TextStyle(
color = Color.White,
fontSize = 16.sp,
),
cursorBrush = SolidColor(EdenTheme.colors.primary),
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
keyboardController?.hide()
onExpandedChange(false)
focusManager.clearFocus()
}
),
decorationBox = { innerTextField ->
Box(
modifier = Modifier.padding(vertical = 14.dp),
contentAlignment = Alignment.CenterStart,
) {
if (query.isEmpty()) {
androidx.compose.foundation.text.BasicText(
text = searchHint,
style = TextStyle(
color = Color.White.copy(alpha = 0.5f),
fontSize = 16.sp,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
innerTextField()
}
}
)
// Clear/Close button (X icon)
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.clickable {
onQueryChange("")
onExpandedChange(false)
keyboardController?.hide()
focusManager.clearFocus()
}
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_clear),
contentDescription = context.getString(R.string.home_clear),
tint = Color.White.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp),
)
}
Spacer(modifier = Modifier.width(4.dp))
}
} else {
// Collapsed state: search icon
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(
if (isIconFocused) EdenTheme.colors.primary
else EdenTheme.colors.surface
)
.focusRequester(focusRequester)
.focusable(interactionSource = iconInteractionSource)
.clickable(
interactionSource = iconInteractionSource,
indication = null,
) {
if (hasActiveFilter) {
onQueryChange("")
} else {
onExpandedChange(true)
}
}
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key in ConfirmKeys) {
if (hasActiveFilter) {
onQueryChange("")
} else {
onExpandedChange(true)
}
true
} else false
},
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_search),
contentDescription = context.getString(R.string.home_search),
tint = if (hasActiveFilter) EdenTheme.colors.primary else Color.White,
modifier = Modifier.size(24.dp),
)
// Active filter indicator dot
if (hasActiveFilter) {
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(6.dp)
.size(10.dp)
.clip(CircleShape)
.background(EdenTheme.colors.primary)
)
}
}
}
}
}
@@ -0,0 +1,319 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import kotlin.math.abs
import kotlin.math.cos
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun GameCarousel(
gameTiles: List<GameTile>,
onGameClick: (GameTile) -> Unit,
onGameLongClick: (GameTile) -> Unit = {},
focusRequester: FocusRequester,
onNavigateUp: () -> Unit = {},
modifier: Modifier = Modifier,
initialFocusedIndex: Int = 0,
onFocusedIndexChanged: (Int) -> Unit = {},
onShowGameInfo: (GameTile) -> Unit = {},
) {
if (gameTiles.isEmpty()) return
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = safeInitialIndex,
initialFirstVisibleItemScrollOffset = 0
)
val snapFlingBehavior = rememberSnapFlingBehavior(
lazyListState = listState,
snapPosition = SnapPosition.Center
)
val coroutineScope = rememberCoroutineScope()
var centerItemIndex by remember { mutableIntStateOf(safeInitialIndex) }
LaunchedEffect(centerItemIndex) {
onFocusedIndexChanged(centerItemIndex)
}
suspend fun centerOnIndex(index: Int, animate: Boolean = true) {
if (gameTiles.isEmpty()) return
if (listState.layoutInfo.visibleItemsInfo.none { it.index == index }) {
if (animate) {
listState.animateScrollToItem(index)
} else {
listState.scrollToItem(index)
}
}
delay(10)
val layoutInfo = listState.layoutInfo
val itemInfo = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return
val viewportCenter = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2
val desiredStart = viewportCenter - itemInfo.size / 2
if (itemInfo.offset != desiredStart) {
val scrollOffset = -(viewportCenter - itemInfo.size / 2)
if (animate) {
listState.animateScrollToItem(index, scrollOffset = scrollOffset)
} else {
listState.scrollToItem(index, scrollOffset = scrollOffset)
}
}
}
LaunchedEffect(Unit) {
listState.scrollToItem(safeInitialIndex)
delay(50)
centerOnIndex(safeInitialIndex, animate = false)
}
val focusRequesters = remember(gameTiles.size) {
List(gameTiles.size) { FocusRequester() }
}
var isFocusTriggeredScroll by remember { mutableStateOf(false) }
LaunchedEffect(listState) {
snapshotFlow { listState.isScrollInProgress }
.collect { isScrolling ->
if (!isScrolling && !isFocusTriggeredScroll && gameTiles.isNotEmpty()) {
val firstVisible = listState.firstVisibleItemIndex
val scrollOffset = listState.firstVisibleItemScrollOffset
val layoutInfo = listState.layoutInfo
val firstVisibleItem = layoutInfo.visibleItemsInfo.firstOrNull()
val itemSize = firstVisibleItem?.size ?: 1
val newCenterIndex = if (scrollOffset > itemSize / 2) {
(firstVisible + 1).coerceAtMost(gameTiles.size - 1)
} else {
firstVisible
}
if (newCenterIndex != centerItemIndex && newCenterIndex in focusRequesters.indices) {
centerItemIndex = newCenterIndex
try {
focusRequesters[newCenterIndex].requestFocus()
} catch (_: Exception) { }
}
}
// Reset the flag when scroll stops
if (!isScrolling) {
isFocusTriggeredScroll = false
}
}
}
// Carousel settings (matching original CarouselRecyclerView)
val borderScale = 0.6f
val borderAlpha = 0.35f
val overlapFactor = 0.15f
BoxWithConstraints(
modifier = modifier
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
val density = LocalDensity.current
val screenWidthPx = with(density) { maxWidth.toPx() }
val screenHeightPx = with(density) { maxHeight.toPx() }
val tileTextHeight = 40.dp
val tileSpacing = 6.dp
// Card size
val totalTextAreaPx = with(density) { (tileTextHeight + tileSpacing).toPx() }
val cardSizePx = (screenHeightPx - totalTextAreaPx) * 0.7f
val cardSize = with(density) { cardSizePx.toDp() }
val tileHeight = cardSize + tileSpacing + tileTextHeight
// Horizontal padding to center first/last card
val horizontalPaddingPx = (screenWidthPx - cardSizePx) / 2f
val horizontalPadding = with(density) { horizontalPaddingPx.toDp() }
// Negative spacing for overlap, no overlap currently tho
val overlapPx = cardSizePx * overlapFactor
val itemSpacing = with(density) { (-overlapPx).toDp() }
val centerPushPx = cardSizePx * 0.12f
val screenCenterPx = screenWidthPx / 2f
val itemWidthWithSpacing = cardSizePx - overlapPx
LazyRow(
state = listState,
modifier = Modifier.fillMaxSize(),
flingBehavior = snapFlingBehavior,
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
contentPadding = PaddingValues(start = horizontalPadding, end = horizontalPadding, top = 30.dp),
verticalAlignment = Alignment.CenterVertically,
) {
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
val distanceFromCenter by remember {
derivedStateOf {
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
if (itemInfo == null) {
1f
} else {
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
val distance = abs(itemCenterInScreen - screenCenterPx)
(distance / screenCenterPx).coerceIn(0f, 1f)
}
}
}
val shapedScale = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
val scale = borderScale + (1f - borderScale) * shapedScale
val shapedAlpha = cos(distanceFromCenter * Math.PI * 0.5).toFloat().coerceIn(0f, 1f)
val alpha = borderAlpha + (1f - borderAlpha) * shapedAlpha
val zIndex = 100f * (1f - distanceFromCenter)
val itemInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == index }
val isLeftOfCenter = if (itemInfo != null) {
val itemLeftInScreen = itemInfo.offset.toFloat() + horizontalPaddingPx
val itemCenterInScreen = itemLeftInScreen + itemInfo.size / 2f
itemCenterInScreen < screenCenterPx
} else {
index < (gameTiles.size / 2)
}
val pushFactor = (1f - distanceFromCenter) * distanceFromCenter * 4f
val horizontalPush = centerPushPx * pushFactor * (if (isLeftOfCenter) -1f else 1f)
val tileModifier = if (index == centerItemIndex) {
Modifier
.focusRequester(focusRequester)
.focusRequester(focusRequesters[index])
} else {
Modifier.focusRequester(focusRequesters[index])
}
Box(
modifier = Modifier
.size(width = cardSize, height = tileHeight)
.zIndex(zIndex)
.onFocusChanged { focusState ->
if (focusState.hasFocus && centerItemIndex != index) {
isFocusTriggeredScroll = true
centerItemIndex = index
coroutineScope.launch {
centerOnIndex(index, animate = true)
}
}
}
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown) {
when (keyEvent.key) {
Key.ButtonX -> {
onShowGameInfo(tile)
true
}
Key.DirectionUp, Key.DirectionUpLeft, Key.DirectionUpRight -> {
onNavigateUp()
true
}
Key.DirectionLeft -> {
if (index > 0) {
focusRequesters[index - 1].requestFocus()
}
true
}
Key.DirectionRight -> {
if (index < gameTiles.size - 1) {
focusRequesters[index + 1].requestFocus()
}
true
}
else -> false
}
} else {
false
}
}
.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
translationX = horizontalPush
},
contentAlignment = Alignment.Center,
) {
EdenTile(
title = tile.title,
iconSize = cardSize,
tileHeight = tileHeight,
onClick = {
if (index == centerItemIndex) {
onGameClick(tile)
} else {
isFocusTriggeredScroll = true
centerItemIndex = index
coroutineScope.launch {
centerOnIndex(index, animate = true)
}
focusRequesters[index].requestFocus()
}
},
onLongClick = {
if (index == centerItemIndex) {
onGameLongClick(tile)
} else {
isFocusTriggeredScroll = true
centerItemIndex = index
coroutineScope.launch {
centerOnIndex(index, animate = true)
onGameLongClick(tile)
}
focusRequesters[index].requestFocus()
}
},
modifier = tileModifier,
imageUri = tile.iconUri,
useLargerFont = true,
)
}
}
}
}
}
@@ -0,0 +1,304 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.EdenTheme
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Composable
fun GameGrid(
gameTiles: List<GameTile>,
onGameClick: (GameTile) -> Unit,
onGameLongClick: (GameTile) -> Unit = {},
onNavigateToSettings: () -> Unit = {},
focusRequester: FocusRequester,
onNavigateUp: () -> Unit = {},
rowCount: Int = 2,
tileIconSize: Dp = 100.dp,
paddingTop: Dp = 0.dp,
modifier: Modifier = Modifier,
isLoading: Boolean = false,
emptyMessage: String? = null,
initialFocusedIndex: Int = 0,
onFocusedIndexChanged: (Int) -> Unit = {},
onShowGameInfo: (GameTile) -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxSize()
) {
when {
isLoading -> LoadingMessage()
emptyMessage != null -> EmptyMessage(emptyMessage)
else -> {
GameGridContent(
gameTiles = gameTiles,
onGameClick = onGameClick,
onGameLongClick = onGameLongClick,
focusRequester = focusRequester,
onNavigateUp = onNavigateUp,
rowCount = rowCount,
tileIconSize = tileIconSize,
paddingTop = paddingTop,
initialFocusedIndex = initialFocusedIndex,
onFocusedIndexChanged = onFocusedIndexChanged,
onXPress = { index ->
gameTiles.getOrNull(index)?.let { tile ->
onShowGameInfo(tile)
}
},
)
}
}
}
}
@Composable
private fun GameGridContent(
gameTiles: List<GameTile>,
onGameClick: (GameTile) -> Unit,
onGameLongClick: (GameTile) -> Unit = {},
focusRequester: FocusRequester,
onNavigateUp: () -> Unit,
rowCount: Int,
tileIconSize: Dp,
paddingTop: Dp,
initialFocusedIndex: Int = 0,
onFocusedIndexChanged: (Int) -> Unit = {},
onXPress: (Int) -> Unit = {},
) {
val safeInitialIndex = initialFocusedIndex.coerceIn(0, (gameTiles.size - 1).coerceAtLeast(0))
var focusedIndex by remember { mutableIntStateOf(safeInitialIndex) }
val gridState = rememberLazyGridState(
initialFirstVisibleItemIndex = safeInitialIndex
)
LaunchedEffect(gameTiles.size) {
if (focusedIndex >= gameTiles.size) {
focusedIndex = 0
gridState.scrollToItem(0)
}
}
val tileFocusRequesters = remember(gameTiles.size) {
List(gameTiles.size) { FocusRequester() }
}
LaunchedEffect(focusRequester) {
if (tileFocusRequesters.isNotEmpty()) {
focusRequester.requestFocus()
}
}
LaunchedEffect(Unit) {
if (tileFocusRequesters.isNotEmpty() && safeInitialIndex in tileFocusRequesters.indices) {
gridState.scrollToItem(safeInitialIndex)
tileFocusRequesters[safeInitialIndex].requestFocus()
}
}
LaunchedEffect(focusedIndex) {
if (focusedIndex in tileFocusRequesters.indices) {
tileFocusRequesters[focusedIndex].requestFocus()
onFocusedIndexChanged(focusedIndex)
}
}
val outerPaddingTop = 62.dp
val outerPaddingBottom = 16.dp
val outerPaddingHorizontal = 0.dp
val gridVerticalSpacing = 0.dp
val gridVerticalPadding = 12.dp
val spacerHeight = 2.dp
val minTextHeight = 42.dp
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(
top = outerPaddingTop,
start = outerPaddingHorizontal,
end = outerPaddingHorizontal,
bottom = outerPaddingBottom
)
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown && gameTiles.isNotEmpty()) {
val currentRow = focusedIndex % rowCount
when (keyEvent.key) {
Key.ButtonX -> {
onXPress(focusedIndex)
true
}
Key.DirectionRight, Key.D -> {
val nextIndex = focusedIndex + rowCount
if (nextIndex < gameTiles.size) {
focusedIndex = nextIndex
}
true
}
Key.DirectionLeft, Key.A -> {
val prevIndex = focusedIndex - rowCount
if (prevIndex >= 0) {
focusedIndex = prevIndex
}
true
}
Key.DirectionDown, Key.S -> {
val nextIndex = focusedIndex + 1
if (nextIndex < gameTiles.size && nextIndex % rowCount != 0) {
focusedIndex = nextIndex
}
true
}
Key.DirectionUp, Key.W -> {
if (currentRow > 0) {
focusedIndex -= 1
true
} else {
onNavigateUp()
true
}
}
else -> false
}
} else {
false
}
},
contentAlignment = Alignment.TopStart
) {
val availableHeight = maxHeight
val heightForRows = (availableHeight - (gridVerticalPadding * 2) -
(gridVerticalSpacing * (rowCount - 1))).coerceAtLeast(0.dp)
val perRowHeight = heightForRows.safeDiv(rowCount)
val iconMaxSize = tileIconSize * 2
val iconSizeForGrid = (perRowHeight - spacerHeight - minTextHeight)
.coerceAtLeast(96.dp)
.coerceAtMost(iconMaxSize)
val tileHeight = perRowHeight.coerceAtLeast(iconSizeForGrid + spacerHeight + minTextHeight)
LazyHorizontalGrid(
rows = GridCells.Fixed(rowCount),
state = gridState,
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = gridVerticalPadding),
) {
itemsIndexed(gameTiles, key = { _, tile -> tile.id }) { index, tile ->
val tileModifier = if (index == focusedIndex) {
Modifier
.focusRequester(focusRequester)
.focusRequester(tileFocusRequesters[index])
} else {
Modifier.focusRequester(tileFocusRequesters[index])
}
EdenTile(
title = tile.title,
iconSize = iconSizeForGrid,
tileHeight = tileHeight,
onClick = {
focusedIndex = index
onGameClick(tile)
},
onLongClick = {
focusedIndex = index
onGameLongClick(tile)
},
modifier = tileModifier,
imageUri = tile.iconUri,
)
}
}
}
}
@Composable
private fun LoadingMessage() {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
CircularProgressIndicator(
modifier = Modifier.padding(bottom = 16.dp),
color = EdenTheme.colors.primary,
)
BasicText(
text = context.getString(R.string.loading),
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
)
}
}
@Composable
private fun EmptyMessage(message: String) {
val context = LocalContext.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
BasicText(
text = message,
style = EdenTheme.typography.body.copy(color = EdenTheme.colors.onBackground),
)
BasicText(
text = context.getString(R.string.manage_game_folders),
style = EdenTheme.typography.label.copy(color = EdenTheme.colors.onBackground),
modifier = Modifier.padding(top = 8.dp)
)
}
}
private fun Dp.safeDiv(divisor: Int): Dp = if (divisor > 0) this / divisor else 0.dp
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import org.yuzu.yuzu_emu.model.Game
data class GameTile(
val id: String,
val title: String,
val uri: String? = null,
val iconUri: String? = null,
val programId: Long = 0L,
val developer: String = "",
val version: String = "",
val isHomebrew: Boolean = false,
) {
companion object {
fun fromGame(game: Game): GameTile = GameTile(
id = game.path,
title = game.title,
uri = game.path,
iconUri = game.path,
programId = game.programId.toLongOrNull() ?: 0L,
developer = game.developer,
version = game.version,
isHomebrew = game.isHomebrew,
)
}
}
@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.eden.emu.ui.theme.Dimens
import org.yuzu.yuzu_emu.R
enum class FooterButton(@DrawableRes val iconRes: Int) {
A(R.drawable.facebutton_a),
B(R.drawable.facebutton_b),
X(R.drawable.facebutton_x),
Y(R.drawable.facebutton_y),
}
@Composable
fun HomeFooter(
actions: List<FooterAction>,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = Dimens.paddingLg, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingXl),
verticalAlignment = Alignment.CenterVertically,
) {
actions.forEach { action ->
Row(
modifier = Modifier.padding(horizontal = Dimens.paddingXs, vertical = 2.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
painter = painterResource(id = action.button.iconRes),
contentDescription = null,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(Color.White.copy(alpha = 0.9f)),
)
BasicText(action.text, style = TextStyle(Color.White.copy(0.7f), 14.sp))
}
}
}
}
data class FooterAction(val button: FooterButton, val text: String)
data class FooterFocusRequesters(val primaryAction: FocusRequester, val secondaryAction: FocusRequester)
@@ -0,0 +1,186 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import android.graphics.BitmapFactory
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.ConfirmKeys
import java.io.File
@Composable
fun HomeHeader(
currentUser: String,
onUserClick: () -> Unit,
searchQuery: String,
onSearchQueryChange: (String) -> Unit,
isSearchExpanded: Boolean,
onSearchExpandedChange: (Boolean) -> Unit,
onSortClick: () -> Unit,
onFilterClick: () -> Unit,
onSettingsClick: () -> Unit,
focusRequesters: HeaderFocusRequesters,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
var username by remember { mutableStateOf("Eden") }
var imagePath by remember { mutableStateOf<String?>(null) }
LaunchedEffect(currentUser) {
val uuid = currentUser.ifEmpty { NativeLibrary.getCurrentUser() ?: "" }
if (uuid.isNotEmpty()) {
username = NativeLibrary.getUserUsername(uuid) ?: "Eden"
imagePath = NativeLibrary.getUserImagePath(uuid)
}
}
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = Dimens.paddingXl, vertical = Dimens.paddingMd),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
UserProfileButton(username, imagePath, onUserClick, focusRequesters.userButton)
Row(
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingLg),
verticalAlignment = Alignment.CenterVertically,
) {
ExpandableSearchBar(searchQuery, onSearchQueryChange, isSearchExpanded, onSearchExpandedChange, focusRequesters.searchButton)
HeaderIconButton(R.drawable.ic_sort, context.getString(R.string.home_sort), onSortClick, focusRequesters.sortButton)
HeaderIconButton(R.drawable.ic_filter, context.getString(R.string.home_layout), onFilterClick, focusRequesters.filterButton)
HeaderIconButton(R.drawable.ic_settings, context.getString(R.string.home_settings), onSettingsClick, focusRequesters.settingsButton)
}
}
}
@Composable
private fun UserProfileButton(
username: String,
imagePath: String?,
onClick: () -> Unit,
focusRequester: FocusRequester,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val bitmap = remember(imagePath) {
imagePath?.takeIf { it.isNotEmpty() }?.let { path ->
File(path).takeIf { it.exists() }?.let { BitmapFactory.decodeFile(path) }
}
}
val defaultBitmap = remember {
NativeLibrary.getDefaultAccountBackupJpeg().let { BitmapFactory.decodeByteArray(it, 0, it.size) }
}
Row(
modifier = Modifier
.focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
}
.clickable(interactionSource, null, onClick = onClick)
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
.border(Dimens.borderWidth, if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface, Shapes.medium)
.padding(horizontal = Dimens.paddingMd, vertical = 6.dp),
horizontalArrangement = Arrangement.spacedBy(Dimens.paddingMd),
verticalAlignment = Alignment.CenterVertically,
) {
(bitmap ?: defaultBitmap)?.let { bmp ->
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(46.dp).clip(CircleShape)
)
}
BasicText(username, style = EdenTheme.typography.body.copy(fontSize = 18.sp, color = Color.White))
}
}
@Composable
fun HeaderIconButton(
iconRes: Int,
contentDescription: String,
onClick: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
Box(
modifier = modifier
.size(Dimens.iconLg)
.focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
.onPreviewKeyEvent { e ->
if (e.type == KeyEventType.KeyDown && e.key in ConfirmKeys) { onClick(); true } else false
}
.clip(CircleShape)
.background(if (isFocused) EdenTheme.colors.primary else EdenTheme.colors.surface)
.clickable(interactionSource, null, onClick = onClick),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(iconRes),
contentDescription = contentDescription,
tint = Color.White,
modifier = Modifier.size(Dimens.iconSm),
)
}
}
data class HeaderFocusRequesters(
val userButton: FocusRequester,
val searchButton: FocusRequester,
val sortButton: FocusRequester,
val filterButton: FocusRequester,
val settingsButton: FocusRequester,
)
@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.DialogButton
enum class LayoutMode { TWO_ROW, CAROUSEL /*, ONE_ROW */ }
@Composable
fun LayoutSelectionDialog(
onDismiss: () -> Unit,
onSelectGrid: () -> Unit,
onSelectCarousel: () -> Unit,
) {
val context = LocalContext.current
val focusRequesters = remember { List(3) { FocusRequester() } }
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
Column(
modifier = Modifier
.clip(Shapes.large)
.background(EdenTheme.colors.surface)
.padding(Dimens.paddingXl),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BasicText(
text = context.getString(R.string.home_layout),
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.view_grid),
onClick = { onSelectGrid(); onDismiss() },
focusRequester = focusRequesters[0],
)
Spacer(Modifier.height(Dimens.paddingMd))
DialogButton(
text = context.getString(R.string.view_carousel),
onClick = { onSelectCarousel(); onDismiss() },
focusRequester = focusRequesters[1],
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.cancel),
onClick = onDismiss,
focusRequester = focusRequesters[2],
isSecondary = true,
)
}
}
}
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import org.yuzu.yuzu_emu.R
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
import dev.eden.emu.ui.utils.DialogButton
enum class SortMode { LAST_PLAYED, ALPHABETICAL }
@Composable
fun SortSelectionDialog(
currentSortMode: SortMode,
onDismiss: () -> Unit,
onSelectSort: (SortMode) -> Unit,
) {
val context = LocalContext.current
val focusRequesters = remember { List(3) { FocusRequester() } }
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(dismissOnBackPress = true, dismissOnClickOutside = true)) {
Column(
modifier = Modifier
.clip(Shapes.large)
.background(EdenTheme.colors.surface)
.padding(Dimens.paddingXl),
horizontalAlignment = Alignment.CenterHorizontally,
) {
BasicText(
text = context.getString(R.string.home_sort),
style = EdenTheme.typography.title.copy(color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold)
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.search_recently_played),
onClick = { onSelectSort(SortMode.LAST_PLAYED); onDismiss() },
focusRequester = focusRequesters[0],
isSelected = currentSortMode == SortMode.LAST_PLAYED,
)
Spacer(Modifier.height(Dimens.paddingMd))
DialogButton(
text = context.getString(R.string.alphabetical),
onClick = { onSelectSort(SortMode.ALPHABETICAL); onDismiss() },
focusRequester = focusRequesters[1],
isSelected = currentSortMode == SortMode.ALPHABETICAL,
)
Spacer(Modifier.height(Dimens.paddingXl))
DialogButton(
text = context.getString(R.string.cancel),
onClick = onDismiss,
focusRequester = focusRequesters[2],
isSecondary = true,
)
}
}
}
@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.navigation
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.*
import dev.eden.emu.ui.components.HeaderFocusRequesters
import dev.eden.emu.ui.utils.NavKeys
/** Navigation zones for the home screen */
enum class NavigationZone { HEADER, CONTENT/*, FOOTER */ }
/**
* Manages focus and navigation between Header and Content zones.
* Uses MutableState for reactive UI updates.
* (Default Focus Manager breaks and is based on "placement", not suitable)
*/
class HomeNavigationManager(
private val headerFocusRequesters: HeaderFocusRequesters,
private val contentFocusRequester: FocusRequester,
) {
private val _currentZone: MutableState<NavigationZone> = mutableStateOf(NavigationZone.CONTENT)
val currentZone: NavigationZone get() = _currentZone.value
private val _headerIndex: MutableState<Int> = mutableIntStateOf(0)
val headerIndex: Int get() = _headerIndex.value
// Expose state for Compose observation
val currentZoneState: MutableState<NavigationZone> get() = _currentZone
val headerIndexState: MutableState<Int> get() = _headerIndex
private val headerElements = listOf(
headerFocusRequesters.userButton,
headerFocusRequesters.searchButton,
headerFocusRequesters.sortButton,
headerFocusRequesters.filterButton,
headerFocusRequesters.settingsButton,
)
fun navigateUp() {
if (_currentZone.value == NavigationZone.CONTENT) {
_currentZone.value = NavigationZone.HEADER
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
}
}
fun navigateDown() {
if (_currentZone.value == NavigationZone.HEADER) {
_currentZone.value = NavigationZone.CONTENT
contentFocusRequester.requestFocus()
}
}
fun navigateLeft() {
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value > 0) {
_headerIndex.value--
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
}
}
fun navigateRight() {
if (_currentZone.value == NavigationZone.HEADER && _headerIndex.value < headerElements.lastIndex) {
_headerIndex.value++
headerElements.getOrNull(_headerIndex.value)?.requestFocus()
}
}
fun requestInitialFocus() {
_currentZone.value = NavigationZone.CONTENT
contentFocusRequester.requestFocus()
}
fun resetToContent() {
_currentZone.value = NavigationZone.CONTENT
}
}
/** Handle D-Pad navigation for home screen */
fun handleHomeNavigation(keyEvent: KeyEvent, nav: HomeNavigationManager): Boolean {
if (keyEvent.type != KeyEventType.KeyDown) return false
return when (keyEvent.key) {
in NavKeys.up -> { nav.navigateUp(); true }
in NavKeys.down -> { nav.navigateDown(); true }
in NavKeys.left -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateLeft(); true } else false
in NavKeys.right -> if (nav.currentZone == NavigationZone.HEADER) { nav.navigateRight(); true } else false
else -> false
}
}
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
private val LocalEdenTypography = staticCompositionLocalOf {
EdenTypography(
title = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.SemiBold,
fontSize = 20.sp,
),
body = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
),
label = TextStyle(
fontFamily = FontFamily.SansSerif,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
),
)
}
object EdenTheme {
val colors: EdenColors
@Composable get() = LocalEdenColors.current
val typography: EdenTypography
@Composable get() = LocalEdenTypography.current
}
@Composable
fun EdenTheme(
colors: EdenColors = LocalEdenColors.current,
typography: EdenTypography = LocalEdenTypography.current,
content: @Composable () -> Unit,
) {
CompositionLocalProvider(
LocalEdenColors provides colors,
LocalEdenTypography provides typography,
content = content,
)
}
data class EdenTypography(
val title: TextStyle,
val body: TextStyle,
val label: TextStyle,
)
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.theme
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
val EdenBrandColor = Color(0xFFA161F3)
val EdenSplashColor = EdenBrandColor
val EdenGridLine = Color(0x2AA161F3)
val EdenBackground = Color(0xFF191521)
val EdenSurface = Color(0xFF221C2B)
val EdenSurfaceVariant = Color(0xFF2D2538)
val EdenOnBackground = Color(0xFFEAE5F2)
val EdenOnSurface = Color(0xFFEAE5F2)
val EdenOnSurfaceVariant = Color(0xFFB0A8BA)
val EdenOnPrimary = Color(0xFFFFFFFF)
internal val LocalEdenColors = staticCompositionLocalOf {
EdenColors(
background = EdenBackground,
surface = EdenSurface,
surfaceVariant = EdenSurfaceVariant,
primary = EdenBrandColor,
onBackground = EdenOnBackground,
onSurface = EdenOnSurface,
onSurfaceVariant = EdenOnSurfaceVariant,
onPrimary = EdenOnPrimary,
)
}
data class EdenColors(
val background: Color,
val surface: Color,
val surfaceVariant: Color,
val primary: Color,
val onBackground: Color,
val onSurface: Color,
val onSurfaceVariant: Color,
val onPrimary: Color,
)
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp
// Grid background
val EdenGridSpacing = 58.dp
val EdenGridLineWidth = 2.dp
const val EdenGridSpeedDpPerSecond = 12f
// Common dimensions
object Dimens {
// Padding
val paddingXs = 4.dp
val paddingSm = 8.dp
val paddingMd = 12.dp
val paddingLg = 16.dp
val paddingXl = 24.dp
// Icon sizes
val iconSm = 24.dp
val iconMd = 32.dp
val iconLg = 48.dp
// Border
val borderWidth = 2.dp
// Corner radius
val radiusSm = 8.dp
val radiusMd = 12.dp
val radiusLg = 16.dp
}
// Common shapes
object Shapes {
val small = RoundedCornerShape(Dimens.radiusSm)
val medium = RoundedCornerShape(Dimens.radiusMd)
val large = RoundedCornerShape(Dimens.radiusLg)
}
@@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.utils
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.eden.emu.ui.theme.Dimens
import dev.eden.emu.ui.theme.EdenTheme
import dev.eden.emu.ui.theme.Shapes
/**
* New dialog button for consistent styling across dialogs
*/
@Composable
fun DialogButton(
text: String,
onClick: () -> Unit,
focusRequester: FocusRequester,
modifier: Modifier = Modifier,
isSelected: Boolean = false,
isSecondary: Boolean = false,
) {
val interactionSource = remember { MutableInteractionSource() }
val isFocused by interactionSource.collectIsFocusedAsState()
val backgroundColor = when {
isFocused -> EdenTheme.colors.primary
isSelected -> EdenTheme.colors.primary.copy(alpha = 0.3f)
isSecondary -> Color.Transparent
else -> EdenTheme.colors.background
}
val borderColor = when {
isFocused || isSelected -> EdenTheme.colors.primary
isSecondary -> EdenTheme.colors.onBackground.copy(alpha = 0.3f)
else -> EdenTheme.colors.onBackground.copy(alpha = 0.2f)
}
Box(
modifier = modifier
.fillMaxWidth()
.clip(Shapes.medium)
.background(backgroundColor)
.border(Dimens.borderWidth, borderColor, Shapes.medium)
.focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key in confirmKeys) {
onClick()
true
} else false
}
.clickable(interactionSource, null, onClick = onClick)
.padding(vertical = 14.dp, horizontal = 20.dp),
contentAlignment = Alignment.Center,
) {
BasicText(
text = if (isSelected && !isSecondary) "$text" else text,
style = EdenTheme.typography.body.copy(color = Color.White, fontSize = 16.sp)
)
}
}
private val confirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package dev.eden.emu.ui.utils
import androidx.compose.ui.input.key.Key
val ConfirmKeys = setOf(Key.Enter, Key.DirectionCenter, Key.ButtonA)
val MenuKeys = setOf(Key.ButtonX, Key.Menu)
val BackKeys = setOf(Key.Escape, Key.Back, Key.ButtonB)
object NavKeys {
val up = setOf(Key.DirectionUp, Key.W)
val down = setOf(Key.DirectionDown, Key.S)
val left = setOf(Key.DirectionLeft, Key.A)
val right = setOf(Key.DirectionRight, Key.D)
}
@@ -203,24 +203,6 @@ object NativeLibrary {
external fun getDebugKnobAt(index: Int): Boolean
/**
* Set the current speed limit to the configured turbo speed.
*/
external fun setTurboSpeedLimit(enabled: Boolean)
/**
* Set the current speed limit to the configured slow speed.
*/
external fun setSlowSpeedLimit(enabled: Boolean)
/**
* Set the current speed limit to the configured standard speed.
*/
external fun setStandardSpeedLimit(enabled: Boolean)
external fun isTurboMode(): Boolean
external fun isSlowMode(): Boolean
/**
* Returns Vulkan driver version / API version / GPU model
*/
@@ -18,18 +18,19 @@ import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import android.content.res.Configuration
import android.os.LocaleList
import coil.ImageLoader
import coil.ImageLoaderFactory
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.DocumentsTree
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.PowerStateUpdater
import org.yuzu.yuzu_emu.utils.ControllerNavigationGlobalHook
import java.util.Locale
fun Context.getPublicFilesDir(): File = getExternalFilesDir(null) ?: filesDir
class YuzuApplication : Application() {
class YuzuApplication : Application(), ImageLoaderFactory {
private fun createNotificationChannels() {
val name: CharSequence = getString(R.string.app_notification_channel_name)
val description = getString(R.string.app_notification_channel_description)
@@ -73,11 +74,16 @@ class YuzuApplication : Application() {
NativeLibrary.logDeviceInfo()
PowerStateUpdater.start()
Log.logDeviceInfo()
ControllerNavigationGlobalHook.install(this)
createNotificationChannels()
}
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.crossfade(true)
.build()
}
companion object {
var documentsTree: DocumentsTree? = null
lateinit var application: YuzuApplication
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -649,7 +649,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
fun launch(activity: AppCompatActivity, game: Game) {
val launcher = Intent(activity, EmulationActivity::class.java)
launcher.putExtra(EXTRA_SELECTED_GAME, game)
launcher.putExtra("game", game) // Use "game" to match navigation argus
activity.startActivity(launcher)
}
@@ -11,7 +11,6 @@ import android.widget.RadioGroup
import android.widget.TextView
import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.material.color.MaterialColors
import com.google.android.material.materialswitch.MaterialSwitch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
@@ -43,7 +42,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
statusText.setTextColor(
MaterialColors.getColor(
statusText,
com.google.android.material.R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
)
)
@@ -136,39 +135,6 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
container.addView(itemView)
}
fun addCustomToggle(
name: Int,
isChecked: Boolean,
isEnabled: Boolean,
container: ViewGroup,
callback: (Boolean) -> Unit
): MaterialSwitch? {
val inflater = LayoutInflater.from(emulationFragment.requireContext())
val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false)
val switchContainer = itemView.findViewById<ViewGroup>(R.id.switch_container)
val titleView = itemView.findViewById<TextView>(R.id.switch_title)
val switchView = itemView.findViewById<MaterialSwitch>(R.id.setting_switch)
titleView.text = YuzuApplication.appContext.getString(name)
switchContainer.visibility = View.VISIBLE
switchView.isChecked = isChecked
switchView.setOnCheckedChangeListener { _, checked ->
callback(checked)
saveSettings()
}
switchContainer.setOnClickListener {
switchView.toggle()
}
container.addView(itemView)
return switchView
}
fun addSliderSetting(
name: Int,
container: ViewGroup,
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.fetcher
@@ -171,7 +171,7 @@ class ReleaseAdapter(
iconTint = ColorStateList.valueOf(
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
)
)
@@ -33,11 +33,9 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
RENDERER_PROVOKING_VERTEX("provoking_vertex"),
RENDERER_DESCRIPTOR_INDEXING("descriptor_indexing"),
RENDERER_SAMPLE_SHADING("sample_shading"),
GPU_UNSWIZZLE_ENABLED("gpu_unswizzle_enabled"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),
INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS("invert_confirm_back_controller_buttons"),
ENABLE_FOLDER_BUTTON("enable_folder_button"),
ENABLE_QLAUNCH_BUTTON("enable_qlaunch_button"),
@@ -29,7 +29,6 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
RENDERER_DYNA_STATE("dyna_state"),
DMA_ACCURACY("dma_accuracy"),
FRAME_PACING_MODE("frame_pacing_mode"),
AUDIO_OUTPUT_ENGINE("output_engine"),
MAX_ANISOTROPY("max_anisotropy"),
THEME("theme"),
@@ -68,6 +67,7 @@ enum class IntSetting(override val key: String) : AbstractIntSetting {
MY_PAGE_APPLET("my_page_applet_mode"),
INPUT_OVERLAY_AUTO_HIDE("input_overlay_auto_hide"),
OVERLAY_GRID_SIZE("overlay_grid_size"),
DEBUG_KNOBS("debug_knobs"),
GPU_LOG_RING_BUFFER_SIZE("gpu_log_ring_buffer_size")
;
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -9,11 +6,7 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ShortSetting(override val key: String) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit"),
RENDERER_TURBO_SPEED_LIMIT("turbo_speed_limit"),
RENDERER_SLOW_SPEED_LIMIT("slow_speed_limit"),
DEBUG_KNOBS("debug_knobs")
;
RENDERER_SPEED_LIMIT("speed_limit");
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
@@ -29,4 +22,4 @@ enum class ShortSetting(override val key: String) : AbstractShortSetting {
override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
override fun reset() = NativeConfig.setShort(key, defaultValue)
}
}
@@ -1,75 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.ArrayRes
import androidx.annotation.StringRes
import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
class GpuUnswizzleSetting(
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
@ArrayRes val textureSizeChoicesId: Int,
@ArrayRes val textureSizeValuesId: Int,
@ArrayRes val streamSizeChoicesId: Int,
@ArrayRes val streamSizeValuesId: Int,
@ArrayRes val chunkSizeChoicesId: Int,
@ArrayRes val chunkSizeValuesId: Int
) : SettingsItem(
object : AbstractSetting {
override val key: String = SettingsItem.GPU_UNSWIZZLE_COMBINED
override val defaultValue: Any = false
override val isSaveable = true
override val isRuntimeModifiable = true
override val isSwitchable = true
override fun getValueAsString(needsGlobal: Boolean): String = "combined"
override fun reset() {
BooleanSetting.GPU_UNSWIZZLE_ENABLED.reset()
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.reset()
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.reset()
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.reset()
}
},
titleId,
titleString,
descriptionId,
descriptionString
) {
override val type = SettingsItem.TYPE_GPU_UNSWIZZLE
// Check if GPU unswizzle is enabled via the dedicated boolean setting
fun isEnabled(needsGlobal: Boolean = false): Boolean =
BooleanSetting.GPU_UNSWIZZLE_ENABLED.getBoolean(needsGlobal)
fun setEnabled(value: Boolean) =
BooleanSetting.GPU_UNSWIZZLE_ENABLED.setBoolean(value)
fun enable() = setEnabled(true)
fun disable() = setEnabled(false)
fun getTextureSize(needsGlobal: Boolean = false): Int =
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.getInt(needsGlobal)
fun setTextureSize(value: Int) =
IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.setInt(value)
fun getStreamSize(needsGlobal: Boolean = false): Int =
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.getInt(needsGlobal)
fun setStreamSize(value: Int) =
IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.setInt(value)
fun getChunkSize(needsGlobal: Boolean = false): Int =
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.getInt(needsGlobal)
fun setChunkSize(value: Int) =
IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.setInt(value)
fun reset() = setting.reset()
}
@@ -104,10 +104,8 @@ abstract class SettingsItem(
const val TYPE_SPINBOX = 12
const val TYPE_LAUNCHABLE = 13
const val TYPE_PATH = 14
const val TYPE_GPU_UNSWIZZLE = 15
const val FASTMEM_COMBINED = "fastmem_combined"
const val GPU_UNSWIZZLE_COMBINED = "gpu_unswizzle_combined"
val emptySetting = object : AbstractSetting {
override val key: String = ""
@@ -180,26 +178,6 @@ abstract class SettingsItem(
units = "%"
)
)
put(
SliderSetting(
ShortSetting.RENDERER_TURBO_SPEED_LIMIT,
titleId = R.string.turbo_speed_limit,
descriptionId = R.string.turbo_speed_limit_description,
min = 1,
max = 400,
units = "%"
)
)
put(
SliderSetting(
ShortSetting.RENDERER_SLOW_SPEED_LIMIT,
titleId = R.string.slow_speed_limit,
descriptionId = R.string.slow_speed_limit_description,
min = 1,
max = 400,
units = "%"
)
)
put(
SingleChoiceSetting(
IntSetting.CPU_BACKEND,
@@ -378,13 +356,6 @@ abstract class SettingsItem(
warningMessage = R.string.warning_resolution
)
)
put(
SwitchSetting(
BooleanSetting.INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS,
titleId = R.string.invert_confirm_back_controller_buttons,
descriptionId = R.string.invert_confirm_back_controller_buttons_description
)
)
put(
SwitchSetting(
BooleanSetting.SHOW_INPUT_OVERLAY,
@@ -670,15 +641,6 @@ abstract class SettingsItem(
valuesId = R.array.dmaAccuracyValues
)
)
put(
SingleChoiceSetting(
IntSetting.FRAME_PACING_MODE,
titleId = R.string.frame_pacing_mode,
descriptionId = R.string.frame_pacing_mode_description,
choicesId = R.array.framePacingModeNames,
valuesId = R.array.framePacingModeValues
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS,
@@ -722,18 +684,6 @@ abstract class SettingsItem(
valuesId = R.array.gpuSwizzleChunkValues
)
)
put(
GpuUnswizzleSetting(
titleId = R.string.gpu_unswizzle_settings,
descriptionId = R.string.gpu_unswizzle_settings_description,
textureSizeChoicesId = R.array.gpuTextureSizeSwizzleEntries,
textureSizeValuesId = R.array.gpuTextureSizeSwizzleValues,
streamSizeChoicesId = R.array.gpuSwizzleEntries,
streamSizeValuesId = R.array.gpuSwizzleValues,
chunkSizeChoicesId = R.array.gpuSwizzleChunkEntries,
chunkSizeValuesId = R.array.gpuSwizzleChunkValues
)
)
put(
SingleChoiceSetting(
IntSetting.FAST_CPU_TIME,
@@ -903,7 +853,7 @@ abstract class SettingsItem(
)
put(
SpinBoxSetting(
ShortSetting.DEBUG_KNOBS,
IntSetting.DEBUG_KNOBS,
titleId = R.string.debug_knobs,
descriptionId = R.string.debug_knobs_description,
valueHint = R.string.debug_knobs_hint,
@@ -1,206 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.ArrayAdapter
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogGpuUnswizzleBinding
import org.yuzu.yuzu_emu.features.settings.model.view.GpuUnswizzleSetting
class GpuUnswizzleDialogFragment : DialogFragment() {
private var position = 0
private val settingsViewModel: SettingsViewModel by activityViewModels()
private lateinit var binding: DialogGpuUnswizzleBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
position = requireArguments().getInt(POSITION)
if (settingsViewModel.clickedItem == null) dismiss()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
binding = DialogGpuUnswizzleBinding.inflate(LayoutInflater.from(requireContext()))
val item = settingsViewModel.clickedItem as GpuUnswizzleSetting
// Setup texture size dropdown
val textureSizeEntries = resources.getStringArray(item.textureSizeChoicesId)
val textureSizeValues = resources.getIntArray(item.textureSizeValuesId)
val textureSizeAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
textureSizeEntries.toMutableList()
)
binding.dropdownTextureSize.setAdapter(textureSizeAdapter)
// Setup stream size dropdown
val streamSizeEntries = resources.getStringArray(item.streamSizeChoicesId)
val streamSizeValues = resources.getIntArray(item.streamSizeValuesId)
val streamSizeAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
streamSizeEntries.toMutableList()
)
binding.dropdownStreamSize.setAdapter(streamSizeAdapter)
// Setup chunk size dropdown
val chunkSizeEntries = resources.getStringArray(item.chunkSizeChoicesId)
val chunkSizeValues = resources.getIntArray(item.chunkSizeValuesId)
val chunkSizeAdapter = ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
chunkSizeEntries.toMutableList()
)
binding.dropdownChunkSize.setAdapter(chunkSizeAdapter)
// Load current values
val isEnabled = item.isEnabled()
binding.switchEnable.isChecked = isEnabled
if (isEnabled) {
val textureSizeIndex = textureSizeValues.indexOf(item.getTextureSize())
if (textureSizeIndex >= 0) {
binding.dropdownTextureSize.setText(textureSizeEntries[textureSizeIndex], false)
}
val streamSizeIndex = streamSizeValues.indexOf(item.getStreamSize())
if (streamSizeIndex >= 0) {
binding.dropdownStreamSize.setText(streamSizeEntries[streamSizeIndex], false)
}
val chunkSizeIndex = chunkSizeValues.indexOf(item.getChunkSize())
if (chunkSizeIndex >= 0) {
binding.dropdownChunkSize.setText(chunkSizeEntries[chunkSizeIndex], false)
}
} else {
// Set default/recommended values when disabling
binding.dropdownTextureSize.setText(textureSizeEntries[3], false)
binding.dropdownStreamSize.setText(streamSizeEntries[3], false)
binding.dropdownChunkSize.setText(chunkSizeEntries[3], false)
}
// Clear adapter filters after setText to fix rotation bug
textureSizeAdapter.filter.filter(null)
streamSizeAdapter.filter.filter(null)
chunkSizeAdapter.filter.filter(null)
// Enable/disable dropdowns based on switch state
updateDropdownsState(isEnabled)
binding.switchEnable.setOnCheckedChangeListener { _, checked ->
updateDropdownsState(checked)
}
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setView(binding.root)
.create()
// Setup button listeners
binding.btnDefault.setOnClickListener {
// Reset to defaults
item.reset()
// Refresh values with adapters reset
val textureSizeIndex = textureSizeValues.indexOf(item.getTextureSize())
if (textureSizeIndex >= 0) {
binding.dropdownTextureSize.setText(textureSizeEntries[textureSizeIndex], false)
}
val streamSizeIndex = streamSizeValues.indexOf(item.getStreamSize())
if (streamSizeIndex >= 0) {
binding.dropdownStreamSize.setText(streamSizeEntries[streamSizeIndex], false)
}
val chunkSizeIndex = chunkSizeValues.indexOf(item.getChunkSize())
if (chunkSizeIndex >= 0) {
binding.dropdownChunkSize.setText(chunkSizeEntries[chunkSizeIndex], false)
}
// Clear filters
textureSizeAdapter.filter.filter(null)
streamSizeAdapter.filter.filter(null)
chunkSizeAdapter.filter.filter(null)
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
binding.btnCancel.setOnClickListener {
dialog.dismiss()
}
binding.btnOk.setOnClickListener {
if (binding.switchEnable.isChecked) {
item.enable()
// Save the selected values
val selectedTextureIndex = textureSizeEntries.indexOf(
binding.dropdownTextureSize.text.toString()
)
if (selectedTextureIndex >= 0) {
item.setTextureSize(textureSizeValues[selectedTextureIndex])
}
val selectedStreamIndex = streamSizeEntries.indexOf(
binding.dropdownStreamSize.text.toString()
)
if (selectedStreamIndex >= 0) {
item.setStreamSize(streamSizeValues[selectedStreamIndex])
}
val selectedChunkIndex = chunkSizeEntries.indexOf(
binding.dropdownChunkSize.text.toString()
)
if (selectedChunkIndex >= 0) {
item.setChunkSize(chunkSizeValues[selectedChunkIndex])
}
} else {
// Disable GPU unswizzle
item.disable()
}
settingsViewModel.setAdapterItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
dialog.dismiss()
}
// Ensure filters are cleared after dialog is shown
binding.root.post {
textureSizeAdapter.filter.filter(null)
streamSizeAdapter.filter.filter(null)
chunkSizeAdapter.filter.filter(null)
}
return dialog
}
private fun updateDropdownsState(enabled: Boolean) {
binding.layoutTextureSize.isEnabled = enabled
binding.dropdownTextureSize.isEnabled = enabled
binding.layoutStreamSize.isEnabled = enabled
binding.dropdownStreamSize.isEnabled = enabled
binding.layoutChunkSize.isEnabled = enabled
binding.dropdownChunkSize.isEnabled = enabled
}
companion object {
const val TAG = "GpuUnswizzleDialogFragment"
const val POSITION = "Position"
fun newInstance(
settingsViewModel: SettingsViewModel,
item: GpuUnswizzleSetting,
position: Int
): GpuUnswizzleDialogFragment {
val dialog = GpuUnswizzleDialogFragment()
val args = Bundle()
args.putInt(POSITION, position)
dialog.arguments = args
settingsViewModel.clickedItem = item
return dialog
}
}
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui
@@ -101,11 +101,6 @@ class SettingsAdapter(
SettingsItem.TYPE_PATH -> {
PathViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_GPU_UNSWIZZLE -> {
GpuUnswizzleViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
else -> {
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
@@ -479,14 +474,6 @@ class SettingsAdapter(
settingsViewModel.setShouldShowPathResetDialog(true)
}
fun onGpuUnswizzleClick(item: GpuUnswizzleSetting, position: Int) {
GpuUnswizzleDialogFragment.newInstance(
settingsViewModel,
item,
position
).show(fragment.childFragmentManager, GpuUnswizzleDialogFragment.TAG)
}
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key
@@ -227,8 +227,6 @@ class SettingsFragmentPresenter(
add(StringSetting.DEVICE_NAME.key)
add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_TURBO_SPEED_LIMIT.key)
add(ShortSetting.RENDERER_SLOW_SPEED_LIMIT.key)
add(BooleanSetting.USE_DOCKED_MODE.key)
add(IntSetting.REGION_INDEX.key)
add(IntSetting.LANGUAGE_INDEX.key)
@@ -267,7 +265,6 @@ class SettingsFragmentPresenter(
add(IntSetting.RENDERER_ACCURACY.key)
add(IntSetting.DMA_ACCURACY.key)
add(IntSetting.FRAME_PACING_MODE.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_VRAM_USAGE_MODE.key)
add(IntSetting.RENDERER_ASTC_DECODE_METHOD.key)
@@ -285,7 +282,9 @@ class SettingsFragmentPresenter(
add(BooleanSetting.SKIP_CPU_INNER_INVALIDATION.key)
add(BooleanSetting.FIX_BLOOM_EFFECTS.key)
add(BooleanSetting.RENDERER_ASYNCHRONOUS_SHADERS.key)
add(SettingsItem.GPU_UNSWIZZLE_COMBINED)
add(IntSetting.GPU_UNSWIZZLE_TEXTURE_SIZE.key)
add(IntSetting.GPU_UNSWIZZLE_STREAM_SIZE.key)
add(IntSetting.GPU_UNSWIZZLE_CHUNK_SIZE.key)
add(HeaderSetting(R.string.extensions))
@@ -1076,7 +1075,6 @@ class SettingsFragmentPresenter(
}
add(BooleanSetting.ENABLE_QUICK_SETTINGS.key)
add(BooleanSetting.INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS.key)
add(HeaderSetting(R.string.theme_and_color))
@@ -1230,7 +1228,7 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.general))
add(ShortSetting.DEBUG_KNOBS.key)
add(IntSetting.DEBUG_KNOBS.key)
add(HeaderSetting(R.string.gpu_logging_header))
add(BooleanSetting.GPU_LOGGING_ENABLED.key)
@@ -1,71 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.GpuUnswizzleSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class GpuUnswizzleViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: GpuUnswizzleSetting
override fun bind(item: SettingsItem) {
setting = item as GpuUnswizzleSetting
binding.textSettingName.text = setting.title
binding.textSettingDescription.setVisible(item.description.isNotEmpty())
binding.textSettingDescription.text = item.description
binding.textSettingValue.setVisible(true)
val resMgr = binding.root.context.resources
if (setting.isEnabled()) {
// Show a summary of current settings
val textureSizeEntries = resMgr.getStringArray(setting.textureSizeChoicesId)
val textureSizeValues = resMgr.getIntArray(setting.textureSizeValuesId)
val textureSizeIndex = textureSizeValues.indexOf(setting.getTextureSize())
val textureSizeLabel = if (textureSizeIndex >= 0) textureSizeEntries[textureSizeIndex] else "?"
val streamSizeEntries = resMgr.getStringArray(setting.streamSizeChoicesId)
val streamSizeValues = resMgr.getIntArray(setting.streamSizeValuesId)
val streamSizeIndex = streamSizeValues.indexOf(setting.getStreamSize())
val streamSizeLabel = if (streamSizeIndex >= 0) streamSizeEntries[streamSizeIndex] else "?"
val chunkSizeEntries = resMgr.getStringArray(setting.chunkSizeChoicesId)
val chunkSizeValues = resMgr.getIntArray(setting.chunkSizeValuesId)
val chunkSizeIndex = chunkSizeValues.indexOf(setting.getChunkSize())
val chunkSizeLabel = if (chunkSizeIndex >= 0) chunkSizeEntries[chunkSizeIndex] else "?"
binding.textSettingValue.text = "$textureSizeLabel$streamSizeLabel$chunkSizeLabel"
} else {
binding.textSettingValue.text = resMgr.getString(R.string.gpu_unswizzle_disabled)
}
binding.buttonClear.setVisible(setting.clearable)
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)
}
override fun onClick(clicked: View) {
if (!setting.isEditable) {
return
}
adapter.onGpuUnswizzleClick(setting, bindingAdapterPosition)
}
override fun onLongClick(clicked: View): Boolean {
if (setting.isEditable) {
return adapter.onLongClick(setting, bindingAdapterPosition)
}
return false
}
}
@@ -55,7 +55,6 @@ import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.textview.MaterialTextView
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -1056,47 +1055,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
quickSettings.addPerGameConfigStatusIndicator(container)
}
lateinit var slowSpeed: MaterialSwitch
lateinit var turboSpeed: MaterialSwitch
turboSpeed = quickSettings.addCustomToggle(
R.string.turbo_speed_limit,
NativeLibrary.isTurboMode(),
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
container
) { enabled ->
if (enabled)
slowSpeed.isChecked = false
NativeLibrary.setTurboSpeedLimit(enabled)
}!!
slowSpeed = quickSettings.addCustomToggle(
R.string.slow_speed_limit,
NativeLibrary.isSlowMode(),
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
container
) { enabled ->
if (enabled)
turboSpeed.isChecked = false
NativeLibrary.setSlowSpeedLimit(enabled)
}!!
quickSettings.addCustomToggle(
quickSettings.addBooleanSetting(
R.string.frame_limit_enable,
BooleanSetting.RENDERER_USE_SPEED_LIMIT.getBoolean(false),
true,
container
) { enabled ->
if (!enabled) {
turboSpeed.isChecked = false
slowSpeed.isChecked = false
}
turboSpeed.isEnabled = enabled
slowSpeed.isEnabled = enabled
NativeLibrary.setStandardSpeedLimit(enabled)
}!!
container,
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
)
quickSettings.addSliderSetting(
R.string.frame_limit_slider,
@@ -1347,7 +1310,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
)
)
}
@@ -1534,7 +1497,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
)
)
}
@@ -61,16 +61,26 @@ class GamesViewModel : ViewModel() {
}
fun setGames(games: List<Game>) {
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
val sortedList = games.sortedWith(
compareBy(
{ it.title.lowercase(Locale.getDefault()) },
{ it.path }
)
compareByDescending<Game> { game ->
preferences.getLong(game.keyLastPlayedTime, 0L)
}.thenBy { it.title.lowercase(Locale.getDefault()) }
.thenBy { it.path }
)
_games.value = sortedList
}
fun resortGames() {
val currentGames = _games.value
if (currentGames.isNotEmpty()) {
setGames(currentGames)
_shouldScrollToTop.value = true
}
}
fun setShouldSwapData(shouldSwap: Boolean) {
_shouldSwapData.value = shouldSwap
}
@@ -3,565 +3,468 @@
package org.yuzu.yuzu_emu.ui
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.core.widget.doOnTextChanged
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import java.util.Locale
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.yuzu.yuzu_emu.HomeNavigationDirections
import kotlinx.coroutines.delay
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GameAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.model.AppletInfo
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
import org.yuzu.yuzu_emu.utils.collect
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import java.util.Locale
import androidx.core.content.edit
import androidx.core.view.doOnNextLayout
import dev.eden.emu.ui.background.RetroGridBackground
import dev.eden.emu.ui.components.FooterAction
import dev.eden.emu.ui.components.FooterButton
import dev.eden.emu.ui.components.GameCarousel
import dev.eden.emu.ui.components.GameGrid
import dev.eden.emu.ui.components.GameTile
import dev.eden.emu.ui.components.HeaderFocusRequesters
import dev.eden.emu.ui.components.HomeFooter
import dev.eden.emu.ui.components.HomeHeader
import dev.eden.emu.ui.components.LayoutMode
import dev.eden.emu.ui.components.LayoutSelectionDialog
import dev.eden.emu.ui.components.SortMode
import dev.eden.emu.ui.components.SortSelectionDialog
import dev.eden.emu.ui.navigation.HomeNavigationManager
import dev.eden.emu.ui.navigation.NavigationZone
import dev.eden.emu.ui.navigation.handleHomeNavigation
import dev.eden.emu.ui.theme.EdenTheme
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.model.Game
private const val PREF_LAYOUT_MODE = "home_layout_mode"
private const val PREF_SORT_MODE = "home_sort_mode"
private const val PREF_LAST_FOCUSED_INDEX = "home_last_focused_index"
private const val PREF_GAME_WAS_LAUNCHED = "home_game_was_launched"
/**
* Compose-based Games Fragment with Header/Footer navigation
*/
class GamesFragment : Fragment() {
private var _binding: FragmentGamesBinding? = null
private val binding get() = _binding!!
private var originalHeaderTopMargin: Int? = null
private var originalHeaderBottomMargin: Int? = null
private var originalHeaderRightMargin: Int? = null
private var originalHeaderLeftMargin: Int? = null
private var lastViewType: Int = GameAdapter.VIEW_TYPE_GRID
private var fallbackBottomInset: Int = 0
companion object {
private const val SEARCH_TEXT = "SearchText"
private const val PREF_SORT_TYPE = "GamesSortType"
}
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
private lateinit var gameAdapter: GameAdapter
private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private lateinit var mainActivity: MainActivity
private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
mainActivity.processGamesDir(result, true)
}
}
private fun getCurrentViewType(): Int {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
val fallback = if (isLandscape) GameAdapter.VIEW_TYPE_CAROUSEL else GameAdapter.VIEW_TYPE_GRID
return preferences.getInt(key, fallback)
}
private fun setCurrentViewType(type: Int) {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val key = if (isLandscape) CarouselRecyclerView.CAROUSEL_VIEW_TYPE_LANDSCAPE else CarouselRecyclerView.CAROUSEL_VIEW_TYPE_PORTRAIT
preferences.edit { putInt(key, type) }
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentGamesBinding.inflate(inflater)
return binding.root
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
EdenTheme {
HomeScreen(
gamesViewModel = gamesViewModel,
onGameLaunch = { path: String ->
val game = gamesViewModel.games.value.find { it.path == path }
if (game != null) {
// Save last played time for sorting
PreferenceManager.getDefaultSharedPreferences(requireContext())
.edit()
.putLong(game.keyLastPlayedTime, System.currentTimeMillis())
.putBoolean(PREF_GAME_WAS_LAUNCHED, true)
.apply()
EmulationActivity.launch(
requireActivity() as androidx.appcompat.app.AppCompatActivity,
game
)
}
},
onNavigateToSettings = {
findNavController().navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
},
onNavigateToUserManagement = {
findNavController().navigate(R.id.action_gamesFragment_to_profileManagerFragment)
},
onShowGameInfo = { game: Game ->
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(game)
findNavController().navigate(action)
}
)
}
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setStatusBarShadeVisibility(true)
mainActivity = requireActivity() as MainActivity
if (savedInstanceState != null) {
binding.searchText.setText(savedInstanceState.getString(SEARCH_TEXT))
}
gameAdapter = GameAdapter(
requireActivity() as AppCompatActivity
)
applyGridGamesBinding()
binding.swipeRefresh.apply {
(binding.swipeRefresh as? SwipeRefreshLayout)?.setOnRefreshListener {
gamesViewModel.reloadGames(false)
}
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressBackgroundColorSchemeColor(
com.google.android.material.color.MaterialColors.getColor(
binding.swipeRefresh,
com.google.android.material.R.attr.colorPrimary
)
)
(binding.swipeRefresh as? SwipeRefreshLayout)?.setColorSchemeColors(
com.google.android.material.color.MaterialColors.getColor(
binding.swipeRefresh,
com.google.android.material.R.attr.colorOnPrimary
)
)
post {
if (_binding == null) {
return@post
}
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = gamesViewModel.isReloading.value
}
}
gamesViewModel.isReloading.collect(viewLifecycleOwner) {
(binding.swipeRefresh as? SwipeRefreshLayout)?.isRefreshing = it
binding.noticeText.setVisible(
visible = gamesViewModel.games.value.isEmpty() && !it,
gone = false
)
}
gamesViewModel.games.collect(viewLifecycleOwner) {
if (it.isNotEmpty()) {
setAdapter(it)
}
}
gamesViewModel.shouldSwapData.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setShouldSwapData(false) }
) {
if (it) {
setAdapter(gamesViewModel.games.value)
}
}
gamesViewModel.shouldScrollToTop.collect(
viewLifecycleOwner,
resetState = { gamesViewModel.setShouldScrollToTop(false) }
) { if (it) scrollToTop() }
gamesViewModel.shouldScrollAfterReload.collect(viewLifecycleOwner) { shouldScroll ->
if (shouldScroll) {
binding.gridGames.post {
(binding.gridGames as? CarouselRecyclerView)?.pendingScrollAfterReload = true
gameAdapter.notifyDataSetChanged()
}
gamesViewModel.setShouldScrollAfterReload(false)
}
}
setupTopView()
updateButtonsVisibility()
binding.addDirectory.setOnClickListener {
getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
}
binding.launchQlaunch?.setOnClickListener {
launchQLaunch()
}
setInsets()
}
val applyGridGamesBinding = {
(binding.gridGames as? RecyclerView)?.apply {
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val currentViewType = getCurrentViewType()
val savedViewType = if (isLandscape || currentViewType != GameAdapter.VIEW_TYPE_CAROUSEL) currentViewType else GameAdapter.VIEW_TYPE_GRID
//This prevents Grid/List views from reusing scaled or otherwise modified ViewHolders left over from the carousel.
adapter = null
recycledViewPool.clear()
gameAdapter.setViewType(savedViewType)
currentFilter = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
// Set the correct layout manager
layoutManager = when (savedViewType) {
GameAdapter.VIEW_TYPE_GRID -> {
val columns = resources.getInteger(R.integer.game_columns_grid)
GridLayoutManager(context, columns)
}
GameAdapter.VIEW_TYPE_GRID_COMPACT -> {
val columns = resources.getInteger(R.integer.game_columns_grid)
GridLayoutManager(context, columns)
}
GameAdapter.VIEW_TYPE_LIST -> {
val columns = resources.getInteger(R.integer.game_columns_list)
GridLayoutManager(context, columns)
}
GameAdapter.VIEW_TYPE_CAROUSEL -> {
LinearLayoutManager(context, RecyclerView.HORIZONTAL, false)
}
else -> throw IllegalArgumentException("Invalid view type: $savedViewType")
}
if (savedViewType == GameAdapter.VIEW_TYPE_CAROUSEL) {
(binding.gridGames as? View)?.let { it -> ViewCompat.requestApplyInsets(it)}
doOnNextLayout { //Carousel: important to avoid overlap issues
(this as? CarouselRecyclerView)?.notifyLaidOut(fallbackBottomInset)
}
} else {
(this as? CarouselRecyclerView)?.setupCarousel(false)
}
adapter = gameAdapter
lastViewType = savedViewType
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (_binding != null) {
outState.putString(SEARCH_TEXT, binding.searchText.text.toString())
}
}
override fun onPause() {
super.onPause()
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
gamesViewModel.lastScrollPosition = (binding.gridGames as? CarouselRecyclerView)?.getClosestChildPosition() ?: 0
}
}
override fun onResume() {
super.onResume()
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) {
(binding.gridGames as? CarouselRecyclerView)?.setupCarousel(true)
(binding.gridGames as? CarouselRecyclerView)?.restoreScrollState(gamesViewModel.lastScrollPosition)
// Only resort games if a game was actually launched (not when coming back from settings)
val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (prefs.getBoolean(PREF_GAME_WAS_LAUNCHED, false)) {
prefs.edit().putBoolean(PREF_GAME_WAS_LAUNCHED, false).apply()
gamesViewModel.resortGames()
}
}
}
@Composable
private fun HomeScreen(
gamesViewModel: GamesViewModel,
onGameLaunch: (String) -> Unit,
onNavigateToSettings: () -> Unit,
onNavigateToUserManagement: () -> Unit,
onShowGameInfo: (Game) -> Unit,
) {
val context = LocalContext.current
val games by gamesViewModel.games.collectAsState()
val isLoading by gamesViewModel.isReloading.collectAsState()
val shouldScrollToTop by gamesViewModel.shouldScrollToTop.collectAsState()
val preferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
val savedLayoutMode = remember {
val savedValue = preferences.getString(PREF_LAYOUT_MODE, LayoutMode.CAROUSEL.name)
try {
LayoutMode.valueOf(savedValue ?: LayoutMode.CAROUSEL.name)
} catch (_: Exception) {
LayoutMode.CAROUSEL
}
}
// Layout mode state (Grid or Carousel)
var layoutMode by remember { mutableStateOf(savedLayoutMode) }
var showLayoutDialog by remember { mutableStateOf(false) }
var showSortDialog by remember { mutableStateOf(false) }
var searchQuery by remember { mutableStateOf("") }
var isSearchExpanded by remember { mutableStateOf(false) }
// Game properties screen state
var selectedGameTile by remember { mutableStateOf<GameTile?>(null) }
// Sort mode state
val savedSortMode = remember {
val savedValue = preferences.getString(PREF_SORT_MODE, SortMode.LAST_PLAYED.name)
try {
SortMode.valueOf(savedValue ?: SortMode.LAST_PLAYED.name)
} catch (_: Exception) {
SortMode.LAST_PLAYED
}
}
var sortMode by remember { mutableStateOf(savedSortMode) }
// Save sort mode when it changes
LaunchedEffect(sortMode) {
preferences.edit().putString(PREF_SORT_MODE, sortMode.name).apply()
}
// Save layout mode when it changes
LaunchedEffect(layoutMode) {
preferences.edit().putString(PREF_LAYOUT_MODE, layoutMode.name).apply()
}
// Remember last focused index across layout changes - load from preferences
val savedFocusedIndex = remember {
preferences.getInt(PREF_LAST_FOCUSED_INDEX, 0)
}
var lastFocusedIndex by remember { mutableStateOf(savedFocusedIndex) }
LaunchedEffect(lastFocusedIndex) {
preferences.edit().putInt(PREF_LAST_FOCUSED_INDEX, lastFocusedIndex).apply()
}
var listVersion by remember { mutableStateOf(0) }
LaunchedEffect(sortMode) {
lastFocusedIndex = 0
listVersion++
}
// Reset to first item when games are resorted (e.g. after playing a game)
// Else the sorting only takes effect after relaunch
LaunchedEffect(shouldScrollToTop) {
if (shouldScrollToTop) {
lastFocusedIndex = 0
listVersion++ // Force complete recomposition
gamesViewModel.setShouldScrollToTop(false)
}
}
// Load current user UUID
var currentUserUuid by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
currentUserUuid = NativeLibrary.getCurrentUser() ?: ""
}
val headerFocusRequesters = remember {
HeaderFocusRequesters(
userButton = FocusRequester(),
searchButton = FocusRequester(),
sortButton = FocusRequester(),
filterButton = FocusRequester(),
settingsButton = FocusRequester(),
)
}
// Don't recreate on layoutMode change - this causes zones state loss
val contentFocusRequester = remember { FocusRequester() }
val navigationManager = remember {
HomeNavigationManager(
headerFocusRequesters = headerFocusRequesters,
contentFocusRequester = contentFocusRequester,
)
}
// Request initial focus
LaunchedEffect(Unit) {
delay(200)
navigationManager.requestInitialFocus()
}
// Filter and sort games based on search query and sort mode
val filteredGames = remember(games, searchQuery, sortMode) {
val filtered = if (searchQuery.isBlank()) {
games
} else {
games.filter { game ->
game.title.lowercase(Locale.getDefault())
.contains(searchQuery.lowercase(Locale.getDefault()))
}
}
// Apply sorting
when (sortMode) {
SortMode.LAST_PLAYED -> {
filtered.sortedByDescending { game ->
preferences.getLong(game.keyLastPlayedTime, 0L)
}
}
SortMode.ALPHABETICAL -> {
filtered.sortedBy { it.title.lowercase(Locale.getDefault()) }
}
}
}
val gameTiles = remember(filteredGames) {
filteredGames.map { game ->
GameTile.fromGame(game)
}
}
// NO key handling at top level
Box(modifier = Modifier.fillMaxSize()) {
RetroGridBackground()
val contentOffset = 24.dp
key(listVersion, layoutMode) {
when (layoutMode) {
LayoutMode.TWO_ROW -> {
GameGrid(
gameTiles = gameTiles,
onGameClick = { tile: GameTile ->
val path = tile.uri ?: return@GameGrid
onGameLaunch(path)
},
onGameLongClick = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
onNavigateToSettings = {},
focusRequester = contentFocusRequester,
onNavigateUp = {
navigationManager.navigateUp()
},
rowCount = 2,
tileIconSize = 120.dp,
isLoading = isLoading,
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
initialFocusedIndex = lastFocusedIndex,
onFocusedIndexChanged = { newIndex ->
lastFocusedIndex = newIndex
},
onShowGameInfo = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
modifier = Modifier
.fillMaxSize()
.padding(top = contentOffset),
)
}
LayoutMode.CAROUSEL -> {
GameCarousel(
gameTiles = gameTiles,
onGameClick = { tile: GameTile ->
val path = tile.uri ?: return@GameCarousel
onGameLaunch(path)
},
onGameLongClick = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
focusRequester = contentFocusRequester,
onNavigateUp = {
navigationManager.navigateUp()
},
initialFocusedIndex = lastFocusedIndex,
onFocusedIndexChanged = { newIndex ->
lastFocusedIndex = newIndex
},
onShowGameInfo = { tile: GameTile ->
val game = filteredGames.firstOrNull { it.path == tile.uri }
if (game != null) {
onShowGameInfo(game)
}
},
modifier = Modifier
.fillMaxSize()
.padding(top = contentOffset),
)
}
else -> {
// Default to Grid
GameGrid(
gameTiles = gameTiles,
onGameClick = { tile: GameTile ->
val path = tile.uri ?: return@GameGrid
onGameLaunch(path)
},
onNavigateToSettings = {},
focusRequester = contentFocusRequester,
onNavigateUp = {
navigationManager.navigateUp()
},
rowCount = 2,
tileIconSize = 120.dp,
isLoading = isLoading,
emptyMessage = if (games.isEmpty() && !isLoading) context.getString(R.string.empty_gamelist) else null,
modifier = Modifier
.fillMaxSize()
.padding(top = contentOffset),
)
}
}
// Refocus content when layout mode changes
LaunchedEffect(layoutMode) {
delay(200)
contentFocusRequester.requestFocus()
navigationManager.resetToContent()
}
// Refocus content when sort mode changes
LaunchedEffect(sortMode) {
delay(200)
contentFocusRequester.requestFocus()
navigationManager.resetToContent()
}
// Header
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.onPreviewKeyEvent { keyEvent ->
if (navigationManager.currentZone == NavigationZone.HEADER) {
handleHomeNavigation(keyEvent, navigationManager)
} else {
false
}
}
) {
HomeHeader(
currentUser = currentUserUuid,
onUserClick = onNavigateToUserManagement,
searchQuery = searchQuery,
onSearchQueryChange = { query -> searchQuery = query },
isSearchExpanded = isSearchExpanded,
onSearchExpandedChange = { expanded -> isSearchExpanded = expanded },
onSortClick = {
showSortDialog = true
},
onFilterClick = {
showLayoutDialog = true
},
onSettingsClick = onNavigateToSettings,
focusRequesters = headerFocusRequesters,
)
}
val currentZone by navigationManager.currentZoneState
val headerIdx by navigationManager.headerIndexState
val footerActions = when (currentZone) {
NavigationZone.HEADER -> {
val headerText = when (headerIdx) {
0 -> context.getString(R.string.footer_open) // User profile
1 -> context.getString(R.string.home_search) // Search
else -> context.getString(R.string.footer_open) // Sort, Filter, Settings
}
listOf(FooterAction(FooterButton.A, headerText))
}
NavigationZone.CONTENT -> {
listOf(
FooterAction(FooterButton.A, context.getString(R.string.home_start)),
FooterAction(FooterButton.X, context.getString(R.string.footer_game_info)),
)
}
}
HomeFooter(
actions = footerActions,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
// Layout selection dialog
if (showLayoutDialog) {
LayoutSelectionDialog(
onDismiss = { showLayoutDialog = false },
onSelectGrid = { layoutMode = LayoutMode.TWO_ROW },
onSelectCarousel = { layoutMode = LayoutMode.CAROUSEL },
)
}
// Sort selection dialog
if (showSortDialog) {
SortSelectionDialog(
currentSortMode = sortMode,
onDismiss = { showSortDialog = false },
onSelectSort = { selectedSortMode ->
sortMode = selectedSortMode
gamesViewModel.resortGames()
},
)
}
}
private var lastSearchText: String = ""
private var lastFilter: Int = preferences.getInt(PREF_SORT_TYPE, View.NO_ID)
private fun setAdapter(games: List<Game>) {
val currentSearchText = binding.searchText.text.toString()
val currentFilter = binding.filterButton.id
val searchChanged = currentSearchText != lastSearchText
val filterChanged = currentFilter != lastFilter
if (searchChanged || filterChanged) {
filterAndSearch(games)
lastSearchText = currentSearchText
lastFilter = currentFilter
} else {
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(games)
gamesViewModel.setFilteredGames(games)
}
}
private fun setupTopView() {
binding.searchText.doOnTextChanged() { text: CharSequence?, _: Int, _: Int, _: Int ->
if (text.toString().isNotEmpty()) {
binding.clearButton.visibility = View.VISIBLE
} else {
binding.clearButton.visibility = View.INVISIBLE
}
filterAndSearch()
}
binding.clearButton.setOnClickListener { binding.searchText.setText("") }
binding.searchBackground.setOnClickListener { focusSearch() }
// Setup view button
binding.viewButton.setOnClickListener { showViewMenu(it) }
// Setup filter button
binding.filterButton.setOnClickListener { view ->
showFilterMenu(view)
}
// Setup settings button
binding.settingsButton.setOnClickListener { navigateToSettings() }
}
private fun navigateToSettings() {
val navController = findNavController()
navController.navigate(R.id.action_gamesFragment_to_homeSettingsFragment)
}
private fun showViewMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.menu_game_views, popup.menu)
val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
if (!isLandscape) {
popup.menu.findItem(R.id.view_carousel)?.isVisible = false
}
val currentViewType = getCurrentViewType()
when (currentViewType) {
GameAdapter.VIEW_TYPE_LIST -> popup.menu.findItem(R.id.view_list).isChecked = true
GameAdapter.VIEW_TYPE_GRID_COMPACT -> popup.menu.findItem(R.id.view_grid_compact).isChecked = true
GameAdapter.VIEW_TYPE_GRID -> popup.menu.findItem(R.id.view_grid).isChecked = true
GameAdapter.VIEW_TYPE_CAROUSEL -> popup.menu.findItem(R.id.view_carousel).isChecked = true
}
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.view_grid -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID)
applyGridGamesBinding()
item.isChecked = true
true
}
R.id.view_grid_compact -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_GRID_COMPACT)
applyGridGamesBinding()
item.isChecked = true
true
}
R.id.view_list -> {
if (getCurrentViewType() == GameAdapter.VIEW_TYPE_CAROUSEL) onPause()
setCurrentViewType(GameAdapter.VIEW_TYPE_LIST)
applyGridGamesBinding()
item.isChecked = true
true
}
R.id.view_carousel -> {
if (!item.isChecked || getCurrentViewType() != GameAdapter.VIEW_TYPE_CAROUSEL) {
setCurrentViewType(GameAdapter.VIEW_TYPE_CAROUSEL)
applyGridGamesBinding()
item.isChecked = true
onResume()
}
true
}
else -> false
}
}
popup.show()
}
private fun showFilterMenu(anchor: View) {
val popup = PopupMenu(requireContext(), anchor)
popup.menuInflater.inflate(R.menu.menu_game_filters, popup.menu)
// Set checked state based on current filter
when (currentFilter) {
R.id.alphabetical -> popup.menu.findItem(R.id.alphabetical).isChecked = true
R.id.filter_recently_played -> popup.menu.findItem(R.id.filter_recently_played).isChecked =
true
R.id.filter_recently_added -> popup.menu.findItem(R.id.filter_recently_added).isChecked =
true
}
popup.setOnMenuItemClickListener { item ->
currentFilter = item.itemId
preferences.edit { putInt(PREF_SORT_TYPE, currentFilter) }
filterAndSearch()
true
}
popup.show()
}
// Track current filter
private var currentFilter = View.NO_ID
private fun filterAndSearch(baseList: List<Game> = gamesViewModel.games.value) {
val filteredList: List<Game> = when (currentFilter) {
R.id.alphabetical -> baseList.sortedBy { it.title }
R.id.filter_recently_played -> {
baseList.filter {
val lastPlayedTime = preferences.getLong(it.keyLastPlayedTime, 0L)
lastPlayedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyLastPlayedTime, 0L) }
}
R.id.filter_recently_added -> {
baseList.filter {
val addedTime = preferences.getLong(it.keyAddedToLibraryTime, 0L)
addedTime > (System.currentTimeMillis() - 24 * 60 * 60 * 1000)
}.sortedByDescending { preferences.getLong(it.keyAddedToLibraryTime, 0L) }
}
else -> baseList
}
val searchTerm = binding.searchText.text.toString().lowercase(Locale.getDefault())
if (searchTerm.isEmpty()) {
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(
filteredList
)
gamesViewModel.setFilteredGames(filteredList)
return
}
val searchAlgorithm = if (searchTerm.length > 1) Jaccard(2) else JaroWinkler()
val sortedList = filteredList.mapNotNull { game ->
val title = game.title.lowercase(Locale.getDefault())
val score = searchAlgorithm.similarity(searchTerm, title)
if (score > 0.03) {
ScoredGame(score, game)
} else {
null
}
}.sortedByDescending { it.score }.map { it.item }
((binding.gridGames as? RecyclerView)?.adapter as? GameAdapter)?.submitList(sortedList)
gamesViewModel.setFilteredGames(sortedList)
}
private inner class ScoredGame(val score: Double, val item: Game)
private fun focusSearch() {
binding.searchText.requestFocus()
val imm = requireActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager?
imm?.showSoftInput(binding.searchText, InputMethodManager.SHOW_IMPLICIT)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun scrollToTop() {
if (_binding != null) {
(binding.gridGames as? CarouselRecyclerView)?.smoothScrollToPosition(0)
}
}
private fun launchQLaunch() {
try {
val appletPath = NativeLibrary.getAppletLaunchPath(AppletInfo.QLaunch.entryId)
if (appletPath.isEmpty()) {
Toast.makeText(
requireContext(),
R.string.applets_error_applet,
Toast.LENGTH_SHORT
).show()
return
}
NativeLibrary.setCurrentAppletId(AppletInfo.QLaunch.appletId)
val qlaunchGame = Game(
title = getString(R.string.qlaunch_applet),
path = appletPath
)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(qlaunchGame)
findNavController().navigate(action)
} catch (e: Exception) {
Toast.makeText(
requireContext(),
"Failed to launch QLaunch: ${e.message}",
Toast.LENGTH_SHORT
).show()
}
}
private fun updateButtonsVisibility() {
val showQLaunch = BooleanSetting.ENABLE_QLAUNCH_BUTTON.getBoolean()
val showFolder = BooleanSetting.ENABLE_FOLDER_BUTTON.getBoolean()
val isFirmwareAvailable = NativeLibrary.isFirmwareAvailable()
val shouldShowQLaunch = showQLaunch && isFirmwareAvailable
binding.launchQlaunch.visibility = if (shouldShowQLaunch) View.VISIBLE else View.GONE
binding.addDirectory.visibility = if (showFolder) View.VISIBLE else View.GONE
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val spacingNavigation = resources.getDimensionPixelSize(R.dimen.spacing_navigation)
resources.getDimensionPixelSize(R.dimen.spacing_navigation_rail)
(binding.swipeRefresh as? SwipeRefreshLayout)?.setProgressViewEndTarget(
false,
barInsets.top + resources.getDimensionPixelSize(R.dimen.spacing_refresh_end)
)
val leftInset = barInsets.left + cutoutInsets.left
val rightInset = barInsets.right + cutoutInsets.right
val topInset = maxOf(barInsets.top, cutoutInsets.top)
val mlpSwipe = binding.swipeRefresh.layoutParams as ViewGroup.MarginLayoutParams
mlpSwipe.leftMargin = leftInset
mlpSwipe.rightMargin = rightInset
binding.swipeRefresh.layoutParams = mlpSwipe
val mlpHeader = binding.header.layoutParams as ViewGroup.MarginLayoutParams
// Store original margins only once
if (originalHeaderTopMargin == null) {
originalHeaderTopMargin = mlpHeader.topMargin
originalHeaderRightMargin = mlpHeader.rightMargin
originalHeaderLeftMargin = mlpHeader.leftMargin
}
// Always set margin as original + insets
mlpHeader.leftMargin = (originalHeaderLeftMargin ?: 0) + leftInset
mlpHeader.rightMargin = (originalHeaderRightMargin ?: 0) + rightInset
mlpHeader.topMargin = (originalHeaderTopMargin ?: 0) + topInset + resources.getDimensionPixelSize(
R.dimen.spacing_med
)
binding.header.layoutParams = mlpHeader
binding.noticeText.updatePadding(bottom = spacingNavigation)
binding.gridGames.updatePadding(
top = resources.getDimensionPixelSize(R.dimen.spacing_med)
)
val mlpFab = binding.addDirectory.layoutParams as ViewGroup.MarginLayoutParams
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
mlpFab.leftMargin = leftInset + fabPadding
mlpFab.bottomMargin = barInsets.bottom + fabPadding
mlpFab.rightMargin = rightInset + fabPadding
binding.addDirectory.layoutParams = mlpFab
binding.launchQlaunch?.let { qlaunchButton ->
val mlpQLaunch = qlaunchButton.layoutParams as ViewGroup.MarginLayoutParams
mlpQLaunch.leftMargin = leftInset + fabPadding
mlpQLaunch.bottomMargin = barInsets.bottom + fabPadding
qlaunchButton.layoutParams = mlpQLaunch
}
val navInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
val gestureInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures())
val bottomInset = maxOf(navInsets.bottom, gestureInsets.bottom, cutoutInsets.bottom)
fallbackBottomInset = bottomInset
(binding.gridGames as? CarouselRecyclerView)?.notifyInsetsReady(bottomInset)
windowInsets
}
}
@@ -124,6 +124,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
WindowCompat.setDecorFitsSystemWindows(window, false)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
// Hide status bar and navigation bar for fullscreen experience
// Maybe a setting? Reminds me: SafeArea Testing!
WindowCompat.getInsetsController(window, window.decorView).apply {
hide(WindowInsetsCompat.Type.statusBars())
hide(WindowInsetsCompat.Type.navigationBars())
systemBarsBehavior = androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
window.statusBarColor =
ContextCompat.getColor(applicationContext, android.R.color.transparent)
window.navigationBarColor =
@@ -1,168 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.utils
import android.app.Activity
import android.app.Application
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.View
import android.view.Window
import androidx.activity.ComponentActivity
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import java.util.concurrent.atomic.AtomicBoolean
object ControllerNavigationGlobalHook {
private val installed = AtomicBoolean(false)
fun install(application: Application) {
if (!installed.compareAndSet(false, true)) {
return
}
application.registerActivityLifecycleCallbacks(HookInstaller)
}
private object HookInstaller : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
installHookIfNeeded(activity)
}
override fun onActivityResumed(activity: Activity) {
installHookIfNeeded(activity)
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit
}
private fun installHookIfNeeded(activity: Activity) {
val window = activity.window ?: return
val currentCallback = window.callback ?: return
if (currentCallback is ControllerNavigationWindowCallback) {
return
}
window.callback = ControllerNavigationWindowCallback(activity, currentCallback)
}
private class ControllerNavigationWindowCallback(
private val activity: Activity,
private val delegate: Window.Callback
) : Window.Callback by delegate {
private val componentActivity = activity as? ComponentActivity
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (activity.isFinishing || activity.isDestroyed) {
return delegate.dispatchKeyEvent(event)
}
if (!BooleanSetting.INVERT_CONFIRM_BACK_CONTROLLER_BUTTONS.getBoolean()) {
return delegate.dispatchKeyEvent(event)
}
if (!isControllerInput(event) || componentActivity == null) {
return delegate.dispatchKeyEvent(event)
}
if (shouldBypassInGameplay()) {
return delegate.dispatchKeyEvent(event)
}
if (isConfirmAction(event.keyCode)) {
return when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (event.repeatCount == 0) {
componentActivity.onBackPressedDispatcher.onBackPressed()
}
true
}
KeyEvent.ACTION_UP -> true
else -> false
}
}
if (isBackAction(event.keyCode)) {
val remappedEvent = KeyEvent(
event.downTime,
event.eventTime,
event.action,
KeyEvent.KEYCODE_DPAD_CENTER,
event.repeatCount,
event.metaState,
event.deviceId,
event.scanCode,
event.flags,
event.source
)
return delegate.dispatchKeyEvent(remappedEvent)
}
return delegate.dispatchKeyEvent(event)
}
private fun shouldBypassInGameplay(): Boolean {
if (activity.javaClass.name != "org.yuzu.yuzu_emu.activities.EmulationActivity") {
return false
}
if (!NativeLibrary.isRunning()) {
return false
}
val surface = activity.findViewById<View?>(R.id.surface_emulation) ?: return false
if (!surface.isShown || surface.visibility != View.VISIBLE) {
return false
}
val focused = activity.currentFocus
if (focused != null) {
val inputOverlay = activity.findViewById<View?>(R.id.surface_input_overlay)
if (!isDescendantOf(focused, inputOverlay)) {
return false
}
}
return true
}
private fun isDescendantOf(view: View, parent: View?): Boolean {
if (parent == null) {
return false
}
var current: View? = view
while (current != null) {
if (current === parent) {
return true
}
current = current.parent as? View
}
return false
}
private fun isControllerInput(event: KeyEvent): Boolean {
val source = event.source
val deviceSources = event.device?.sources ?: InputDevice.getDevice(event.deviceId)?.sources ?: 0
return hasControllerSource(source) || hasControllerSource(deviceSources)
}
private fun isConfirmAction(keyCode: Int): Boolean {
return keyCode == KeyEvent.KEYCODE_BUTTON_A
}
private fun isBackAction(keyCode: Int): Boolean {
return keyCode == KeyEvent.KEYCODE_BUTTON_B
}
private fun hasControllerSource(source: Int): Boolean {
return source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD ||
source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK ||
source and InputDevice.SOURCE_DPAD == InputDevice.SOURCE_DPAD
}
}
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -25,31 +28,29 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
class GameIconFetcher(
class GameObjectIconFetcher(
private val game: Game,
private val options: Options
) : Fetcher {
override suspend fun fetch(): FetchResult {
val bitmap = decodeGameIcon(game.path)
?: throw IllegalStateException("Failed to decode game icon for: ${game.title}")
return DrawableResult(
drawable = decodeGameIcon(game.path)!!.toDrawable(options.context.resources),
drawable = bitmap.toDrawable(options.context.resources),
isSampled = false,
dataSource = DataSource.DISK
)
}
private fun decodeGameIcon(uri: String): Bitmap? {
val data = GameMetadata.getIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
private fun decodeGameIcon(path: String): Bitmap? {
val iconBytes = GameMetadata.getIcon(path)
return BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
}
class Factory : Fetcher.Factory<Game> {
override fun create(data: Game, options: Options, imageLoader: ImageLoader): Fetcher =
GameIconFetcher(data, options)
GameObjectIconFetcher(data, options)
}
}
@@ -61,7 +62,7 @@ object GameIconUtils {
private val imageLoader = ImageLoader.Builder(YuzuApplication.appContext)
.components {
add(GameIconKeyer())
add(GameIconFetcher.Factory())
add(GameObjectIconFetcher.Factory())
}
.memoryCache {
MemoryCache.Builder(YuzuApplication.appContext)
@@ -99,9 +100,6 @@ object GameIconUtils {
R.id.shortcut_foreground,
getGameIcon(lifecycleOwner, game).toDrawable(YuzuApplication.appContext.resources)
)
val inset = YuzuApplication.appContext.resources
.getDimensionPixelSize(R.dimen.icon_inset)
layerDrawable.setLayerInset(1, inset, inset, inset, inset)
return IconCompat.createWithAdaptiveBitmap(
layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888)
)
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -74,7 +74,7 @@ class GradientBorderCardView @JvmOverloads constructor(
borderPaint.shader = null
val typedValue = android.util.TypedValue()
context.theme.resolveAttribute(
com.google.android.material.R.attr.colorPrimary,
androidx.appcompat.R.attr.colorPrimary,
typedValue,
true
)
@@ -65,8 +65,6 @@ namespace AndroidSettings {
Settings::Category::Android};
Settings::Setting<bool> enable_qlaunch_button{linkage, false, "enable_qlaunch_button",
Settings::Category::Android};
Settings::Setting<bool> invert_confirm_back_controller_buttons{
linkage, false, "invert_confirm_back_controller_buttons", Settings::Category::Android};
// Input/performance overlay settings
std::vector<OverlayControlData> overlay_control_data;
-33
View File
@@ -1233,39 +1233,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_getDebugKnobAt(JNIEnv* env, jobje
return static_cast<jboolean>(Settings::getDebugKnobAt(static_cast<u8>(index)));
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setTurboSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
if (enabled) {
Settings::values.use_speed_limit.SetValue(true);
Settings::SetSpeedMode(Settings::SpeedMode::Turbo);
} else {
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setSlowSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
if (enabled) {
Settings::values.use_speed_limit.SetValue(true);
Settings::SetSpeedMode(Settings::SpeedMode::Slow);
} else {
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
}
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_setStandardSpeedLimit(JNIEnv *env, jobject jobj, jboolean enabled) {
Settings::values.use_speed_limit.SetValue(enabled);
if (enabled) {
Settings::SetSpeedMode(Settings::SpeedMode::Standard);
}
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isTurboMode(JNIEnv *env, jobject jobj) {
return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Turbo;
}
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isSlowMode(JNIEnv *env, jobject jobj) {
return Settings::values.current_speed_mode.GetValue() == Settings::SpeedMode::Slow;
}
void Java_org_yuzu_yuzu_1emu_NativeLibrary_run(JNIEnv* env, jobject jobj, jstring j_path,
jint j_program_index,
jboolean j_frontend_initiated) {
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M3,18h6v-2H3v2zM3,6v2h18V6H3zm0,7h12v-2H3v2z"/>
</vector>
@@ -1,96 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_enable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="@string/gpu_unswizzle_enable" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_texture_size"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/gpu_unswizzle_texture_size">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_texture_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_stream_size"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:hint="@string/gpu_unswizzle_stream_size">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_stream_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_chunk_size"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/gpu_unswizzle_chunk_size">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/dropdown_chunk_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal"
android:gravity="end"
android:spacing="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_default"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/gpu_unswizzle_default_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_cancel"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/cancel" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_ok"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@android:string/ok" />
</LinearLayout>
</LinearLayout>
</ScrollView>
@@ -8,7 +8,7 @@
<fragment
android:id="@+id/gamesFragment"
android:name="org.yuzu.yuzu_emu.ui.GamesFragment"
android:label="PlatformGamesFragment" />
android:label="PlatformGamesFragment">
<action
android:id="@+id/action_gamesFragment_to_homeSettingsFragment"
app:destination="@id/homeSettingsFragment"
@@ -16,6 +16,14 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_gamesFragment_to_profileManagerFragment"
app:destination="@id/profileManagerFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/homeSettingsFragment"
@@ -158,6 +166,9 @@
<action
android:id="@+id/action_global_perGamePropertiesFragment"
app:destination="@id/perGamePropertiesFragment" />
<action
android:id="@+id/action_global_gameInfoFragment"
app:destination="@id/gameInfoFragment" />
<fragment
android:id="@+id/gameInfoFragment"
android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
@@ -229,6 +229,10 @@
<string name="update_install_failed">Installieren des Updates fehlgeschlagen: %1$s</string>
<string name="home_search">Suche</string>
<string name="home_settings">Einstellungen</string>
<string name="home_sort">Sortieren</string>
<string name="home_layout">Layout</string>
<string name="home_start">Start</string>
<string name="home_clear">Löschen</string>
<string name="empty_gamelist">Es wurden keine Dateien gefunden oder es wurde noch kein Spielverzeichnis ausgewählt.</string>
<string name="manage_game_folders">Spiele-Ordner verwalten</string>
<string name="select_games_folder_description">Erlaubt Eden die Spieleliste zu füllen</string>
@@ -705,6 +709,11 @@ Wird der Handheld-Modus verwendet, verringert es die Auflösung und erhöht die
<string name="path_set">Pfad erfolgreich gesetzt</string>
<string name="skip_migration">Überspringen</string>
<!-- Footer actions -->
<string name="footer_open">Öffnen</string>
<string name="footer_game_info">Spielinfo</string>
<string name="footer_back">Zurück</string>
<!-- Game properties -->
<string name="info">Info</string>
<string name="info_description">Programm-ID, Entwickler, Version</string>
@@ -530,21 +530,6 @@
<item>2</item>
</integer-array>
<string-array name="framePacingModeNames">
<item>@string/frame_pacing_mode_target_Auto</item>
<item>@string/frame_pacing_mode_target_30</item>
<item>@string/frame_pacing_mode_target_60</item>
<item>@string/frame_pacing_mode_target_90</item>
<item>@string/frame_pacing_mode_target_120</item>
</string-array>
<integer-array name="framePacingModeValues">
<item>0</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
</integer-array>
<string-array name="appletEntries">
<item>@string/applet_hle</item>
<item>@string/applet_lle</item>
+10 -21
View File
@@ -36,8 +36,6 @@
<string name="enable_input_overlay_auto_hide">Enable Overlay Auto Hide</string>
<string name="hide_overlay_on_controller_input">Hide Overlay on Controller Input</string>
<string name="hide_overlay_on_controller_input_description">Automatically hide the touch controls overlay when a physical controller is used. Overlay reappears when controller is disconnected.</string>
<string name="invert_confirm_back_controller_buttons">Invert Confirm/Back Controller Buttons</string>
<string name="invert_confirm_back_controller_buttons_description">Swap Android Confirm and Back button handling to match both Switch and Xbox styles while using the app UI.</string>
<string name="input_overlay_options">Input Overlay</string>
<string name="input_overlay_options_description">Configure on-screen controls</string>
@@ -242,6 +240,10 @@
<string name="update_install_failed">Failed to install update: %1$s</string>
<string name="home_search">Search</string>
<string name="home_settings">Settings</string>
<string name="home_sort">Sort</string>
<string name="home_layout">Layout</string>
<string name="home_start">Start</string>
<string name="home_clear">Clear</string>
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
<string name="manage_game_folders">Manage game folders</string>
<string name="select_games_folder_description">Allows Eden to populate the games list</string>
@@ -424,10 +426,6 @@
<string name="frame_limit_enable_description">Limits emulation speed to a specified percentage of normal speed.</string>
<string name="frame_limit_slider">Limit speed percent</string>
<string name="frame_limit_slider_description">Specifies the percentage to limit emulation speed. 100% is the normal speed. Values higher or lower will increase or decrease the speed limit.</string>
<string name="turbo_speed_limit">Turbo speed</string>
<string name="turbo_speed_limit_description">When Turbo Mode is enabled, emulation will run at this speed.</string>
<string name="slow_speed_limit">Slow speed</string>
<string name="slow_speed_limit_description">When Slow Mode is enabled, emulation will run at this speed.</string>
<string name="cpu_backend">CPU backend</string>
<string name="cpu_accuracy">CPU accuracy</string>
<string name="value_with_units">%1$s%2$s</string>
@@ -479,8 +477,6 @@
<string name="renderer_accuracy_description">Controls the GPU emulation mode. Most games render fine with Fast or Balanced modes, but Accurate is still required for some. Particles tend to only render correctly with Accurate mode.</string>
<string name="dma_accuracy">DMA Accuracy</string>
<string name="dma_accuracy_description">Controls the DMA precision accuracy. Safe precision can fix issues in some games, but it can also impact performance in some cases. If unsure, leave this on Default.</string>
<string name="frame_pacing_mode">Frame Pacing Mode</string>
<string name="frame_pacing_mode_description">Controls how the emulator manages frame pacing to reduce stuttering and make the frame rate smoother and more consistent.</string>
<string name="anisotropic_filtering">Anisotropic filtering</string>
<string name="anisotropic_filtering_description">Improves the quality of textures when viewed at oblique angles</string>
<string name="vram_usage_mode">VRAM Usage Mode</string>
@@ -509,20 +505,15 @@
<string name="skip_cpu_inner_invalidation">Skip CPU Inner Invalidation</string>
<string name="skip_cpu_inner_invalidation_description">Skips certain CPU-side cache invalidations during memory updates, reducing CPU usage and improving it\'s performance. This may cause glitches or crashes on some games.</string>
<string name="fix_bloom_effects">Fix Bloom Effects</string>
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno 700), removes bloom in Burnout. Warning: may cause graphical artifacts in other games.</string>
<string name="fix_bloom_effects_description">Reduces bloom blur in LA/EOW (Adreno 700), removes bloom in Burnout</string>
<string name="renderer_asynchronous_shaders">Use asynchronous shaders</string>
<string name="renderer_asynchronous_shaders_description">Compiles shaders asynchronously. This may reduce stutters but may also introduce glitches.</string>
<string name="gpu_unswizzle_settings">GPU Unswizzle Settings</string>
<string name="gpu_unswizzle_settings_description">Configure GPU-based texture unswizzling parameters or disable it entirely. Adjust these settings to balance performance and texture loading quality.</string>
<string name="gpu_unswizzle_enable">Enable GPU Unswizzle</string>
<string name="gpu_unswizzle_disabled">Disabled</string>
<string name="gpu_unswizzle_texture_size">GPU Unswizzle Max Texture Size</string>
<string name="gpu_unswizzle_texture_size_description">Sets the maximum size (MB) for GPU-based texture unswizzling. While the GPU is faster for medium and large textures, the CPU may be more efficient for very small ones. Adjust this to find the balance between GPU acceleration and CPU overhead.</string>
<string name="gpu_unswizzle_stream_size">GPU Unswizzle Stream Size</string>
<string name="gpu_unswizzle_stream_size_description">Sets the data limit per frame for unswizzling large textures. Higher values speed up texture loading at the cost of higher frame latency; lower values reduce GPU overhead but may cause visible texture pop-in.</string>
<string name="gpu_unswizzle_chunk_size">GPU Unswizzle Chunk Size</string>
<string name="gpu_unswizzle_chunk_size_description">Defines the number of depth slices processed per batch for 3D textures. Increasing this improves throughput efficiency on powerful GPUs but may cause stuttering or driver timeouts on weaker hardware.</string>
<string name="gpu_unswizzle_default_button">Default</string>
<string name="extensions">Extensions</string>
@@ -779,6 +770,11 @@
<string name="path_set">Path set successfully</string>
<string name="skip_migration">Skip</string>
<!-- Footer actions -->
<string name="footer_open">Open</string>
<string name="footer_game_info">Game Info</string>
<string name="footer_back">Back</string>
<!-- Game properties -->
<string name="info">Info</string>
<string name="info_description">Program ID, developer, version</string>
@@ -1036,13 +1032,6 @@
<string name="dma_accuracy_unsafe">Unsafe</string>
<string name="dma_accuracy_safe">Safe</string>
<!-- Frame Pacing Mode -->
<string name="frame_pacing_mode_target_Auto">Auto</string>
<string name="frame_pacing_mode_target_30">30 FPS</string>
<string name="frame_pacing_mode_target_60">60 FPS</string>
<string name="frame_pacing_mode_target_90">90 FPS</string>
<string name="frame_pacing_mode_target_120">120 FPS</string>
<!-- ASTC Decoding Method Choices -->
<string name="accelerate_astc_cpu" translatable="false">CPU</string>
<string name="accelerate_astc_gpu" translatable="false">GPU</string>
+5 -1
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-License-Identifier: GPL-3.0-or-later
@@ -5,7 +8,8 @@
plugins {
id("com.android.application") version "8.9.1" apply false
id("com.android.library") version "8.1.4" apply false
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
id("org.jetbrains.kotlin.android") version "2.3.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.0" apply false
}
tasks.register("clean").configure {
-2
View File
@@ -186,8 +186,6 @@ if(ARCHITECTURE_x86_64)
common
PRIVATE x64/cpu_detect.cpp
x64/cpu_detect.h
x64/cpu_wait.cpp
x64/cpu_wait.h
x64/native_clock.cpp
x64/native_clock.h
x64/rdtsc.cpp
+1 -2
View File
@@ -25,8 +25,7 @@ void AssertFailSoftImpl();
#define ASSERT_MSG(_a_, ...) \
([&]() YUZU_NO_INLINE { \
auto&& assert_condition = (_a_); \
if (!(assert_condition)) [[unlikely]] { \
if (!(_a_)) [[unlikely]] { \
LOG_CRITICAL(Debug, __FILE__ ": assert " __VA_ARGS__); \
AssertFailSoftImpl(); \
} \
+26 -1
View File
@@ -219,6 +219,11 @@ public:
}
}
bool ClearBackingRegion(size_t physical_offset, size_t length) {
// TODO: This does not seem to be possible on Windows.
return false;
}
void EnableDirectMappedAddress() {
// TODO
UNREACHABLE();
@@ -612,6 +617,24 @@ public:
ASSERT_MSG(ret == 0, "mprotect failed: {}", strerror(errno));
}
bool ClearBackingRegion(size_t physical_offset, size_t length) {
#ifdef __linux__
// Only incur syscall cost IF memset would be slower (theshold = 16MiB)
// TODO(lizzie): Smarter way to dynamically get this threshold (broadwell != raptor lake) for example
if (length >= 2097152UL * 8) {
// Set MADV_REMOVE on backing map to destroy it instantly.
// This also deletes the area from the backing file.
int ret = madvise(backing_base + physical_offset, length, MADV_REMOVE);
ASSERT_MSG(ret == 0, "madvise failed: {}", strerror(errno));
return true;
} else {
return false;
}
#else
return false;
#endif
}
void EnableDirectMappedAddress() {
virtual_base = nullptr;
}
@@ -725,7 +748,9 @@ void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission
}
void HostMemory::ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value) {
std::memset(backing_base + physical_offset, fill_value, length);
if (!impl || fill_value != 0 || !impl->ClearBackingRegion(physical_offset, length)) {
std::memset(backing_base + physical_offset, fill_value, length);
}
}
void HostMemory::EnableDirectMappedAddress() {
-2
View File
@@ -20,7 +20,6 @@
#define COMPILER_ID "@CXX_COMPILER@"
#define BUILD_AUTO_UPDATE_WEBSITE "@BUILD_AUTO_UPDATE_WEBSITE@"
#define BUILD_AUTO_UPDATE_API "@BUILD_AUTO_UPDATE_API@"
#define BUILD_AUTO_UPDATE_API_PATH "@BUILD_AUTO_UPDATE_API_PATH@"
#define BUILD_AUTO_UPDATE_REPO "@BUILD_AUTO_UPDATE_REPO@"
#define IS_NIGHTLY_BUILD @IS_NIGHTLY_BUILD@
@@ -43,7 +42,6 @@ constexpr const bool g_is_nightly_build = IS_NIGHTLY_BUILD;
constexpr const char g_build_auto_update_website[] = BUILD_AUTO_UPDATE_WEBSITE;
constexpr const char g_build_auto_update_api[] = BUILD_AUTO_UPDATE_API;
constexpr const char g_build_auto_update_api_path[] = BUILD_AUTO_UPDATE_API_PATH;
constexpr const char g_build_auto_update_repo[] = BUILD_AUTO_UPDATE_REPO;
} // namespace Common
-1
View File
@@ -26,7 +26,6 @@ extern const bool g_is_nightly_build;
extern const char g_build_auto_update_website[];
extern const char g_build_auto_update_api[];
extern const char g_build_auto_update_api_path[];
extern const char g_build_auto_update_repo[];
} // namespace Common
-48
View File
@@ -379,52 +379,4 @@ void SetConfiguringGlobal(bool is_global) {
configuring_global = is_global;
}
u16 SpeedLimit() {
switch (SpeedMode(values.current_speed_mode)) {
case SpeedMode::Standard:
return values.speed_limit.GetValue();
case SpeedMode::Turbo:
return values.turbo_speed_limit.GetValue();
case SpeedMode::Slow:
return values.slow_speed_limit.GetValue();
default:
UNIMPLEMENTED();
}
return 100;
}
void SetSpeedMode(const SpeedMode& mode) {
values.current_speed_mode.SetValue(mode);
switch (mode) {
case SpeedMode::Turbo:
case SpeedMode::Slow:
values.use_speed_limit.SetValue(true);
break;
case SpeedMode::Standard:
default:
break;
}
}
void ToggleStandardMode() {
values.use_speed_limit.SetValue(!values.use_speed_limit.GetValue());
SetSpeedMode(SpeedMode::Standard);
}
void ToggleTurboMode() {
if (values.current_speed_mode.GetValue() != SpeedMode::Turbo)
SetSpeedMode(SpeedMode::Turbo);
else
SetSpeedMode(SpeedMode::Standard);
}
void ToggleSlowMode() {
if (values.current_speed_mode.GetValue() != SpeedMode::Slow)
SetSpeedMode(SpeedMode::Slow);
else
SetSpeedMode(SpeedMode::Standard);
}
} // namespace Settings
+14 -51
View File
@@ -204,7 +204,6 @@ struct Values {
true};
SwitchableSetting<bool> use_speed_limit{
linkage, true, "use_speed_limit", Category::Core, Specialization::Paired, true, true};
SwitchableSetting<u16, true> speed_limit{linkage,
100,
0,
@@ -215,30 +214,6 @@ struct Values {
true,
true,
&use_speed_limit};
SwitchableSetting<u16, true> slow_speed_limit{linkage,
50,
0,
9999,
"slow_speed_limit",
Category::Core,
Specialization::Countable | Specialization::Percentage,
true,
true};
SwitchableSetting<u16, true> turbo_speed_limit{linkage,
200,
0,
9999,
"turbo_speed_limit",
Category::Core,
Specialization::Countable | Specialization::Percentage,
true,
true};
// The currently used speed mode.
Setting<SpeedMode> current_speed_mode{linkage, SpeedMode::Standard, "current_speed_mode", Category::Core, Specialization::Default, false, true};
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core,
Specialization::Default};
@@ -459,16 +434,6 @@ struct Values {
"accelerate_astc",
Category::RendererAdvanced};
SwitchableSetting<FramePacingMode, true> frame_pacing_mode{linkage,
FramePacingMode::Target_Auto,
FramePacingMode::Target_Auto,
FramePacingMode::Target_120,
"frame_pacing_mode",
Category::RendererAdvanced,
Specialization::Default,
true,
true};
SwitchableSetting<AstcRecompression, true> astc_recompression{linkage,
AstcRecompression::Uncompressed,
"astc_recompression",
@@ -567,11 +532,14 @@ struct Values {
Category::RendererHacks,
Specialization::Default};
SwitchableSetting<bool> gpu_unswizzle_enabled{linkage, false, "gpu_unswizzle_enabled",
Category::RendererHacks};
SwitchableSetting<ExtendedDynamicState> dyna_state{linkage,
#if defined (ANDROID) || defined (__APPLE__)
#if defined (_WIN32)
ExtendedDynamicState::EDS3,
#elif defined (__FreeBSD__)
ExtendedDynamicState::EDS3,
#elif defined (ANDROID)
ExtendedDynamicState::Disabled,
#elif defined (__APPLE__)
ExtendedDynamicState::Disabled,
#else
ExtendedDynamicState::EDS2,
@@ -623,11 +591,11 @@ struct Values {
"language_index",
Category::System};
SwitchableSetting<Region, true> region_index{linkage, Region::Usa, "region_index", Category::System};
SwitchableSetting<TimeZone, true> time_zone_index{linkage, TimeZone::Auto, "time_zone_index", Category::System};
Setting<u32> serial_battery{linkage, 0, "serial_battery", Category::System};
Setting<u32> serial_unit{linkage, 0, "serial_unit", Category::System};
SwitchableSetting<TimeZone, true> time_zone_index{linkage, TimeZone::Auto,
"time_zone_index", Category::System};
// Measured in seconds since epoch
SwitchableSetting<bool> custom_rtc_enabled{linkage, false, "custom_rtc_enabled", Category::System, Specialization::Paired, true, true};
SwitchableSetting<bool> custom_rtc_enabled{
linkage, false, "custom_rtc_enabled", Category::System, Specialization::Paired, true, true};
SwitchableSetting<s64> custom_rtc{
linkage, 0, "custom_rtc", Category::System, Specialization::Time,
false, true, &custom_rtc_enabled};
@@ -791,12 +759,14 @@ struct Values {
0,
65535,
"debug_knobs",
Category::Core,
Category::Debugging,
Specialization::Countable,
true,
true};
// Miscellaneous
Setting<std::string> serial_battery{linkage, std::string(), "serial_battery", Category::Miscellaneous};
Setting<std::string> serial_unit{linkage, std::string(), "serial_unit", Category::Miscellaneous};
Setting<std::string> log_filter{linkage, "*:Info", "log_filter", Category::Miscellaneous};
Setting<bool> log_flush_line{linkage, false, "flush_line", Category::Miscellaneous, Specialization::Default, true, true};
Setting<bool> censor_username{linkage, true, "censor_username", Category::Miscellaneous};
@@ -844,13 +814,6 @@ bool IsDockedMode();
float Volume();
// speed limit ops
u16 SpeedLimit();
void SetSpeedMode(const SpeedMode &mode);
void ToggleStandardMode();
void ToggleTurboMode();
void ToggleSlowMode();
std::string GetTimeZoneString(TimeZone time_zone);
void LogSettings();
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -162,7 +162,7 @@ public:
/**
* @returns A unique identifier for the Setting's internal data type.
*/
[[nodiscard]] virtual std::string_view TypeId() const = 0;
[[nodiscard]] virtual std::type_index TypeId() const = 0;
/**
* Returns true if the Setting's internal data type is an enum.
-2
View File
@@ -129,7 +129,6 @@ ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt
ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16, X32, X64, None);
ENUM(AstcDecodeMode, Cpu, Gpu, CpuAsynchronous);
ENUM(AstcRecompression, Uncompressed, Bc1, Bc3);
ENUM(FramePacingMode, Target_Auto, Target_30, Target_60, Target_90, Target_120);
ENUM(VSyncMode, Immediate, Mailbox, Fifo, FifoRelaxed);
ENUM(VramUsageMode, Conservative, Aggressive);
ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV);
@@ -157,7 +156,6 @@ ENUM(TemperatureUnits, Celsius, Fahrenheit)
ENUM(ExtendedDynamicState, Disabled, EDS1, EDS2, EDS3);
ENUM(GpuLogLevel, Off, Errors, Standard, Verbose, All)
ENUM(GameListMode, TreeView, GridView);
ENUM(SpeedMode, Standard, Turbo, Slow);
template <typename Type>
inline std::string_view CanonicalizeEnum(Type id) {
+3 -9
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -220,14 +220,8 @@ public:
*
* @returns the type_index of the setting's type
*/
[[nodiscard]] std::string_view TypeId() const override final {
if constexpr (std::is_same_v<Type, std::string>) {
return "string";
} else if constexpr (std::is_same_v<Type, bool>) {
return "bool";
} else {
return "other";
}
[[nodiscard]] std::type_index TypeId() const override final {
return std::type_index(typeid(Type));
}
[[nodiscard]] constexpr u32 EnumIndex() const override final {
-75
View File
@@ -1,75 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <thread>
#ifdef _MSC_VER
#include <intrin.h>
#endif
#include "common/x64/cpu_detect.h"
#include "common/x64/cpu_wait.h"
#include "common/x64/rdtsc.h"
namespace Common::X64 {
namespace {
// 100,000 cycles is a reasonable amount of time to wait to save on CPU resources.
// For reference:
// At 1 GHz, 100K cycles is 100us
// At 2 GHz, 100K cycles is 50us
// At 4 GHz, 100K cycles is 25us
constexpr auto PauseCycles = 100'000U;
} // Anonymous namespace
#if defined(_MSC_VER) && !defined(__clang__)
__forceinline static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
_tpause(RequestC02State, FencedRDTSC() + PauseCycles);
}
__forceinline static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
_mm_monitorx(&monitor_var, 0, 0);
_mm_mwaitx(EnableWaitTimeFlag, RequestC1State, PauseCycles);
}
#else
static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
const auto tsc = FencedRDTSC() + PauseCycles;
const auto eax = static_cast<u32>(tsc & 0xFFFFFFFF);
const auto edx = static_cast<u32>(tsc >> 32);
asm volatile("tpause %0" : : "r"(RequestC02State), "d"(edx), "a"(eax));
}
static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
asm volatile("monitorx" : : "a"(&monitor_var), "c"(0), "d"(0));
asm volatile("mwaitx" : : "a"(RequestC1State), "b"(PauseCycles), "c"(EnableWaitTimeFlag));
}
#endif
void MicroSleep() {
static const bool has_waitpkg = GetCPUCaps().waitpkg;
static const bool has_monitorx = GetCPUCaps().monitorx;
if (has_waitpkg) {
TPAUSE();
} else if (has_monitorx) {
MWAITX();
} else {
std::this_thread::yield();
}
}
} // namespace Common::X64
-10
View File
@@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
namespace Common::X64 {
void MicroSleep();
} // namespace Common::X64
+23 -19
View File
@@ -1219,9 +1219,12 @@ target_link_libraries(core PRIVATE
fmt::fmt
nlohmann_json::nlohmann_json
RenderDoc::API
ZLIB::ZLIB)
MbedTLS::mbedcrypto${MBEDTLS_LIB_SUFFIX}
MbedTLS::mbedtls${MBEDTLS_LIB_SUFFIX})
target_link_libraries(core PRIVATE httplib::httplib)
if (ENABLE_WEB_SERVICE OR ENABLE_OPENSSL)
target_link_libraries(core PRIVATE httplib::httplib)
endif()
if (ENABLE_WEB_SERVICE)
target_compile_definitions(core PUBLIC ENABLE_WEB_SERVICE)
@@ -1267,24 +1270,25 @@ if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
target_link_libraries(core PRIVATE dynarmic::dynarmic)
endif()
target_sources(core PRIVATE hle/service/ssl/ssl_backend_openssl.cpp)
if(ENABLE_OPENSSL)
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_openssl.cpp)
target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(core PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
find_package(OpenSSL REQUIRED)
# TODO
# elseif (APPLE)
# target_sources(core PRIVATE
# hle/service/ssl/ssl_backend_securetransport.cpp)
# target_link_libraries(core PRIVATE "-framework Security")
# elseif (WIN32)
# target_sources(core PRIVATE
# hle/service/ssl/ssl_backend_schannel.cpp)
# target_link_libraries(core PRIVATE crypt32 secur32)
# else()
# target_sources(core PRIVATE
# hle/service/ssl/ssl_backend_none.cpp)
# endif()
target_link_libraries(core PRIVATE OpenSSL::SSL OpenSSL::Crypto)
target_compile_definitions(core PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT)
elseif (APPLE)
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_securetransport.cpp)
target_link_libraries(core PRIVATE "-framework Security")
elseif (WIN32)
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_schannel.cpp)
target_link_libraries(core PRIVATE crypt32 secur32)
else()
target_sources(core PRIVATE
hle/service/ssl/ssl_backend_none.cpp)
endif()
create_target_directory_groups(core)
+9 -8
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -38,16 +38,10 @@ u64 DynarmicCallbacks32::MemoryRead64(u32 vaddr) {
CheckMemoryAccess(vaddr, 8, Kernel::DebugWatchpointType::Read);
return m_memory.Read64(vaddr);
}
std::optional<u32> DynarmicCallbacks32::MemoryReadCode(u32 vaddr) {
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
return std::nullopt;
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
if (last_code_addr != aligned_vaddr) {
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
last_code_addr = aligned_vaddr;
}
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
return m_memory.Read32(vaddr);
}
void DynarmicCallbacks32::MemoryWrite8(u32 vaddr, u8 value) {
@@ -88,6 +82,13 @@ bool DynarmicCallbacks32::MemoryWriteExclusive64(u32 vaddr, u64 value, u64 expec
m_memory.WriteExclusive64(vaddr, value, expected);
}
void DynarmicCallbacks32::InterpreterFallback(u32 pc, std::size_t num_instructions) {
m_parent.LogBacktrace(m_process);
LOG_ERROR(Core_ARM,
"Unimplemented instruction @ {:#X} for {} instructions (instr = {:08X})", pc,
num_instructions, m_memory.Read32(pc));
}
void DynarmicCallbacks32::ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) {
switch (exception) {
case Dynarmic::A32::Exception::NoExecuteFault:
+2 -8
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -7,7 +7,6 @@
#pragma once
#include <dynarmic/interface/A32/a32.h>
#include <dynarmic/interface/code_page.h>
#include "core/arm/arm_interface.h"
#include "core/arm/dynarmic/dynarmic_exclusive_monitor.h"
@@ -35,9 +34,6 @@ public:
u32 MemoryRead32(u32 vaddr) override;
u64 MemoryRead64(u32 vaddr) override;
std::optional<u32> MemoryReadCode(u32 vaddr) override;
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = 0; //reset back, force refetch
}
void MemoryWrite8(u32 vaddr, u8 value) override;
void MemoryWrite16(u32 vaddr, u16 value) override;
void MemoryWrite32(u32 vaddr, u32 value) override;
@@ -46,15 +42,13 @@ public:
bool MemoryWriteExclusive16(u32 vaddr, u16 value, u16 expected) override;
bool MemoryWriteExclusive32(u32 vaddr, u32 value, u32 expected) override;
bool MemoryWriteExclusive64(u32 vaddr, u64 value, u64 expected) override;
void InterpreterFallback(u32 pc, std::size_t num_instructions) override;
void ExceptionRaised(u32 pc, Dynarmic::A32::Exception exception) override;
void CallSVC(u32 swi) override;
void AddTicks(u64 ticks) override;
u64 GetTicksRemaining() override;
bool CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type);
void ReturnException(u32 pc, Dynarmic::HaltReason hr);
//
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = 0;
ArmDynarmic32& m_parent;
Core::Memory::Memory& m_memory;
Kernel::KProcess* m_process{};
+9 -9
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -41,17 +41,10 @@ Dynarmic::A64::Vector DynarmicCallbacks64::MemoryRead128(u64 vaddr) {
CheckMemoryAccess(vaddr, 16, Kernel::DebugWatchpointType::Read);
return {m_memory.Read64(vaddr), m_memory.Read64(vaddr + 8)};
}
std::optional<u32> DynarmicCallbacks64::MemoryReadCode(u64 vaddr) {
if (!m_memory.IsValidVirtualAddressRange(vaddr, sizeof(u32)))
return std::nullopt;
// return m_memory.Read32(vaddr);
auto const aligned_vaddr = vaddr & ~Core::Memory::YUZU_PAGEMASK;
if (last_code_addr != aligned_vaddr) {
m_memory.ReadBlock(aligned_vaddr, &cached_code_page, sizeof(cached_code_page));
last_code_addr = aligned_vaddr;
}
return cached_code_page.inst[(vaddr & Core::Memory::YUZU_PAGEMASK) / sizeof(u32)];
return m_memory.Read32(vaddr);
}
void DynarmicCallbacks64::MemoryWrite8(u64 vaddr, u8 value) {
@@ -102,6 +95,13 @@ bool DynarmicCallbacks64::MemoryWriteExclusive128(u64 vaddr, Dynarmic::A64::Vect
m_memory.WriteExclusive128(vaddr, value, expected);
}
void DynarmicCallbacks64::InterpreterFallback(u64 pc, std::size_t num_instructions) {
m_parent.LogBacktrace(m_process);
LOG_ERROR(Core_ARM, "Unimplemented instruction @ {:#X} for {} instructions (instr = {:08X})", pc,
num_instructions, m_memory.Read32(pc));
ReturnException(pc, PrefetchAbort);
}
void DynarmicCallbacks64::InstructionCacheOperationRaised(Dynarmic::A64::InstructionCacheOperation op, u64 value) {
switch (op) {
case Dynarmic::A64::InstructionCacheOperation::InvalidateByVAToPoU: {
+1 -6
View File
@@ -11,7 +11,6 @@
#include <ankerl/unordered_dense.h>
#include <dynarmic/interface/A64/a64.h>
#include <dynarmic/interface/code_page.h>
#include "common/common_types.h"
#include "common/hash.h"
#include "core/arm/arm_interface.h"
@@ -42,9 +41,6 @@ public:
u64 MemoryRead64(u64 vaddr) override;
Dynarmic::A64::Vector MemoryRead128(u64 vaddr) override;
std::optional<u32> MemoryReadCode(u64 vaddr) override;
void InstructionSynchronizationBarrierRaised() override {
last_code_addr = 0; //reset back, force refetch
}
void MemoryWrite8(u64 vaddr, u8 value) override;
void MemoryWrite16(u64 vaddr, u16 value) override;
void MemoryWrite32(u64 vaddr, u32 value) override;
@@ -55,6 +51,7 @@ public:
bool MemoryWriteExclusive32(u64 vaddr, std::uint32_t value, std::uint32_t expected) override;
bool MemoryWriteExclusive64(u64 vaddr, std::uint64_t value, std::uint64_t expected) override;
bool MemoryWriteExclusive128(u64 vaddr, Dynarmic::A64::Vector value, Dynarmic::A64::Vector expected) override;
void InterpreterFallback(u64 pc, std::size_t num_instructions) override;
void InstructionCacheOperationRaised(Dynarmic::A64::InstructionCacheOperation op, u64 value) override;
void ExceptionRaised(u64 pc, Dynarmic::A64::Exception exception) override;
void CallSVC(u32 svc) override;
@@ -64,8 +61,6 @@ public:
bool CheckMemoryAccess(u64 addr, u64 size, Kernel::DebugWatchpointType type);
void ReturnException(u64 pc, Dynarmic::HaltReason hr);
Dynarmic::CodePage cached_code_page;
u64 last_code_addr = 0;
ArmDynarmic64& m_parent;
Core::Memory::Memory& m_memory;
u64 m_tpidrro_el0{};
+7 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -772,8 +772,12 @@ std::optional<u64> MatchAndExecuteOneInstruction(Core::Memory::Memory& memory, m
u32 instruction = memory.Read32(pc);
bool was_executed = false;
auto decoder = Dynarmic::A64::Decode<VisitorBase>(instruction);
was_executed = decoder.get().call(visitor, instruction);
if (auto decoder = Dynarmic::A64::Decode<VisitorBase>(instruction)) {
was_executed = decoder->get().call(visitor, instruction);
} else {
LOG_ERROR(Core_ARM, "Unallocated encoding: {:#x}", instruction);
}
return was_executed ? std::optional<u64>(pc + 4) : std::nullopt;
}
+4
View File
@@ -806,6 +806,10 @@ void System::RegisterContentProvider(FileSys::ContentProviderUnionSlot slot,
impl->content_provider->SetSlot(slot, provider);
}
void System::ClearContentProvider(FileSys::ContentProviderUnionSlot slot) {
impl->content_provider->ClearSlot(slot);
}
const Reporter& System::GetReporter() const {
return impl->reporter;
}
+5 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -358,7 +358,10 @@ public:
[[nodiscard]] Service::FileSystem::FileSystemController& GetFileSystemController();
[[nodiscard]] const Service::FileSystem::FileSystemController& GetFileSystemController() const;
void RegisterContentProvider(FileSys::ContentProviderUnionSlot slot, FileSys::ContentProvider* provider);
void RegisterContentProvider(FileSys::ContentProviderUnionSlot slot,
FileSys::ContentProvider* provider);
void ClearContentProvider(FileSys::ContentProviderUnionSlot slot);
[[nodiscard]] const Reporter& GetReporter() const;
+2 -27
View File
@@ -13,10 +13,6 @@
#include "common/windows/timer_resolution.h"
#endif
#ifdef ARCHITECTURE_x86_64
#include "common/x64/cpu_wait.h"
#endif
#include "common/settings.h"
#include "core/core_timing.h"
#include "core/hardware_properties.h"
@@ -205,7 +201,7 @@ u64 CoreTiming::GetClockTicks() const {
if (Settings::values.sync_core_speed.GetValue()) {
const auto ticks = double(fres);
const auto speed_limit = double(Settings::SpeedLimit())*0.01;
const auto speed_limit = double(Settings::values.speed_limit.GetValue())*0.01;
return u64(ticks/speed_limit);
} else {
return fres;
@@ -287,28 +283,7 @@ void CoreTiming::ThreadLoop() {
if (next_time) {
// There are more events left in the queue, wait until the next event.
auto wait_time = *next_time - GetGlobalTimeNs().count();
if (wait_time > 0) {
#ifdef _WIN32
while (!paused && !event.IsSet() && wait_time > 0) {
wait_time = *next_time - GetGlobalTimeNs().count();
if (wait_time >= timer_resolution_ns) {
Common::Windows::SleepForOneTick();
} else {
#ifdef ARCHITECTURE_x86_64
Common::X64::MicroSleep();
#else
std::this_thread::yield();
#endif
}
}
if (event.IsSet()) {
event.Reset();
}
#else
event.WaitFor(std::chrono::nanoseconds(wait_time));
#endif
}
event.WaitFor(std::chrono::nanoseconds(wait_time));
} else {
// Queue is empty, wait until another event is scheduled and signals us to
// continue.
+60 -99
View File
@@ -1,13 +1,13 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cstring>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <mbedtls/cipher.h>
#include "common/assert.h"
#include "common/logging/log.h"
#include "core/crypto/aes_util.h"
@@ -28,121 +28,83 @@ NintendoTweak CalculateNintendoTweak(std::size_t sector_id) {
}
} // Anonymous namespace
// Structure to hide OpenSSL types from header file
static_assert(static_cast<std::size_t>(Mode::CTR) ==
static_cast<std::size_t>(MBEDTLS_CIPHER_AES_128_CTR),
"CTR has incorrect value.");
static_assert(static_cast<std::size_t>(Mode::ECB) ==
static_cast<std::size_t>(MBEDTLS_CIPHER_AES_128_ECB),
"ECB has incorrect value.");
static_assert(static_cast<std::size_t>(Mode::XTS) ==
static_cast<std::size_t>(MBEDTLS_CIPHER_AES_128_XTS),
"XTS has incorrect value.");
// Structure to hide mbedtls types from header file
struct CipherContext {
EVP_CIPHER_CTX* encryption_context = nullptr;
EVP_CIPHER_CTX* decryption_context = nullptr;
EVP_CIPHER* cipher = nullptr;
mbedtls_cipher_context_t encryption_context;
mbedtls_cipher_context_t decryption_context;
};
static inline const std::string GetCipherName(Mode mode, u32 key_size) {
std::string cipher;
std::size_t effective_bits = key_size * 8;
switch (mode) {
case Mode::CTR:
cipher = "CTR";
break;
case Mode::ECB:
cipher = "ECB";
break;
case Mode::XTS:
cipher = "XTS";
effective_bits /= 2;
break;
default:
UNREACHABLE();
}
return fmt::format("AES-{}-{}", effective_bits, cipher);
};
static EVP_CIPHER *GetCipher(Mode mode, u32 key_size) {
static auto fetch_cipher = [](Mode m, u32 k) {
return EVP_CIPHER_fetch(nullptr, GetCipherName(m, k).c_str(), nullptr);
};
static const struct {
EVP_CIPHER* ctr_16 = fetch_cipher(Mode::CTR, 16);
EVP_CIPHER* ecb_16 = fetch_cipher(Mode::ECB, 16);
EVP_CIPHER* xts_16 = fetch_cipher(Mode::XTS, 16);
EVP_CIPHER* ctr_32 = fetch_cipher(Mode::CTR, 32);
EVP_CIPHER* ecb_32 = fetch_cipher(Mode::ECB, 32);
EVP_CIPHER* xts_32 = fetch_cipher(Mode::XTS, 32);
} ciphers = {};
switch (mode) {
case Mode::CTR:
return key_size == 16 ? ciphers.ctr_16 : ciphers.ctr_32;
case Mode::ECB:
return key_size == 16 ? ciphers.ecb_16 : ciphers.ecb_32;
case Mode::XTS:
return key_size == 16 ? ciphers.xts_16 : ciphers.xts_32;
default:
UNIMPLEMENTED();
}
return nullptr;
}
// TODO: WHY TEMPLATE???????
template <typename Key, std::size_t KeySize>
Crypto::AESCipher<Key, KeySize>::AESCipher(Key key, Mode mode)
: ctx(std::make_unique<CipherContext>()) {
mbedtls_cipher_init(&ctx->encryption_context);
mbedtls_cipher_init(&ctx->decryption_context);
ctx->encryption_context = EVP_CIPHER_CTX_new();
ctx->decryption_context = EVP_CIPHER_CTX_new();
ctx->cipher = GetCipher(mode, KeySize);
if (ctx->cipher) {
EVP_CIPHER_up_ref(ctx->cipher);
} else {
UNIMPLEMENTED();
}
ASSERT_MSG((mbedtls_cipher_setup(
&ctx->encryption_context,
mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode))) ||
mbedtls_cipher_setup(
&ctx->decryption_context,
mbedtls_cipher_info_from_type(static_cast<mbedtls_cipher_type_t>(mode)))) == 0,
"Failed to initialize mbedtls ciphers.");
ASSERT_MSG(ctx->encryption_context && ctx->decryption_context && ctx->cipher,
"OpenSSL cipher context failed init!");
// now init ciphers
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));
EVP_CIPHER_CTX_set_padding(ctx->encryption_context, 0);
EVP_CIPHER_CTX_set_padding(ctx->decryption_context, 0);
ASSERT(
!mbedtls_cipher_setkey(&ctx->encryption_context, key.data(), KeySize * 8, MBEDTLS_ENCRYPT));
ASSERT(
!mbedtls_cipher_setkey(&ctx->decryption_context, key.data(), KeySize * 8, MBEDTLS_DECRYPT));
//"Failed to set key on mbedtls ciphers.");
}
template <typename Key, std::size_t KeySize>
AESCipher<Key, KeySize>::~AESCipher() {
EVP_CIPHER_CTX_free(ctx->encryption_context);
EVP_CIPHER_CTX_free(ctx->decryption_context);
EVP_CIPHER_free(ctx->cipher);
mbedtls_cipher_free(&ctx->encryption_context);
mbedtls_cipher_free(&ctx->decryption_context);
}
template <typename Key, std::size_t KeySize>
void AESCipher<Key, KeySize>::Transcode(const u8* src, std::size_t size, u8* dest, Op op) const {
auto* const context = op == Op::Encrypt ? ctx->encryption_context : ctx->decryption_context;
auto* const context = op == Op::Encrypt ? &ctx->encryption_context : &ctx->decryption_context;
mbedtls_cipher_reset(context);
if (size == 0)
return;
// reset
ASSERT(EVP_CipherInit_ex(context, nullptr, nullptr, nullptr, nullptr, -1));
const auto mode = mbedtls_cipher_get_cipher_mode(context);
std::size_t written = 0;
const int block_size = EVP_CIPHER_CTX_get_block_size(context);
ASSERT(block_size > 0 && block_size <= int(AesBlockBytes));
if (mode != MBEDTLS_MODE_ECB) {
const int ret = mbedtls_cipher_update(context, src, size, dest, &written);
ASSERT(ret == 0);
if (written != size) {
LOG_WARNING(Crypto, "Not all data was processed requested={:016X}, actual={:016X}.", size, written);
}
return;
}
const auto block_size = mbedtls_cipher_get_block_size(context);
ASSERT(block_size <= AesBlockBytes);
const std::size_t whole_block_bytes = size - (size % block_size);
int written = 0;
if (whole_block_bytes != 0) {
ASSERT(EVP_CipherUpdate(context, dest, &written, src, static_cast<int>(whole_block_bytes)));
if (std::size_t(written) != whole_block_bytes) {
const int ret = mbedtls_cipher_update(context, src, whole_block_bytes, dest, &written);
ASSERT(ret == 0);
if (written != whole_block_bytes) {
LOG_WARNING(Crypto, "Not all data was processed requested={:016X}, actual={:016X}.",
whole_block_bytes, written);
}
}
// tail
const std::size_t tail = size - whole_block_bytes;
if (tail == 0)
return;
@@ -150,13 +112,13 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, std::size_t size, u8* des
std::array<u8, AesBlockBytes> tail_buffer{};
std::memcpy(tail_buffer.data(), src + whole_block_bytes, tail);
int tail_written = 0;
ASSERT(EVP_CipherUpdate(context, tail_buffer.data(), &tail_written, tail_buffer.data(), block_size));
std::size_t tail_written = 0;
const int ret = mbedtls_cipher_update(context, tail_buffer.data(), block_size, tail_buffer.data(),
&tail_written);
ASSERT(ret == 0);
if (tail_written != block_size) {
LOG_WARNING(Crypto, "Tail block not fully processed requested={:016X}, actual={:016X}.",
block_size, tail_written);
LOG_WARNING(Crypto, "Not all data was processed requested={:016X}, actual={:016X}.", block_size,
tail_written);
}
std::memcpy(dest + whole_block_bytes, tail_buffer.data(), tail);
@@ -175,10 +137,9 @@ void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, std::size_t size, u8*
template <typename Key, std::size_t KeySize>
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_dec = EVP_CipherInit_ex(ctx->decryption_context, nullptr, nullptr, nullptr, data.data(), -1);
ASSERT_MSG(ret_enc == 1 && ret_dec == 1, "Failed to set IV on OpenSSL contexts");
ASSERT_MSG((mbedtls_cipher_set_iv(&ctx->encryption_context, data.data(), data.size()) ||
mbedtls_cipher_set_iv(&ctx->decryption_context, data.data(), data.size())) == 0,
"Failed to set IV on mbedtls ciphers.");
}
template class AESCipher<Key128>;
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -10,6 +10,7 @@
#include <span>
#include <type_traits>
#include "common/common_types.h"
#include "core/file_sys/vfs/vfs.h"
namespace Core::Crypto {
@@ -61,5 +62,4 @@ public:
private:
std::unique_ptr<CipherContext> ctx;
};
} // namespace Core::Crypto
+39 -48
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -9,26 +9,35 @@
#include <bitset>
#include <cctype>
#include <fstream>
#include <locale>
#include <map>
#include <sstream>
#include <tuple>
#include <vector>
#include <openssl/evp.h>
#include <mbedtls/bignum.h>
#include <mbedtls/cipher.h>
#include <mbedtls/cmac.h>
#include <mbedtls/sha256.h>
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "common/string_util.h"
#include "core/crypto/aes_util.h"
#include "core/crypto/key_manager.h"
#include "core/crypto/partition_data_manager.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/nca_metadata.h"
#include "core/file_sys/registered_cache.h"
#include "core/hle/service/filesystem/filesystem.h"
#include "core/loader/loader.h"
#ifndef MBEDTLS_CMAC_C
#error mbedtls was compiled without CMAC support. Check your USE flags (Gentoo) or contact your package maintainer.
#endif
namespace Core::Crypto {
namespace {
@@ -518,27 +527,15 @@ static std::array<u8, target_size> MGF1(const std::array<u8, in_size>& seed) {
std::array<u8, in_size + 4> seed_exp{};
std::memcpy(seed_exp.data(), seed.data(), in_size);
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
const EVP_MD* sha256 = EVP_sha256();
std::vector<u8> out;
size_t i = 0;
while (out.size() < target_size) {
size_t offset = out.size();
out.resize(offset + 0x20);
seed_exp[in_size + 3] = u8(i);
u32 hash_len = 0;
EVP_DigestInit_ex(ctx, sha256, nullptr);
EVP_DigestUpdate(ctx, seed_exp.data(), seed_exp.size());
EVP_DigestFinal_ex(ctx, out.data() + offset, &hash_len);
out.resize(out.size() + 0x20);
seed_exp[in_size + 3] = static_cast<u8>(i);
mbedtls_sha256(seed_exp.data(), seed_exp.size(), out.data() + out.size() - 0x20, 0);
++i;
}
EVP_MD_CTX_free(ctx);
std::array<u8, target_size> target;
std::memcpy(target.data(), out.data(), target_size);
return target;
@@ -591,28 +588,32 @@ std::optional<Key128> KeyManager::ParseTicketTitleKey(const Ticket& ticket) {
return std::nullopt;
}
mbedtls_mpi D; // RSA Private Exponent
mbedtls_mpi N; // RSA Modulus
mbedtls_mpi S; // Input
mbedtls_mpi M; // Output
mbedtls_mpi_init(&D);
mbedtls_mpi_init(&N);
mbedtls_mpi_init(&S);
mbedtls_mpi_init(&M);
const auto& title_key_block = ticket.GetData().title_key_block;
mbedtls_mpi_read_binary(&D, eticket_rsa_keypair.decryption_key.data(),
eticket_rsa_keypair.decryption_key.size());
mbedtls_mpi_read_binary(&N, eticket_rsa_keypair.modulus.data(),
eticket_rsa_keypair.modulus.size());
mbedtls_mpi_read_binary(&S, title_key_block.data(), title_key_block.size());
mbedtls_mpi_exp_mod(&M, &S, &D, &N, nullptr);
std::array<u8, 0x100> rsa_step;
{
// Private context for OpenSSL bignumbers
// Inside block because I dont wanna pollute the space...
const auto& title_key_block = ticket.GetData().title_key_block;
BIGNUM* D = BN_bin2bn(eticket_rsa_keypair.decryption_key.data(), int(eticket_rsa_keypair.decryption_key.size()), NULL);
BIGNUM* N = BN_bin2bn(eticket_rsa_keypair.modulus.data(), int(eticket_rsa_keypair.modulus.size()), NULL);
BIGNUM* S = BN_bin2bn(title_key_block.data(), int(title_key_block.size()), NULL);
BIGNUM* M = BN_new();
// M = S ^ D mod N
BN_mod_exp(M, S, D, N, NULL);
BN_bn2bin(M, rsa_step.data());
BN_free(D);
BN_free(N);
BN_free(S);
BN_free(M);
}
mbedtls_mpi_write_binary(&M, rsa_step.data(), rsa_step.size());
u8 m_0 = rsa_step[0];
std::array<u8, 0x20> m_1;
std::array<u8, 0xDF> m_2;
std::memcpy(m_1.data(), rsa_step.data() + 0x01, m_1.size());
std::array<u8, 0xDF> m_2;
std::memcpy(m_2.data(), rsa_step.data() + 0x21, m_2.size());
if (m_0 != 0) {
@@ -953,18 +954,8 @@ void KeyManager::DeriveSDSeedLazy() {
static Key128 CalculateCMAC(const u8* source, size_t size, const Key128& key) {
Key128 out{};
static EVP_MAC* mac = EVP_MAC_fetch(nullptr, "cmac", nullptr);
if (!mac) return out;
static EVP_MAC_CTX* ctx = EVP_MAC_CTX_new(mac);
if (!ctx) return out;
EVP_MAC_init(ctx, key.data(), key.size() * CHAR_BIT, NULL);
EVP_MAC_update(ctx, source, size);
size_t len;
EVP_MAC_final(ctx, out.data(), &len, out.size());
mbedtls_cipher_cmac(mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_AES_128_ECB), key.data(),
key.size() * 8, source, size, out.data());
return out;
}
+7 -2
View File
@@ -1,20 +1,25 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>
#include <mbedtls/sha256.h>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/string_util.h"
#include "common/swap.h"
#include "core/crypto/key_manager.h"
#include "core/crypto/partition_data_manager.h"
#include "core/crypto/xts_encryption_layer.h"
#include "core/file_sys/kernel_executable.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/vfs/vfs_offset.h"
#include "core/file_sys/vfs/vfs_vector.h"
#include "core/loader/loader.h"
@@ -250,4 +255,4 @@ std::array<u8, 576> PartitionDataManager::GetETicketExtendedKek() const {
prodinfo_decrypted->Read(out.data(), out.size(), 0x3890);
return out;
}
} // namespace Core::Crypto
} // namespace Core::Crypto
-7
View File
@@ -127,11 +127,6 @@ public:
void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta);
private:
struct TranslationEntry {
DAddr guest_page{};
u8* host_ptr{};
};
// Internal helper that performs the update assuming the caller already holds the necessary lock.
void UpdatePagesCachedCountNoLock(DAddr addr, size_t size, s32 delta);
@@ -200,8 +195,6 @@ private:
}
Common::VirtualBuffer<VAddr> cpu_backing_address;
std::array<TranslationEntry, 4> t_slot{};
u32 cache_cursor = 0;
using CounterType = u8;
using CounterAtomicType = std::atomic_uint8_t;
static constexpr size_t subentries = 8 / sizeof(CounterType);
+1 -43
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -247,7 +247,6 @@ void DeviceMemoryManager<Traits>::Map(DAddr address, VAddr virtual_address, size
}
impl->multi_dev_address.Register(new_dev, start_id);
}
t_slot = {};
if (track) {
TrackContinuityImpl(address, virtual_address, size, asid);
}
@@ -279,7 +278,6 @@ void DeviceMemoryManager<Traits>::Unmap(DAddr address, size_t size) {
compressed_device_addr[phys_addr - 1] = new_start | MULTI_FLAG;
}
}
t_slot = {};
}
template <typename Traits>
void DeviceMemoryManager<Traits>::TrackContinuityImpl(DAddr address, VAddr virtual_address,
@@ -419,26 +417,6 @@ void DeviceMemoryManager<Traits>::WalkBlock(DAddr addr, std::size_t size, auto o
template <typename Traits>
void DeviceMemoryManager<Traits>::ReadBlock(DAddr address, void* dest_pointer, size_t size) {
device_inter->FlushRegion(address, size);
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
const DAddr guest_page = address & ~static_cast<DAddr>(Memory::YUZU_PAGEMASK);
for (size_t i = 0; i < 4; ++i) {
if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
return;
}
}
const std::size_t page_index = address >> Memory::YUZU_PAGEBITS;
const auto phys_addr = compressed_physical_ptr[page_index];
if (phys_addr != 0) {
auto* const mem_ptr = GetPointerFromRaw<u8>((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
cache_cursor = (cache_cursor + 1) & 3U;
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
return;
}
}
WalkBlock(
address, size,
[&](size_t copy_amount, DAddr current_vaddr) {
@@ -477,26 +455,6 @@ void DeviceMemoryManager<Traits>::WriteBlock(DAddr address, const void* src_poin
template <typename Traits>
void DeviceMemoryManager<Traits>::ReadBlockUnsafe(DAddr address, void* dest_pointer, size_t size) {
const std::size_t page_offset = address & Memory::YUZU_PAGEMASK;
if (size <= Memory::YUZU_PAGESIZE - page_offset) {
const DAddr guest_page = address & ~static_cast<DAddr>(Memory::YUZU_PAGEMASK);
for (size_t i = 0; i < 4; ++i) {
if (t_slot[i].guest_page == guest_page && t_slot[i].host_ptr != nullptr) {
std::memcpy(dest_pointer, t_slot[i].host_ptr + page_offset, size);
return;
}
}
const std::size_t page_index = address >> Memory::YUZU_PAGEBITS;
const auto phys_addr = compressed_physical_ptr[page_index];
if (phys_addr != 0) {
auto* const mem_ptr = GetPointerFromRaw<u8>((PAddr(phys_addr - 1) << Memory::YUZU_PAGEBITS));
t_slot[cache_cursor % t_slot.size()] = TranslationEntry{.guest_page = guest_page, .host_ptr = mem_ptr};
cache_cursor = (cache_cursor + 1) & 3U;
std::memcpy(dest_pointer, mem_ptr + page_offset, size);
return;
}
}
WalkBlock(
address, size,
[&](size_t copy_amount, DAddr current_vaddr) {
-75
View File
@@ -4,13 +4,6 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <cstddef>
#include <cstring>
#include <limits>
#include <span>
#include <zlib.h>
#include "common/settings.h"
#include "common/string_util.h"
#include "common/swap.h"
@@ -38,73 +31,6 @@ const std::array<const char*, 16> LANGUAGE_NAMES{{
"BrazilianPortuguese",
}};
namespace {
constexpr std::size_t LEGACY_LANGUAGE_REGION_SIZE = sizeof(std::array<LanguageEntry, 16>);
constexpr std::size_t PACKED_LANGUAGE_REGION_MAX_SIZE = sizeof(LanguageEntry) * 32;
bool InflateRawDeflate(std::span<const u8> compressed, std::vector<u8>& out) {
if (compressed.empty() || compressed.size() > std::numeric_limits<uInt>::max()) {
return false;
}
z_stream stream{};
stream.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(compressed.data()));
stream.avail_in = static_cast<uInt>(compressed.size());
if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
return false;
}
std::array<u8, 0x1000> chunk{};
int ret = Z_OK;
while (ret == Z_OK) {
stream.next_out = reinterpret_cast<Bytef*>(chunk.data());
stream.avail_out = static_cast<uInt>(chunk.size());
ret = inflate(&stream, Z_NO_FLUSH);
if (ret != Z_OK && ret != Z_STREAM_END) {
inflateEnd(&stream);
return false;
}
const auto produced = chunk.size() - static_cast<std::size_t>(stream.avail_out);
if (produced != 0) {
if (out.size() + produced > PACKED_LANGUAGE_REGION_MAX_SIZE) {
inflateEnd(&stream);
return false;
}
out.insert(out.end(), chunk.begin(),
chunk.begin() + static_cast<std::ptrdiff_t>(produced));
}
}
inflateEnd(&stream);
return ret == Z_STREAM_END;
}
void DecodePackedLanguageEntries(RawNACP& raw) {
auto* packed_region = reinterpret_cast<u8*>(raw.language_entries.data());
u16_le compressed_size_le{};
std::memcpy(&compressed_size_le, packed_region, sizeof(compressed_size_le));
const auto compressed_size = static_cast<std::size_t>(compressed_size_le);
if (compressed_size == 0 || compressed_size > LEGACY_LANGUAGE_REGION_SIZE - sizeof(u16_le)) {
return;
}
std::vector<u8> decompressed;
if (!InflateRawDeflate(
std::span<const u8>(packed_region + sizeof(u16_le), compressed_size), decompressed)) {
return;
}
if (decompressed.size() < LEGACY_LANGUAGE_REGION_SIZE ||
decompressed.size() % sizeof(LanguageEntry) != 0) {
return;
}
std::memcpy(raw.language_entries.data(), decompressed.data(), LEGACY_LANGUAGE_REGION_SIZE);
}
} // namespace
std::string LanguageEntry::GetApplicationName() const {
return Common::StringFromFixedZeroTerminatedBuffer(application_name.data(),
application_name.size());
@@ -140,7 +66,6 @@ NACP::NACP() = default;
NACP::NACP(VirtualFile file) {
file->ReadObject(&raw);
DecodePackedLanguageEntries(raw);
}
NACP::~NACP() = default;
-4
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -40,7 +37,6 @@ enum class ContentRecordType : u8 {
HtmlDocument = 4,
LegalInformation = 5,
DeltaFragment = 6,
Count,
};
struct ContentRecord {
+7 -7
View File
@@ -143,7 +143,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
const auto update_tid = GetUpdateTitleID(title_id);
if (content_union) {
@@ -167,7 +167,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
// Also check ManualContentProvider (for Android)
if (!checked_external) {
const auto* manual_provider = static_cast<const ManualContentProvider*>(
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
@@ -243,7 +243,7 @@ VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
// Also try ManualContentProvider
if (update == nullptr) {
const auto* manual_provider = static_cast<const ManualContentProvider*>(
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
auto file = manual_provider->GetEntryForVersion(update_tid, ContentRecordType::Program, *enabled_version);
@@ -570,7 +570,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
bool checked_external = false;
bool checked_manual = false;
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider
const auto* external_provider = content_union->GetExternalProvider();
@@ -592,7 +592,7 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
}
if (!checked_external) {
const auto* manual_provider = static_cast<const ManualContentProvider*>(
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
@@ -690,7 +690,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
std::vector<Patch> external_update_patches;
const auto* content_union = static_cast<const ContentProviderUnion*>(&content_provider);
const auto* content_union = dynamic_cast<const ContentProviderUnion*>(&content_provider);
if (content_union) {
// First, check ExternalContentProvider for updates
@@ -721,7 +721,7 @@ std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
}
}
const auto* manual_provider = static_cast<const ManualContentProvider*>(
const auto* manual_provider = dynamic_cast<const ManualContentProvider*>(
content_union->GetSlotProvider(ContentProviderUnionSlot::FrontendManual));
if (manual_provider && external_update_patches.empty()) {
const auto manual_update_versions = manual_provider->ListUpdateVersions(update_tid);
+178 -124
View File
@@ -7,11 +7,12 @@
#include <algorithm>
#include <random>
#include <regex>
#include <openssl/evp.h>
#include <mbedtls/sha256.h>
#include "common/assert.h"
#include "common/fs/path_util.h"
#include "common/hex_util.h"
#include "common/logging/log.h"
#include "common/scope_exit.h"
#include "common/string_util.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/card_image.h"
@@ -63,23 +64,17 @@ static bool FollowsNcaIdFormat(std::string_view name) {
static std::string GetRelativePathFromNcaID(const std::array<u8, 16>& nca_id, bool second_hex_upper,
bool within_two_digit, bool cnmt_suffix) {
const auto nca_str = Common::HexToString(nca_id, second_hex_upper);
if (!within_two_digit) {
const auto format_str = fmt::runtime(cnmt_suffix ? "{}.cnmt.nca" : "/{}.nca");
return fmt::format(format_str, nca_str);
return fmt::format(format_str, Common::HexToString(nca_id, second_hex_upper));
}
Core::Crypto::SHA256Hash hash{};
u32 hash_len = 0;
EVP_Digest(nca_id.data(), nca_id.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0);
const auto format_str =
fmt::runtime(cnmt_suffix ? "/000000{:02X}/{}.cnmt.nca" : "/000000{:02X}/{}.nca");
LOG_DEBUG(Loader, "Decoded {} bytes, nca id {}", hash_len, nca_str);
return fmt::format(format_str, hash[0], nca_str);
return fmt::format(format_str, hash[0], Common::HexToString(nca_id, second_hex_upper));
}
static std::string GetCNMTName(TitleType type, u64 title_id) {
@@ -157,11 +152,7 @@ bool PlaceholderCache::Create(const NcaID& id, u64 size) const {
}
Core::Crypto::SHA256Hash hash{};
u32 hash_len = 0;
EVP_Digest(id.data(), id.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca id {}", hash_len, id);
mbedtls_sha256(id.data(), id.size(), hash.data(), 0);
const auto dirname = fmt::format("000000{:02X}", hash[0]);
const auto dir2 = GetOrCreateDirectoryRelative(dir, dirname);
@@ -185,11 +176,7 @@ bool PlaceholderCache::Delete(const NcaID& id) const {
}
Core::Crypto::SHA256Hash hash{};
u32 hash_len = 0;
EVP_Digest(id.data(), id.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca id {}", hash_len, id);
mbedtls_sha256(id.data(), id.size(), hash.data(), 0);
const auto dirname = fmt::format("000000{:02X}", hash[0]);
const auto dir2 = GetOrCreateDirectoryRelative(dir, dirname);
@@ -366,7 +353,8 @@ VirtualFile RegisteredCache::GetFileAtID(NcaID id) const {
return file;
}
static std::optional<NcaID> CheckMapForContentRecord(const ankerl::unordered_dense::map<u64, CNMT>& map, u64 title_id, ContentRecordType type) {
static std::optional<NcaID> CheckMapForContentRecord(const std::map<u64, CNMT>& map, u64 title_id,
ContentRecordType type) {
const auto cmnt_iter = map.find(title_id);
if (cmnt_iter == map.cend()) {
return std::nullopt;
@@ -683,12 +671,7 @@ InstallResult RegisteredCache::InstallEntry(const NCA& nca, TitleType type,
const OptionalHeader opt_header{0, 0};
ContentRecord c_rec{{}, {}, {}, GetCRTypeFromNCAType(nca.GetType()), {}};
const auto& data = nca.GetBaseFile()->ReadBytes(0x100000);
u32 hash_len = 0;
EVP_Digest(data.data(), data.size(), c_rec.hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca {}", hash_len, nca.GetName());
mbedtls_sha256(data.data(), data.size(), c_rec.hash.data(), 0);
std::memcpy(&c_rec.nca_id, &c_rec.hash, 16);
const CNMT new_cnmt(header, opt_header, {c_rec}, {});
if (!RawInstallYuzuMeta(new_cnmt)) {
@@ -799,12 +782,7 @@ InstallResult RegisteredCache::RawInstallNCA(const NCA& nca, const VfsCopyFuncti
id = *override_id;
} else {
const auto& data = in->ReadBytes(0x100000);
u32 hash_len = 0;
EVP_Digest(data.data(), data.size(), hash.data(), &hash_len, EVP_sha256(), nullptr);
LOG_DEBUG(Loader, "Decoded {} bytes, nca {}", hash_len, nca.GetName());
mbedtls_sha256(data.data(), data.size(), hash.data(), 0);
memcpy(id.data(), hash.data(), 16);
}
@@ -851,119 +829,174 @@ bool RegisteredCache::RawInstallYuzuMeta(const CNMT& cnmt) {
}
}
Refresh();
return std::find_if(yuzu_meta.begin(), yuzu_meta.end(), [&cnmt](const std::pair<u64, CNMT>& kv) {
return kv.second.GetType() == cnmt.GetType() && kv.second.GetTitleID() == cnmt.GetTitleID();
}) != yuzu_meta.end();
return std::find_if(yuzu_meta.begin(), yuzu_meta.end(),
[&cnmt](const std::pair<u64, CNMT>& kv) {
return kv.second.GetType() == cnmt.GetType() &&
kv.second.GetTitleID() == cnmt.GetTitleID();
}) != yuzu_meta.end();
}
ContentProviderUnion::~ContentProviderUnion() = default;
void ContentProviderUnion::SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider) {
providers[size_t(slot)] = provider;
providers[slot] = provider;
}
void ContentProviderUnion::ClearSlot(ContentProviderUnionSlot slot) {
providers[slot] = nullptr;
}
void ContentProviderUnion::Refresh() {
for (auto e : providers)
if (e != nullptr)
e->Refresh();
for (auto& provider : providers) {
if (provider.second == nullptr)
continue;
provider.second->Refresh();
}
}
bool ContentProviderUnion::HasEntry(u64 title_id, ContentRecordType type) const {
for (auto const e : providers)
if (e && e->HasEntry(title_id, type))
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (provider.second->HasEntry(title_id, type))
return true;
}
return false;
}
std::optional<u32> ContentProviderUnion::GetEntryVersion(u64 title_id) const {
for (auto const e : providers) {
if (e == nullptr)
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (auto const res = e->GetEntryVersion(title_id); res != std::nullopt)
const auto res = provider.second->GetEntryVersion(title_id);
if (res != std::nullopt)
return res;
}
return std::nullopt;
}
VirtualFile ContentProviderUnion::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
for (auto const e : providers) {
if (e == nullptr)
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (auto const res = e->GetEntryUnparsed(title_id, type); res != nullptr)
const auto res = provider.second->GetEntryUnparsed(title_id, type);
if (res != nullptr)
return res;
}
return nullptr;
}
VirtualFile ContentProviderUnion::GetEntryRaw(u64 title_id, ContentRecordType type) const {
for (auto const e : providers) {
if (e == nullptr)
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (auto const res = e->GetEntryRaw(title_id, type); res != nullptr)
const auto res = provider.second->GetEntryRaw(title_id, type);
if (res != nullptr)
return res;
}
return nullptr;
}
std::unique_ptr<NCA> ContentProviderUnion::GetEntry(u64 title_id, ContentRecordType type) const {
for (auto const e : providers) {
if (e == nullptr)
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (auto res = e->GetEntry(title_id, type); res != nullptr)
auto res = provider.second->GetEntry(title_id, type);
if (res != nullptr)
return res;
}
return nullptr;
}
std::vector<ContentProviderEntry> ContentProviderUnion::ListEntriesFilter(
std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type,
std::optional<u64> title_id) const {
std::vector<ContentProviderEntry> out;
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
const auto vec = provider.second->ListEntriesFilter(title_type, record_type, title_id);
std::copy(vec.begin(), vec.end(), std::back_inserter(out));
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>>
ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnionSlot> origin,
std::optional<TitleType> title_type,
std::optional<ContentRecordType> record_type,
std::optional<u64> title_id) const {
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> out;
for (const auto& provider : providers) {
if (provider.second == nullptr)
continue;
if (origin.has_value() && *origin != provider.first)
continue;
const auto vec = provider.second->ListEntriesFilter(title_type, record_type, title_id);
std::transform(vec.begin(), vec.end(), std::back_inserter(out),
[&provider](const ContentProviderEntry& entry) {
return std::make_pair(provider.first, entry);
});
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(
u64 title_id, ContentRecordType type) const {
const auto iter =
std::find_if(providers.begin(), providers.end(), [title_id, type](const auto& provider) {
return provider.second != nullptr && provider.second->HasEntry(title_id, type);
});
if (iter == providers.end()) {
return std::nullopt;
}
return iter->first;
}
const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const {
auto it = providers.find(ContentProviderUnionSlot::External);
if (it != providers.end() && it->second != nullptr) {
return dynamic_cast<const ExternalContentProvider*>(it->second);
}
return nullptr;
}
std::vector<ContentProviderEntry> ContentProviderUnion::ListEntriesFilter(std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type, std::optional<u64> title_id) const {
std::vector<ContentProviderEntry> out;
for (auto const& e : providers) {
if (e != nullptr) {
auto const vec = e->ListEntriesFilter(title_type, record_type, title_id);
std::copy(vec.begin(), vec.end(), std::back_inserter(out));
}
const ContentProvider* ContentProviderUnion::GetSlotProvider(ContentProviderUnionSlot slot) const {
auto it = providers.find(slot);
if (it != providers.end()) {
return it->second;
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ContentProviderUnion::ListEntriesFilterOrigin(std::optional<ContentProviderUnionSlot> origin, std::optional<TitleType> title_type, std::optional<ContentRecordType> record_type, std::optional<u64> title_id) const {
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> out;
for (size_t i = 0; i < providers.size(); ++i) {
auto const& e = providers[i];
if (e == nullptr)
continue;
if (origin.has_value() && *origin != ContentProviderUnionSlot(i))
continue;
auto const vec = e->ListEntriesFilter(title_type, record_type, title_id);
std::transform(vec.begin(), vec.end(), std::back_inserter(out), [i](const ContentProviderEntry& entry) {
return std::make_pair(ContentProviderUnionSlot(i), entry);
});
}
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()), out.end());
return out;
}
std::optional<ContentProviderUnionSlot> ContentProviderUnion::GetSlotForEntry(u64 title_id, ContentRecordType type) const {
for (size_t i = 0; i < providers.size(); ++i) {
auto const& e = providers[i];
if (e != nullptr && e->HasEntry(title_id, type))
return {ContentProviderUnionSlot(i)};
}
return std::nullopt;
}
const ExternalContentProvider* ContentProviderUnion::GetExternalProvider() const {
return static_cast<const ExternalContentProvider*>(providers[size_t(ContentProviderUnionSlot::External)]);
return nullptr;
}
ManualContentProvider::~ManualContentProvider() = default;
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type, u64 title_id, VirtualFile file) {
void ManualContentProvider::AddEntry(TitleType title_type, ContentRecordType content_type,
u64 title_id, VirtualFile file) {
entries.insert_or_assign({title_type, content_type, title_id}, file);
}
@@ -971,13 +1004,14 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
u64 title_id, u32 version,
const std::string& version_string, VirtualFile file) {
if (title_type == TitleType::Update) {
auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(), [title_id, version](const ExternalUpdateEntry& entry) {
return entry.title_id == title_id && entry.version == version;
});
auto it = std::find_if(multi_version_entries.begin(), multi_version_entries.end(),
[title_id, version](const ExternalUpdateEntry& entry) {
return entry.title_id == title_id && entry.version == version;
});
if (it != multi_version_entries.end()) {
// Update existing entry
it->files[size_t(content_type)] = file;
it->files[content_type] = file;
if (!version_string.empty()) {
it->version_string = version_string;
}
@@ -987,7 +1021,7 @@ void ManualContentProvider::AddEntryWithVersion(TitleType title_type, ContentRec
new_entry.title_id = title_id;
new_entry.version = version;
new_entry.version_string = version_string;
new_entry.files[size_t(content_type)] = file;
new_entry.files[content_type] = file;
multi_version_entries.push_back(new_entry);
}
@@ -1084,19 +1118,26 @@ std::vector<ExternalUpdateEntry> ManualContentProvider::ListUpdateVersions(u64 t
VirtualFile ManualContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.version == version) {
if (auto const p = entry.files[size_t(type)])
return p;
auto it = entry.files.find(type);
if (it != entry.files.end()) {
return it->second;
}
}
}
return nullptr;
}
bool ManualContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
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;
int count = 0;
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.files.count(type) > 0) {
count++;
if (count > 1) {
return true;
}
}
}
return false;
}
ExternalContentProvider::ExternalContentProvider(std::vector<VirtualDir> load_directories)
@@ -1215,7 +1256,7 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
}
}
std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
std::map<std::pair<u64, u32>, std::map<ContentRecordType, VirtualFile>> version_files;
for (const auto& [title_id, nca_map] : ncas) {
for (const auto& [type_pair, nca] : nca_map) {
@@ -1236,7 +1277,7 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
version = ver_it->second;
}
version_files[{title_id, version}][size_t(content_type)] = nca_file;
version_files[{title_id, version}][content_type] = nca_file;
}
LOG_DEBUG(Service_FS, "Added entry - Title ID: {:016X}, Type: {}, Content: {}",
@@ -1257,7 +1298,9 @@ void ExternalContentProvider::ProcessNSP(const VirtualFile& file) {
bool version_exists = false;
for (auto& existing : multi_version_entries) {
if (existing.title_id == title_id && existing.version == version) {
existing.files = files_map;
for (const auto& [content_type, _file] : files_map) {
existing.files[content_type] = _file;
}
if (existing.version_string.empty() && !ver_str.empty()) {
existing.version_string = ver_str;
}
@@ -1340,7 +1383,7 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
}
}
std::map<std::pair<u64, u32>, std::array<VirtualFile, size_t(ContentRecordType::Count)>> version_files;
std::map<std::pair<u64, u32>, std::map<ContentRecordType, VirtualFile>> version_files;
for (const auto& [title_id, nca_map] : ncas) {
for (const auto& [type_pair, nca] : nca_map) {
@@ -1361,7 +1404,7 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
version = ver_it->second;
}
version_files[{title_id, version}][size_t(content_type)] = nca_file;
version_files[{title_id, version}][content_type] = nca_file;
}
}
}
@@ -1379,7 +1422,9 @@ void ExternalContentProvider::ProcessXCI(const VirtualFile& file) {
bool version_exists = false;
for (auto& existing : multi_version_entries) {
if (existing.title_id == title_id && existing.version == version) {
existing.files = files_map;
for (const auto& [content_type, _file] : files_map) {
existing.files[content_type] = _file;
}
if (existing.version_string.empty() && !ver_str.empty()) {
existing.version_string = ver_str;
}
@@ -1484,19 +1529,28 @@ std::vector<ExternalUpdateEntry> ExternalContentProvider::ListUpdateVersions(u64
}
VirtualFile ExternalContentProvider::GetEntryForVersion(u64 title_id, ContentRecordType type, u32 version) const {
for (const auto& entry : multi_version_entries)
if (entry.title_id == title_id && entry.version == version)
if (auto const p = entry.files[size_t(type)])
return p;
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.version == version) {
auto it = entry.files.find(type);
if (it != entry.files.end()) {
return it->second;
}
}
}
return nullptr;
}
bool ExternalContentProvider::HasMultipleVersions(u64 title_id, ContentRecordType type) const {
size_t count = 0;
for (const auto& entry : multi_version_entries)
if (entry.title_id == title_id && entry.files[size_t(type)])
++count;
return count > 1;
for (const auto& entry : multi_version_entries) {
if (entry.title_id == title_id && entry.files.count(type) > 0) {
count++;
if (count > 1) {
return true;
}
}
}
return false;
}
} // namespace FileSys
+17 -16
View File
@@ -11,19 +11,19 @@
#include <memory>
#include <string>
#include <vector>
#include <ankerl/unordered_dense.h>
#include <boost/container/flat_map.hpp>
#include "common/common_types.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/file_sys/nca_metadata.h"
namespace FileSys {
class ExternalContentProvider;
class CNMT;
class ExternalContentProvider;
class CNMT;
class NCA;
class NSP;
class XCI;
enum class ContentRecordType : u8;
enum class NCAContentType : u8;
enum class TitleType : u8;
@@ -56,7 +56,7 @@ struct ExternalUpdateEntry {
u64 title_id;
u32 version;
std::string version_string;
std::array<VirtualFile, size_t(ContentRecordType::Count)> files;
std::map<ContentRecordType, VirtualFile> files;
};
constexpr u64 GetUpdateTitleID(u64 base_title_id) {
@@ -207,11 +207,11 @@ private:
ContentProviderParsingFunction parser;
// maps tid -> NcaID of meta
ankerl::unordered_dense::map<u64, NcaID> meta_id;
std::map<u64, NcaID> meta_id;
// maps tid -> meta
ankerl::unordered_dense::map<u64, CNMT> meta;
std::map<u64, CNMT> meta;
// maps tid -> meta for CNMT in yuzu_meta
ankerl::unordered_dense::map<u64, CNMT> yuzu_meta;
std::map<u64, CNMT> yuzu_meta;
};
enum class ContentProviderUnionSlot {
@@ -220,7 +220,6 @@ enum class ContentProviderUnionSlot {
SDMC, ///< SD Card
FrontendManual, ///< Frontend-defined game list or similar
External, ///< External content from NSP/XCI files in configured directories
Count,
};
// Combines multiple ContentProvider(s) (i.e. SysNAND, UserNAND, SDMC) into one interface.
@@ -229,6 +228,8 @@ public:
~ContentProviderUnion() override;
void SetSlot(ContentProviderUnionSlot slot, ContentProvider* provider);
void ClearSlot(ContentProviderUnionSlot slot);
void Refresh() override;
bool HasEntry(u64 title_id, ContentRecordType type) const override;
std::optional<u32> GetEntryVersion(u64 title_id) const override;
@@ -240,18 +241,18 @@ public:
std::optional<u64> title_id) const override;
const ExternalContentProvider* GetExternalProvider() const;
[[nodiscard]] inline const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const {
return providers[size_t(slot)];
}
const ContentProvider* GetSlotProvider(ContentProviderUnionSlot slot) const;
std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> ListEntriesFilterOrigin(
std::optional<ContentProviderUnionSlot> origin = {},
std::optional<TitleType> title_type = {}, std::optional<ContentRecordType> record_type = {},
std::optional<u64> title_id = {}) const;
std::optional<ContentProviderUnionSlot> GetSlotForEntry(u64 title_id, ContentRecordType type) const;
std::optional<ContentProviderUnionSlot> GetSlotForEntry(u64 title_id,
ContentRecordType type) const;
private:
std::array<ContentProvider*, size_t(ContentProviderUnionSlot::Count)> providers;
std::map<ContentProviderUnionSlot, ContentProvider*> providers;
};
class ManualContentProvider : public ContentProvider {
@@ -311,8 +312,8 @@ private:
void ProcessXCI(const VirtualFile& file);
std::vector<VirtualDir> load_dirs;
ankerl::unordered_dense::map<std::tuple<u64, ContentRecordType, TitleType>, VirtualFile> entries;
ankerl::unordered_dense::map<u64, u32> versions;
std::map<std::tuple<u64, ContentRecordType, TitleType>, VirtualFile> entries;
std::map<u64, u32> versions;
std::vector<ExternalUpdateEntry> multi_version_entries;
};
+2 -7
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -52,9 +52,7 @@ constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(OpenMode mode) {
} // Anonymous namespace
RealVfsFilesystem::RealVfsFilesystem() : VfsFilesystem(nullptr) {}
RealVfsFilesystem::~RealVfsFilesystem() {
in_dtor = true;
}
RealVfsFilesystem::~RealVfsFilesystem() = default;
std::string RealVfsFilesystem::GetName() const {
return "Real";
@@ -226,9 +224,6 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
}
void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
if (in_dtor)
return;
std::scoped_lock lk{list_lock};
// Remove from list.
-5
View File
@@ -1,6 +1,3 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -55,8 +52,6 @@ private:
ReferenceListType closed_references;
std::mutex list_lock;
size_t num_open_files{};
// TODO: Workaround for improper dtor() ordering on clang + FreeBSD
bool in_dtor = false;
private:
friend class RealVfsFile;

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