Compare commits

..

11 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
lizzie 19494bc7ac [network, qt] use jthread and use std::vector for packet list instead of std::list (#254)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/254
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-11 03:00:15 +01:00
Maufeat 28a78d76fe [audio] add splitter_size (#3477)
Thanks to: https://git.ryujinx.app/ryubing/ryujinx/-/merge_requests/257
Credits to Coxxs and his friend.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3477
Reviewed-by: MaranBr <maranbr@eden-emu.dev>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: Maufeat <sahyno1996@gmail.com>
Co-committed-by: Maufeat <sahyno1996@gmail.com>
2026-02-11 01:13:09 +01:00
lizzie 6b3f8e1b62 [core/internal_network] cleanup network interface code a bit (#2905)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Co-authored-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2905
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-10 22:17:24 +01:00
lizzie aacc40254c [cmake, ci/scripts] update missed YUZU_ENABLE_LTO -> ENABLE_LTO (#3503)
Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3503
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-10 21:33:22 +01:00
lizzie efd26c3e4d [android, nce] Remove LRU-Cache (#3500)
This PR removes the obsolete logic of LRU-cache within, removing old and dead code, allows Android to avoid unnecesary usage of memory caching, aside to prevent some old bugs to arise in other systems that allows NCE, improves a small margin of performance and makes memory ram consumption overall better, by 300 - 500mb, revealing that part of the code was still active, even if LRU wasn't enabled.

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Signed-off-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3500
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-10 03:45:02 +01:00
lizzie a8093c2a3c [*] change all std::unordered_map and std::unordered_set into ankerl::unordered_dense::map/set variants (#3442)
mainly doing this to reduce memory footprint; we all know how nice ankerl::unordered_dense is

in theory 4x faster - in practice these maps arent that "hot" anyways so not likely to have much perf gained

i just want to reduce mem fragmentation to ease my porting process, plus it helps other platforms as well (ahem weak Mediatek devices) :)

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

Co-authored-by: Caio Oliveira <caiooliveirafarias0@gmail.com>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3442
Reviewed-by: DraVee <dravee@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-10 03:34:07 +01:00
lizzie 0f8b35c4cd [vk, macos] fix missing VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT sType (#3510)
sType wasn't properly set for the creationInfo struct (was init to 0)

Signed-off-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3510
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Reviewed-by: DraVee <dravee@eden-emu.dev>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-09 21:21:14 +01:00
MrPurple666 866881d0e3 [android] add FD_DEV_FEATURES in env loader (#3493)
This environment variable fixes some glitches in OneUI 7 and HyperOS 3.

Thanks StevenMX for letting me know.

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

or if the microsleeps actually are horrid

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

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3498
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Co-committed-by: lizzie <lizzie@eden-emu.dev>
2026-02-09 01:14:32 +01:00
192 changed files with 3768 additions and 2883 deletions
+2 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash -e
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
case "$1" in
@@ -104,8 +104,7 @@ cmake .. -G Ninja \
-DYUZU_USE_QT_MULTIMEDIA=$MULTIMEDIA \
-DYUZU_USE_QT_WEB_ENGINE=$WEBENGINE \
-DYUZU_USE_FASTER_LD=ON \
-DYUZU_ENABLE_LTO=ON \
-DDYNARMIC_ENABLE_LTO=ON \
-DENABLE_LTO=ON \
"${EXTRA_CMAKE_FLAGS[@]}"
ninja -j${NPROC}
Regular → Executable
+2 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash -ex
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
if [ "$COMPILER" == "clang" ]
@@ -32,9 +32,8 @@ cmake .. -G Ninja \
-DYUZU_ROOM_STANDALONE=OFF \
-DYUZU_USE_QT_MULTIMEDIA=${USE_MULTIMEDIA:-false} \
-DYUZU_USE_QT_WEB_ENGINE=${USE_WEBENGINE:-false} \
-DYUZU_ENABLE_LTO=ON \
-DENABLE_LTO=ON \
-DCMAKE_EXE_LINKER_FLAGS=" /LTCG" \
-DDYNARMIC_ENABLE_LTO=ON \
-DYUZU_USE_BUNDLED_QT=${BUNDLE_QT:-false} \
-DUSE_CCACHE=${CCACHE:-false} \
-DENABLE_UPDATE_CHECKER=${DEVEL:-true} \
@@ -0,0 +1,26 @@
From b3622608433c183ba868a1dc8dd9cf285eb3b916 Mon Sep 17 00:00:00 2001
From: Dario Petrillo <dario.pk1@gmail.com>
Date: Thu, 27 Nov 2025 23:12:38 +0100
Subject: [PATCH] avoid extra memset when clearing an empty table
---
include/ankerl/unordered_dense.h | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/include/ankerl/unordered_dense.h b/include/ankerl/unordered_dense.h
index 0835342..4938212 100644
--- a/include/ankerl/unordered_dense.h
+++ b/include/ankerl/unordered_dense.h
@@ -1490,8 +1490,10 @@ class table : public std::conditional_t<is_map_v<T>, base_table_type_map<T>, bas
// modifiers //////////////////////////////////////////////////////////////
void clear() {
- m_values.clear();
- clear_buckets();
+ if (!empty()) {
+ m_values.clear();
+ clear_buckets();
+ }
}
auto insert(value_type const& value) -> std::pair<iterator, bool> {
+1
View File
@@ -576,6 +576,7 @@ add_subdirectory(externals)
# pass targets from externals
find_package(enet)
find_package(MbedTLS)
find_package(unordered_dense REQUIRED)
if (ARCHITECTURE_x86 OR ARCHITECTURE_x86_64)
find_package(xbyak)
+3 -3
View File
@@ -66,6 +66,9 @@ if (NOT TARGET LLVM::Demangle)
add_library(LLVM::Demangle ALIAS demangle)
endif()
# unordered_dense
AddJsonPackage(unordered-dense)
if (YUZU_STATIC_ROOM)
return()
endif()
@@ -244,9 +247,6 @@ if (ENABLE_WEB_SERVICE OR ENABLE_UPDATE_CHECKER)
AddJsonPackage(cpp-jwt)
endif()
# unordered_dense
AddJsonPackage(unordered-dense)
# FFMpeg
if (YUZU_USE_EXTERNAL_FFMPEG OR YUZU_USE_BUNDLED_FFMPEG)
add_subdirectory(ffmpeg)
+6 -3
View File
@@ -91,10 +91,13 @@
"unordered-dense": {
"package": "unordered_dense",
"repo": "martinus/unordered_dense",
"tag": "v%VERSION%",
"hash": "b98b5d4d96f8e0081b184d6c4c1181fae4e41723b54bed4296717d7f417348b48fad0bbcc664cac142b8c8a47e95aa57c1eb1cf6caa855fd782fad3e3ab99e5e",
"sha": "7b55cab841",
"hash": "d2106f6640f6bfb81755e4b8bfb64982e46ec4a507cacdb38f940123212ccf35a20b43c70c6f01d7bfb8c246d1a16f7845d8052971949cea9def1475e3fa02c8",
"find_args": "CONFIG",
"git_version": "4.8.1"
"bundled": true,
"patches": [
"0001-avoid-memset-when-clearing-an-empty-table.patch"
]
},
"mbedtls": {
"package": "MbedTLS",
+2 -2
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
@@ -33,7 +33,7 @@ endif()
if(NOT YUZU_TZDB_PATH STREQUAL "")
set(NX_TZDB_BASE_DIR "${YUZU_TZDB_PATH}")
elseif (MSVC AND NOT CXX_CLANG AND YUZU_ENABLE_LTO)
elseif (MSVC AND NOT CXX_CLANG AND ENABLE_LTO)
# TODO(crueter): boot up the windows vm
set(NX_TZDB_VERSION "250725")
set(NX_TZDB_ARCHIVE "${CPM_SOURCE_CACHE}/nx_tzdb/${NX_TZDB_VERSION}.zip")
+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)
}
@@ -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
@@ -18,6 +18,8 @@ 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
@@ -28,7 +30,7 @@ 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)
@@ -76,6 +78,12 @@ class YuzuApplication : Application() {
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)
}
@@ -42,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
)
)
@@ -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
)
)
@@ -73,8 +73,6 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
SHOW_SHADERS_BUILDING("show_shaders_building"),
DEBUG_FLUSH_BY_LINE("flush_line"),
USE_LRU_CACHE("use_lru_cache"),
DONT_SHOW_DRIVER_SHADER_WARNING("dont_show_driver_shader_warning"),
ENABLE_OVERLAY("enable_overlay"),
@@ -123,13 +123,6 @@ abstract class SettingsItem(
// List of all general
val settingsItems = HashMap<String, SettingsItem>().apply {
put(StringInputSetting(StringSetting.DEVICE_NAME, titleId = R.string.device_name))
put(
SwitchSetting(
BooleanSetting.USE_LRU_CACHE,
titleId = R.string.use_lru_cache,
descriptionId = R.string.use_lru_cache_description
)
)
put(
SwitchSetting(
BooleanSetting.RENDERER_USE_SPEED_LIMIT,
@@ -235,7 +235,6 @@ class SettingsFragmentPresenter(
add(HeaderSetting(R.string.cpu))
add(IntSetting.FAST_CPU_TIME.key)
add(BooleanSetting.USE_LRU_CACHE.key)
add(BooleanSetting.CORE_SYNC_CORE_SPEED.key)
add(IntSetting.MEMORY_LAYOUT.key)
@@ -1310,7 +1310,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
)
)
}
@@ -1497,7 +1497,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
setTextColor(
MaterialColors.getColor(
this,
com.google.android.material.R.attr.colorPrimary
androidx.appcompat.R.attr.colorPrimary
)
)
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.fragments
@@ -110,7 +110,7 @@ class FreedrenoSettingsFragment : Fragment() {
val commonVars = listOf(
"TU_DEBUG", "FD_MESA_DEBUG", "IR3_SHADER_DEBUG",
"FD_RD_DUMP", "FD_RD_DUMP_FRAMES", "FD_RD_DUMP_TESTNAME",
"TU_BREADCRUMBS"
"TU_BREADCRUMBS", "FD_DEV_FEATURES"
)
for (varName in commonVars) {
@@ -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,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
package org.yuzu.yuzu_emu.utils
@@ -144,6 +144,15 @@ object FreedrenoPresets {
)
)
val DEV_FEATURES_UBWC_HINT = FreedrenoPreset(
name = "Dev - UBWC Flag Hint",
description = "Enable TP UBWC flag hint for development",
icon = "ic_dev_features",
variables = mapOf(
"FD_DEV_FEATURES" to "enable_tp_ubwc_flag_hint=1"
)
)
val PERFORMANCE_DEFAULT = FreedrenoPreset(
name = "Performance - Default",
description = "Clear all debug options for performance",
@@ -159,6 +168,7 @@ object FreedrenoPresets {
CAPTURE_FRAMES,
SHADER_DEBUG,
GPU_HANG_TRACE,
DEV_FEATURES_UBWC_HINT,
PERFORMANCE_DEFAULT
)
}
@@ -1,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
)
@@ -54,10 +54,6 @@ namespace AndroidSettings {
Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
Settings::Category::GpuDriver};
// LRU Cache
Settings::SwitchableSetting<bool> use_lru_cache{linkage, true, "use_lru_cache",
Settings::Category::System};
Settings::Setting<s32> theme{linkage, 0, "theme", Settings::Category::Android};
Settings::Setting<s32> theme_mode{linkage, -1, "theme_mode", Settings::Category::Android};
Settings::Setting<bool> black_backgrounds{linkage, false, "black_backgrounds",
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -18,7 +21,7 @@ struct RomMetadata {
bool isHomebrew;
};
std::unordered_map<std::string, RomMetadata> m_rom_metadata_cache;
ankerl::unordered_dense::map<std::string, RomMetadata> m_rom_metadata_cache;
RomMetadata CacheRomMetadata(const std::string& path) {
const auto file =
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
/**
@@ -258,6 +258,11 @@ Java_org_yuzu_yuzu_1emu_utils_NativeFreedrenoConfig_setFreedrenoEnv(
}
LOG_INFO(Frontend, "[Freedreno] Set {}={}", var_name, value);
if (var_name == "FD_DEV_FEATURES") {
LOG_INFO(Frontend, "[Freedreno] FD_DEV_FEATURES enabled: {}", value);
}
return JNI_TRUE;
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -18,7 +21,7 @@
#include "input_common/drivers/virtual_gamepad.h"
#include "native.h"
std::unordered_map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
ankerl::unordered_dense::map<std::string, std::unique_ptr<AndroidConfig>> map_profiles;
bool IsHandheldOnly() {
const auto npad_style_set =
@@ -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>
@@ -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"
@@ -94,9 +94,6 @@
<string name="use_sync_core">مزامنة سرعة النواة</string>
<string name="use_sync_core_description">قم بمزامنة سرعة النواة مع النسبة المئوية للسرعة القصوى لتحسين الأداء دون تغيير السرعة الفعلية للعبة.</string>
<string name="use_lru_cache">تمكين ذاكرة التخزين المؤقتة LRU</string>
<string name="use_lru_cache_description">قم بتمكين أو تعطيل ذاكرة التخزين المؤقتة الأقل استخدامًا (LRU)، مما يزيد من الأداء عن طريق توفير استخدام معالج وحدة المعالجة المركزية. قد تواجه بعض الألعاب مشكلات مع هذا الإعداد، لذا قم بتعطيله إذا لم يتم تشغيل اللعبة أو تعطلت بشكل عشوائي.</string>
<string name="cpuopt_unsafe_host_mmu">تمكين محاكاة MMU المضيف</string>
<string name="cpuopt_unsafe_host_mmu_description">يعمل هذا التحسين على تسريع وصول الذاكرة بواسطة البرنامج الضيف. يؤدي تمكينه إلى إجراء عمليات قراءة/كتابة ذاكرة الضيف مباشرة في الذاكرة والاستفادة من MMU المضيف. يؤدي تعطيل هذا إلى إجبار جميع عمليات الوصول إلى الذاكرة على استخدام محاكاة MMU البرمجية.</string>
<string name="debug_knobs">مقابض تصحيح الأخطاء</string>
@@ -58,9 +58,6 @@
<string name="use_sync_core">مزامنة سرعة النواة</string>
<string name="use_sync_core_description">خێرایی تیکەکانی ناوک ڕێکبخە لەگەڵ ڕێژەی خێرایی بەرزترین بۆ باشترکردنی کارایی بەبێ گۆڕینی خێرایی ڕاستەقینەی یارییەکە.</string>
<string name="use_lru_cache">تمكين ذاكرة التخزين المؤقت LRU</string>
<string name="use_lru_cache_description">چالاک یان ناچالاککردنی کاشەی LRU، کارایی باشتر دەکات بە هەڵگرتنی بەکارهێنانی پرۆسەی CPU. هەندێک یاری کێشەی لەگەڵ هەیە، بەتایبەتی TotK 1.2.1، بۆیە بیخەوێنە ئەگەر یاریەکە نەگەڕێت یان بە هەڕەمەکی بشکێت.</string>
<string name="cpuopt_unsafe_host_mmu">چالاککردنی میمیکردنی MMU میواندە</string>
<string name="cpuopt_unsafe_host_mmu_description">ئەم باشکردنە خێرایی دەستکەوتنی بیرگە لەلایەن پرۆگرامی میوانەکە زیاد دەکات. چالاککردنی وای لێدەکات کە خوێندنەوە/نووسینەکانی بیرگەی میوانەکە ڕاستەوخۆ لە بیرگە ئەنجام بدرێت و میمیکردنی MMU میواندە بەکاربهێنێت. ناچالاککردنی ئەمە هەموو دەستکەوتنەکانی بیرگە ڕەت دەکاتەوە لە بەکارهێنانی میمیکردنی MMU نەرمەکاڵا.</string>
<!-- NVDEC Emulation -->
@@ -90,9 +90,6 @@
<string name="use_sync_core">Synchronizovat rychlost jádra</string>
<string name="use_sync_core_description">Synchronizuje rychlost jádra s nastaveným limitem rychlosti, aby se zlepšil výkon bez zrychlení samotné hry.</string>
<string name="use_lru_cache">Zapnout mezipaměť LRU</string>
<string name="use_lru_cache_description">Zapne, nebo vypne mezipaměť LRU (Least Recently Used). Ta zvyšuje výkon tím, že šetří využití procesoru. Některé hry s tímto mohou mít problém, takže pokud se hra nespustí, nebo náhodně padá, tuto volbu vypněte.</string>
<string name="cpuopt_unsafe_host_mmu">Povolit emulaci hostitelské MMU</string>
<string name="cpuopt_unsafe_host_mmu_description">Tato optimalizace zrychluje hostovanému programu přístupy do paměti. Zapnutím této funkce se čtení i zápisy do paměti provádějí přímo a využívají hostitelskou MMU. Při vypnutí musí všechny přístupy do paměti použít softwarovou emulaci MMU. </string>
<string name="debug_knobs">Ladící parametry</string>
@@ -90,9 +90,6 @@
<string name="use_sync_core">Kern-Geschwindigkeit synchronisieren</string>
<string name="use_sync_core_description">Synchronisiert die Taktrate des Kerns mit der maximalen Geschwindigkeit, um die Leistung zu verbessern, ohne die tatsächliche Spielgeschwindigkeit zu verändern.</string>
<string name="use_lru_cache">LRU-Cache aktivieren</string>
<string name="use_lru_cache_description">Aktivieren oder deaktivieren Sie den LRU-Cache, um die Leistung durch Einsparung von CPU-Prozessorauslastung zu verbessern. Einige Spiele haben Probleme damit, insbesondere TotK 1.2.1, deaktivieren Sie es also, wenn das Spiel nicht startet oder zufällig abstürzt.</string>
<string name="cpuopt_unsafe_host_mmu">Host-MMU-Emulation aktivieren</string>
<string name="cpuopt_unsafe_host_mmu_description">Diese Optimierung beschleunigt Speicherzugriffe durch das Gastprogramm. Wenn aktiviert, erfolgen Speicherlese- und -schreibvorgänge des Gastes direkt im Speicher und nutzen die MMU des Hosts. Das Deaktivieren erzwingt die Verwendung der Software-MMU-Emulation für alle Speicherzugriffe.</string>
<string name="debug_knobs">Debug-Regler</string>
@@ -232,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>
@@ -708,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>
@@ -75,9 +75,6 @@
<string name="use_sync_core">Sincronizar la velocidad del núcleo</string>
<string name="use_sync_core_description">Sincroniza la velocidad del núcleo con el porcentaje máximo de velocidad para mejorar el rendimiento sin alterar la velocidad real del juego.</string>
<string name="use_lru_cache">Habilitar caché LRU</string>
<string name="use_lru_cache_description">Habilite o deshabilite la caché menos utilizada recientemente (LRU), aumentando el rendimiento al ahorrar el uso del proceso de la CPU. Algunos juegos pueden ver problemas con esta configuración, así que desactívela si el juego no arranca o se bloquea aleatoriamente.</string>
<string name="cpuopt_unsafe_host_mmu">Habilitar emulación de MMU del anfitrión</string>
<string name="cpuopt_unsafe_host_mmu_description">Esta optimización acelera el acceso a la memoria del programa invitado. Al habilitarla, las lecturas y escrituras de la memoria del invitado se realizan directamente en la memoria y utilizan la MMU del anfitrión. Al deshabilitarla, todos los accesos a la memoria utilizan el software de emulación de la MMU.</string>
<!-- NVDEC Emulation -->
@@ -60,8 +60,6 @@
<string name="sync_memory_operations_description">اطمینان از سازگاری داده‌ها بین عملیات محاسباتی و حافظه. این گزینه ممکن است مشکلات برخی بازی‌ها را رفع کند، اما در برخی موارد ممکن است عملکرد را کاهش دهد. به نظر می‌رسد بازی‌های با Unreal Engine 4 بیشترین تأثیر را داشته باشند.</string>
<string name="buffer_reorder_disable">غیرفعال کردن مرتب‌سازی مجدد بافر</string>
<string name="buffer_reorder_disable_description">در صورت انتخاب، مرتب‌سازی مجدد آپلودهای حافظه نگاشت‌شده غیرفعال می‌شود که امکان ارتباط آپلودها با ترسیمات خاص را فراهم می‌کند. ممکن است در برخی موارد عملکرد را کاهش دهد.</string>
<string name="use_lru_cache">فعال‌سازی حافظه نهان LRU</string>
<string name="use_lru_cache_description">حافظه پنهان LRU را فعال یا غیرفعال کنید تا با کاهش استفاده از پردازنده، عملکرد بهبود یابد. برخی بازی‌ها مانند TotK 1.2.1 با این ویژگی مشکل دارند، در صورت عدم راه‌اندازی یا قطعی تصادفی بازی، آن را غیرفعال کنید.</string>
<string name="dyna_state">حالت پویای گسترده</string>
<string name="dyna_state_description">تعداد قابلیت‌هایی که می‌توان در حالت Extended Dynamic State استفاده کرد را کنترل می‌کند. اعداد بالاتر قابلیت‌های بیشتری را فعال کرده و می‌توانند عملکرد را افزایش دهند، اما ممکن است با برخی درایورها و فروشندگان مشکلاتی ایجاد کنند. مقدار پیش‌فرض بسته به سیستم و قابلیت‌های سخت‌افزاری شما ممکن است متفاوت باشد. این مقدار را می‌توان تغییر داد تا زمانی که پایداری و کیفیت بصری بهتری حاصل شود.</string>
<string name="disabled">غیرفعال</string>
@@ -75,9 +75,6 @@
<string name="use_sync_core">Synchroniser la vitesse du cœur</string>
<string name="use_sync_core_description">Synchronise la vitesse du cœur avec le pourcentage de vitesse maximal pour améliorer les performances sans modifier la vitesse réelle du jeu.</string>
<string name="use_lru_cache">Activer le cache LRU</string>
<string name="use_lru_cache_description">Active ou désactive le cache LRU (Least Recently Used) pour améliorer les performances en réduisant lutilisation du processeur. Certains jeux peuvent rencontrer des problèmes avec ce réglage ; désactivez-le si le jeu ne démarre pas ou plante de manière aléatoire.</string>
<string name="cpuopt_unsafe_host_mmu">Activer l\'émulation de la MMU hôte</string>
<string name="cpuopt_unsafe_host_mmu_description">Cette optimisation accélère les accès mémoire par le programme invité. L\'activer entraîne que les lectures/écritures mémoire de l\'invité sont effectuées directement en mémoire et utilisent la MMU de l\'hôte. Désactiver cela force tous les accès mémoire à utiliser l\'émulation logicielle de la MMU.</string>
<string name="debug_knobs">Boutons de débogage</string>
@@ -62,9 +62,6 @@
<string name="use_sync_core">סנכרון מהירות ליבה</string>
<string name="use_sync_core_description">סנכרן את מהירות הליבה לאחוז המהירות המרבי כדי לשפר ביצועים מבלי לשנות את מהירות המשחק בפועל.</string>
<string name="use_lru_cache">הפעלת מטמון LRU</string>
<string name="use_lru_cache_description">הפעל או השבת מטמון LRU לשיפור ביצועים על ידי חיסכון בשימוש במעבד. לחלק מהמשחקים כמו TotK 1.2.1 יש בעיות - השבת אם המשחק לא עולה או קורס באקראי.</string>
<string name="cpuopt_unsafe_host_mmu">הפעל אמולציית MMU מארח</string>
<string name="cpuopt_unsafe_host_mmu_description">אופטימיזציה זו מאיצה את גישת הזיכרון על ידי התוכנית האורחת. הפעלתה גורמת לכך שפעולות קריאה/כתיבה לזיכרון האורח מתבצעות ישירות לזיכרון ומשתמשות ב-MMU של המארח. השבתת זאת מאלצת את כל גישות הזיכרון להשתמש באמולציית MMU תוכנתית.</string>
<string name="debug_knobs_description">לשימוש בפיתוח בלבד.</string>
@@ -58,9 +58,6 @@
<string name="use_sync_core">Magsebesség szinkronizálása</string>
<string name="use_sync_core_description">A mag sebességének szinkronizálása a maximális sebesség százalékával a teljesítmény javítása érdekében a játék tényleges sebességének megváltoztatása nélkül.</string>
<string name="use_lru_cache">LRU gyorsítótár engedélyezése</string>
<string name="use_lru_cache_description">Engedélyezi vagy letiltja az LRU gyorsítótárat, növelve a teljesítményt a CPU használat csökkentésével. Néhány játéknak (különösen a TotK 1.2.1-nek) problémája lehet vele - tiltsa le, ha a játék nem indul el vagy véletlenszerűen összeomlik.</string>
<string name="cpuopt_unsafe_host_mmu">Gazda MMU emuláció engedélyezése</string>
<string name="cpuopt_unsafe_host_mmu_description">Ez az optimalizáció gyorsítja a vendégprogram memória-hozzáférését. Engedélyezése esetén a vendég memóriaolvasási/írási műveletei közvetlenül a memóriában történnek, és kihasználják a gazda MMU-ját. Letiltás esetén minden memória-hozzáférés a szoftveres MMU emulációt használja.</string>
<!-- NVDEC Emulation -->
@@ -77,9 +77,6 @@
<string name="use_sync_core">Sinkronisasi Kecepatan Inti</string>
<string name="use_sync_core_description">Sinkronkan kecepatan inti dengan persentase kecepatan maksimum untuk meningkatkan performa tanpa mengubah kecepatan sebenarnya dari permainan.</string>
<string name="use_lru_cache">Aktifkan LRU Cache</string>
<string name="use_lru_cache_description">Aktifkan atau nonaktifkan cache LRU untuk meningkatkan performa dengan mengurangi penggunaan proses CPU. Beberapa game seperti TotK 1.2.1 memiliki masalah - nonaktifkan jika game tidak mau berjalan atau crash acak.</string>
<string name="cpuopt_unsafe_host_mmu">Aktifkan Emulasi MMU Host</string>
<string name="cpuopt_unsafe_host_mmu_description">Optimasi ini mempercepat akses memori oleh program tamu. Mengaktifkannya menyebabkan pembacaan/penulisan memori tamu dilakukan langsung ke memori dan memanfaatkan MMU Host. Menonaktifkan ini memaksa semua akses memori menggunakan Emulasi MMU Perangkat Lunak.</string>
<!-- NVDEC Emulation -->
@@ -75,9 +75,6 @@
<string name="use_sync_core">Sincronizza velocità core</string>
<string name="use_sync_core_description">Sincronizza la velocità del core con la percentuale massima di velocità per migliorare le prestazioni senza alterare la velocità effettiva del gioco.</string>
<string name="use_lru_cache">Abilita cache LRU</string>
<string name="use_lru_cache_description">Abilita o disabilita la cache LRU per migliorare le prestazioni riducendo l\'uso della CPU. Alcuni giochi come TotK 1.2.1 hanno problemi - disabilitalo se il gioco non si avvia o crasha casualmente.</string>
<string name="cpuopt_unsafe_host_mmu">Abilita l\'emulazione della MMU nell\'host</string>
<string name="cpuopt_unsafe_host_mmu_description">Questa ottimizzazione accelera gli accessi alla memoria da parte del programma guest. Abilitandola, le letture/scritture della memoria guest vengono eseguite direttamente in memoria e sfruttano la MMU host. Disabilitandola, tutti gli accessi alla memoria sono costretti a utilizzare l\'emulazione software della MMU.</string>
<!-- NVDEC Emulation -->
@@ -58,9 +58,6 @@
<string name="use_sync_core">コア速度の同期</string>
<string name="use_sync_core_description">コアの速度を最大速度パーセンテージに同期させ、ゲームの実際の速度を変えずにパフォーマンスを向上させます。</string>
<string name="use_lru_cache">LRUキャッシュを有効化</string>
<string name="use_lru_cache_description">LRUキャッシュを有効/無効にし、CPUプロセスの使用を節約してパフォーマンスを向上させます。TotK 1.2.1など一部のゲームで問題が発生する可能性があるため、ゲームが起動しない場合やランダムにクラッシュする場合は無効にしてください。</string>
<string name="cpuopt_unsafe_host_mmu">ホストMMUエミュレーションを有効化</string>
<string name="cpuopt_unsafe_host_mmu_description">この最適化により、ゲストプログラムによるメモリアクセスが高速化されます。有効にすると、ゲストのメモリ読み書きが直接メモリ内で実行され、ホストのMMUを利用します。無効にすると、すべてのメモリアクセスでソフトウェアMMUエミュレーションが使用されます。</string>
<!-- NVDEC Emulation -->
@@ -58,9 +58,6 @@
<string name="use_sync_core">코어 속도 동기화</string>
<string name="use_sync_core_description">코어 틱 속도를 최대 속도 백분율과 동기화하여 게임의 실제 속도를 변경하지 않고 성능을 향상시킵니다.</string>
<string name="use_lru_cache">LRU 캐시 사용</string>
<string name="use_lru_cache_description">LRU 캐시를 활성화 또는 비활성화하여 CPU 프로세스 사용을 절약하고 성능을 향상시킵니다. TotK 1.2.1을 포함한 일부 게임에서 문제가 발생할 수 있으므로 게임이 부팅되지 않거나 무작위로 충돌하는 경우 비활성화하세요.</string>
<string name="cpuopt_unsafe_host_mmu">호스트 MMU 에뮬레이션 사용</string>
<string name="cpuopt_unsafe_host_mmu_description">이 최적화는 게스트 프로그램의 메모리 접근 속도를 높입니다. 활성화하면 게스트의 메모리 읽기/쓰기가 메모리에서 직접 수행되고 호스트의 MMU를 활용합니다. 비활성화하면 모든 메모리 접근에 소프트웨어 MMU 에뮬레이션을 사용하게 됩니다.</string>
<!-- NVDEC Emulation -->
@@ -58,9 +58,6 @@
<string name="use_sync_core">Synkroniser kjernespeed</string>
<string name="use_sync_core_description">Synkroniser kjernens hastighet med maksimal hastighetsprosent for å forbedre ytelsen uten å endre spillets faktiske hastighet.</string>
<string name="use_lru_cache">Aktiver LRU-mellomlager</string>
<string name="use_lru_cache_description">Aktiver eller deaktiver LRU-mellomlager for å forbedre ytelsen ved å spare CPU-prosessorbruk. Noen spill som TotK 1.2.1 har problemer med dette - deaktiver hvis spillet ikke starter eller krasjer tilfeldig.</string>
<string name="cpuopt_unsafe_host_mmu">Aktiver verts-MMU-emulering</string>
<string name="cpuopt_unsafe_host_mmu_description">Denne optimaliseringen fremskynder minnetilgang av gjesteprogrammet. Hvis aktivert, utføres gjestens minnelesing/skriving direkte i minnet og bruker vertens MMU. Deaktivering tvinger alle minnetilganger til å bruke programvarebasert MMU-emulering.</string>
<!-- NVDEC Emulation -->
@@ -90,9 +90,6 @@
<string name="use_sync_core">Synchronizuj prędkość rdzenia</string>
<string name="use_sync_core_description">Synchronizuje prędkość rdzenia z maksymalnym procentem prędkości, aby poprawić wydajność bez zmiany rzeczywistej prędkości gry.</string>
<string name="use_lru_cache">Włącz pamięć podręczną LRU</string>
<string name="use_lru_cache_description">Włącz lub wyłącz pamięć podręczną LRU, aby poprawić wydajność poprzez zmniejszenie użycia procesora. Niektóre gry, takie jak TotK 1.2.1, mogą mieć problemy - wyłącz, jeśli gra się nie uruchamia lub losowo zawiesza.</string>
<string name="cpuopt_unsafe_host_mmu">Włącz emulację MMU hosta</string>
<string name="cpuopt_unsafe_host_mmu_description">Ta optymalizacja przyspiesza dostęp do pamięci przez program gościa. Włączenie powoduje, że odczyty/zapisy pamięci gościa są wykonywane bezpośrednio w pamięci i wykorzystują MMU hosta. Wyłączenie wymusza użycie programowej emulacji MMU dla wszystkich dostępów do pamięci.</string>
<string name="debug_knobs">Przełączniki debugowania</string>
@@ -84,9 +84,6 @@
<string name="use_sync_core">Sincronizar Velocidade do Núcleo</string>
<string name="use_sync_core_description">Aumenta a velocidade do ciclo do núcleo até o máximo para melhorar o desempenho, mantendo a velocidade real do jogo inalterada.</string>
<string name="use_lru_cache">Ativar Cache LRU</string>
<string name="use_lru_cache_description">O cache Least Recently Used (LRU, ou Itens Menos Usados) aumenta o desempenho, economizando o uso do processador. Alguns jogos podem apresentar problemas com esta configuração. Desative-a caso o jogo não inicie ou trave aleatoriamente.</string>
<string name="cpuopt_unsafe_host_mmu">Ativar Emulação da MMU do Sistema</string>
<string name="cpuopt_unsafe_host_mmu_description">Esta otimização acelera o acesso à memória pelo programa. Ao ativá-la, leituras e gravações são feitas diretamente na memória, usando a MMU (Unidade de Gerenciamento de Memória) do sistema. Ao desativar, todos os acessos passam a utilizar a Emulação de MMU por Software.</string>
<string name="debug_knobs">Controles de Depuração</string>
@@ -58,9 +58,6 @@
<string name="use_sync_core">Sincronizar velocidade do núcleo</string>
<string name="use_sync_core_description">Sincroniza a velocidade do núcleo com a percentagem máxima de velocidade para melhorar o desempenho sem alterar a velocidade real do jogo.</string>
<string name="use_lru_cache">Ativar cache LRU</string>
<string name="use_lru_cache_description">Ative ou desative a cache LRU para melhorar desempenho poupando uso do processador. Alguns jogos como TotK 1.2.1 têm problemas - desative se o jogo não iniciar ou falhar aleatoriamente.</string>
<string name="cpuopt_unsafe_host_mmu">Ativar Emulação de MMU do Anfitrião</string>
<string name="cpuopt_unsafe_host_mmu_description">Esta otimização acelera os acessos à memória pelo programa convidado. Ativar faz com que as leituras/escritas de memória do convidado sejam efetuadas diretamente na memória e utilizem a MMU do Anfitrião. Desativar força todos os acessos à memória a usar a Emulação de MMU por Software.</string>
<!-- NVDEC Emulation -->
@@ -92,9 +92,6 @@
<string name="use_sync_core">Синхронизация скорости ядра</string>
<string name="use_sync_core_description">Синхронизирует скорость ядра с максимальным процентом скорости для улучшения производительности без изменения фактической скорости игры.</string>
<string name="use_lru_cache">Включить LRU-кеш</string>
<string name="use_lru_cache_description">Включите или отключите кэш LRU (наименее недавно использованный), повышая производительность за счёт снижения нагрузки на ЦП. Некоторые игры могут работать с ним некорректно (например, TotK 1.2.1), поэтому отключите, если игра не запускается или случайно вылетает.</string>
<string name="cpuopt_unsafe_host_mmu">Включить эмуляцию MMU хоста</string>
<string name="cpuopt_unsafe_host_mmu_description">Эта оптимизация ускоряет доступ к памяти гостевой программой. При включении операции чтения/записи памяти гостя выполняются напрямую в памяти с использованием MMU хоста. Отключение заставляет все обращения к памяти использовать программную эмуляцию MMU.</string>
<string name="debug_knobs">Настройки отладки</string>
@@ -56,9 +56,6 @@
<string name="use_sync_core">Синхронизујте брзину језгре</string>
<string name="use_sync_core_description">Синхронизујте језгро Тицк Брзина брзине максималне брзине да бисте побољшали перформансе без измене стварне брзине игре.</string>
<string name="use_lru_cache">Омогући ЛРУ кеш</string>
<string name="use_lru_cache_description">Омогућите или онемогућите најмање недавно коришћене (ЛРУ) кеш меморије, све веће перформансе штедећи употребу процеса ЦПУ-а. Неке игре са њом имају проблеме, посебно тотк 1.2.1, тако да онемогућите ако се игра не покрене или се руши насумично.</string>
<string name="cpuopt_unsafe_host_mmu">Омогући емулацију MMU домаћина</string>
<string name="cpuopt_unsafe_host_mmu_description">Ова оптимизација убрзава приступ меморији од стране гостујућег програма. Укључивање изазива да се читања/уписа меморије госта обављају директно у меморији и користе MMU домаћина. Искључивање присиљава све приступе меморији да користе софтверску емулацију MMU.</string>
<!-- NVDEC Emulation -->
@@ -94,9 +94,6 @@
<string name="use_sync_core">Синхронізувати швидкість ядра</string>
<string name="use_sync_core_description">Синхронізує швидкість ядра з максимальним відсотком швидкості для покращення продуктивності без зміни реальної швидкості гри.</string>
<string name="use_lru_cache">Увімкнути LRU-кеш</string>
<string name="use_lru_cache_description">Увімкніть або вимкніть кеш LRU (Least Recently Used) для покращення продуктивності шляхом зменшення навантаження на CPU. Деякі ігри (зокрема TotK 1.2.1) можуть працювати некоректно - вимкніть, якщо гра не запускається або раптово вилітає.</string>
<string name="cpuopt_unsafe_host_mmu">Увімкнути емуляцію MMU хоста</string>
<string name="cpuopt_unsafe_host_mmu_description">Ця оптимізація пришвидшує доступ до пам\'яті гостьовою програмою. Увімкнення призводить до того, що читання/запис пам\'яті гостя виконуються безпосередньо в пам\'яті та використовують MMU хоста. Вимкнення змушує всі звернення до пам\'яті використовувати програмну емуляцію MMU.</string>
<string name="debug_knobs">Зневаджувальні регулятори</string>
@@ -58,9 +58,6 @@
<string name="use_sync_core">Đồng bộ tốc độ lõi</string>
<string name="use_sync_core_description">Đồng bộ tốc độ lõi với tỷ lệ phần trăm tốc độ tối đa để cải thiện hiệu suất mà không làm thay đổi tốc độ thực tế của trò chơi.</string>
<string name="use_lru_cache">Bật bộ nhớ đệm LRU</string>
<string name="use_lru_cache_description">Bật hoặc tắt bộ nhớ đệm LRU để cải thiện hiệu suất bằng cách tiết kiệm quy trình sử dụng CPU. Một số trò chơi như TotK 1.2.1 có vấn đề - hãy tắt nếu trò chơi không khởi động hoặc bị treo ngẫu nhiên.</string>
<string name="cpuopt_unsafe_host_mmu">Bật giả lập MMU Máy chủ</string>
<string name="cpuopt_unsafe_host_mmu_description">Tối ưu hóa này tăng tốc độ truy cập bộ nhớ của chương trình khách. Bật nó lên khiến các thao tác đọc/ghi bộ nhớ khách được thực hiện trực tiếp vào bộ nhớ và sử dụng MMU của Máy chủ. Tắt tính năng này buộc tất cả quyền truy cập bộ nhớ phải sử dụng Giả lập MMU Phần mềm.</string>
<!-- NVDEC Emulation -->
@@ -94,9 +94,6 @@
<string name="use_sync_core">同步核心速度</string>
<string name="use_sync_core_description">将核心速度与最大速度百分比同步,在不改变游戏实际速度的情况下提高性能。</string>
<string name="use_lru_cache">启用LRU缓存</string>
<string name="use_lru_cache_description">启用或禁用LRU缓存,通过节省CPU进程使用来提高性能。某些游戏可能存在问题,特别是TotK 1.2.1,如果游戏无法启动或随机崩溃,请禁用此选项。</string>
<string name="cpuopt_unsafe_host_mmu">启用主机 MMU 模拟</string>
<string name="cpuopt_unsafe_host_mmu_description">此优化可加速来宾程序的内存访问。启用后,来宾内存读取/写入将直接在内存中执行并利用主机的 MMU。禁用此功能将强制所有内存访问使用软件 MMU 模拟。</string>
<string name="debug_knobs">调试开关</string>
@@ -90,9 +90,6 @@
<string name="use_sync_core">同步核心速度</string>
<string name="use_sync_core_description">將核心速度與最大速度百分比同步,在不改變遊戲實際速度的情況下提高效能。</string>
<string name="use_lru_cache">啟用LRU快取</string>
<string name="use_lru_cache_description">啟用或停用LRU快取,透過節省CPU進程使用率來提高效能。某些遊戲可能存在問題,特別是薩爾達傳說:王國之淚 v1.2.1(更新),如果遊戲無法啟動或隨機崩潰,請停用此選項。</string>
<string name="cpuopt_unsafe_host_mmu">啟用主機 MMU 模擬</string>
<string name="cpuopt_unsafe_host_mmu_description">此最佳化可加速來賓程式的記憶體存取。啟用後,來賓記憶體讀取/寫入將直接在記憶體中執行並利用主機的 MMU。停用此功能將強制所有記憶體存取使用軟體 MMU 模擬。</string>
<string name="debug_knobs">偵錯開關</string>
@@ -99,9 +99,6 @@
<string name="use_sync_core">Synchronize Core Speed</string>
<string name="use_sync_core_description">Synchronize the core tick speed to the maximum speed percentage to improve performance without altering the game\'s actual speed.</string>
<string name="use_lru_cache">Enable LRU Cache</string>
<string name="use_lru_cache_description">Enable or disable the Least Recently Used (LRU) cache, increasing performance by saving CPU process usage. Some games may see issues with this setting, so disable it if the game doesn\'t boot or crashes randomly.</string>
<string name="cpuopt_unsafe_host_mmu">Enable Host MMU Emulation</string>
<string name="cpuopt_unsafe_host_mmu_description">This optimization speeds up memory accesses by the guest program. Enabling it causes guest memory reads/writes to be done directly into memory and make use of Host\'s MMU. Disabling this forces all memory accesses to use Software MMU Emulation.</string>
<string name="debug_knobs">Debug knobs</string>
@@ -243,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>
@@ -769,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>
+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 {
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
@@ -616,12 +616,11 @@ Result InfoUpdater::UpdateErrorInfo(const BehaviorInfo& behaviour_) {
}
Result InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) {
u32 consumed_size{0};
if (!splitter_context.Update(input, consumed_size)) {
if (!splitter_context.Update(input)) {
return Service::Audio::ResultInvalidUpdateInfo;
}
input += consumed_size;
input += in_header->splitter_size;
return ResultSuccess;
}
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -35,7 +38,7 @@ class InfoUpdater {
/* 0x18 */ u32 mix_size{};
/* 0x1C */ u32 sinks_size{};
/* 0x20 */ u32 performance_buffer_size{};
/* 0x24 */ char unk24[4];
/* 0x24 */ u32 splitter_size{};
/* 0x28 */ u32 render_info_size{};
/* 0x2C */ char unk2C[0x10];
/* 0x3C */ u32 size{sizeof(UpdateDataHeader)};
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
@@ -93,16 +93,14 @@ bool SplitterContext::Initialize(const BehaviorInfo& behavior,
return true;
}
bool SplitterContext::Update(const u8* input, u32& consumed_size) {
bool SplitterContext::Update(const u8* input) {
auto in_params{reinterpret_cast<const InParameterHeader*>(input)};
if (destinations_count == 0 || info_count == 0) {
consumed_size = 0;
return true;
}
if (in_params->magic != GetSplitterInParamHeaderMagic()) {
consumed_size = 0;
return false;
}
@@ -113,8 +111,6 @@ bool SplitterContext::Update(const u8* input, u32& consumed_size) {
u32 offset{sizeof(InParameterHeader)};
offset = UpdateInfo(input, offset, in_params->info_count);
offset = UpdateData(input, offset, in_params->destination_count);
consumed_size = Common::AlignUp(offset, 0x10);
return true;
}
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
@@ -104,9 +104,8 @@ public:
*
* @param input - Input buffer with the new info,
* expected to point to a InParameterHeader.
* @param consumed_size - Output with the number of bytes consumed from input.
*/
bool Update(const u8* input, u32& consumed_size);
bool Update(const u8* input);
/**
* Update the splitters.
+2 -4
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project SPDX-License-Identifier:
@@ -186,8 +186,6 @@ if(ARCHITECTURE_x86_64)
common
PRIVATE x64/cpu_detect.cpp
x64/cpu_detect.h
x64/cpu_wait.cpp
x64/cpu_wait.h
x64/native_clock.cpp
x64/native_clock.h
x64/rdtsc.cpp
@@ -255,7 +253,7 @@ if (lz4_ADDED)
target_include_directories(common PRIVATE ${lz4_SOURCE_DIR}/lib)
endif()
target_link_libraries(common PUBLIC fmt::fmt stb::headers Threads::Threads)
target_link_libraries(common PUBLIC fmt::fmt stb::headers Threads::Threads unordered_dense::unordered_dense)
target_link_libraries(common PRIVATE lz4::lz4 LLVM::Demangle zstd::zstd)
if(ANDROID)
+4 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -7,7 +7,7 @@
#include <algorithm>
#include <iostream>
#include <sstream>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include "common/assert.h"
#include "common/fs/fs.h"
@@ -193,8 +193,8 @@ private:
SetLegacyPathImpl(legacy_path, new_path);
}
std::unordered_map<EdenPath, fs::path> eden_paths;
std::unordered_map<EmuPath, fs::path> legacy_paths;
ankerl::unordered_dense::map<EdenPath, fs::path> eden_paths;
ankerl::unordered_dense::map<EmuPath, fs::path> legacy_paths;
};
bool ValidatePath(const fs::path& path) {
+3 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -7,7 +7,7 @@
#ifdef _WIN32
#include <iterator>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include <boost/icl/separate_interval_set.hpp>
#include <windows.h>
#include "common/dynamic_library.h"
@@ -391,7 +391,7 @@ private:
std::mutex placeholder_mutex; ///< Mutex for placeholders
boost::icl::separate_interval_set<size_t> placeholders; ///< Mapped placeholders
std::unordered_map<size_t, size_t> placeholder_host_pointers; ///< Placeholder backing offset
ankerl::unordered_dense::map<size_t, size_t> placeholder_host_pointers; ///< Placeholder backing offset
};
#else // ^^^ Windows ^^^ vvv POSIX vvv
+5 -2
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -6,7 +9,7 @@
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include <utility>
#include <vector>
#include "common/logging/log.h"
@@ -409,7 +412,7 @@ public:
namespace Impl {
template <typename InputDeviceType>
using FactoryListType = std::unordered_map<std::string, std::shared_ptr<Factory<InputDeviceType>>>;
using FactoryListType = ankerl::unordered_dense::map<std::string, std::shared_ptr<Factory<InputDeviceType>>>;
template <typename InputDeviceType>
struct FactoryList {
+5 -2
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2017 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
@@ -5,14 +8,14 @@
#include <initializer_list>
#include <string>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
namespace Common {
/// A string-based key-value container supporting serializing to and deserializing from a string
class ParamPackage {
public:
using DataType = std::unordered_map<std::string, std::string>;
using DataType = ankerl::unordered_dense::map<std::string, std::string>;
ParamPackage() = default;
explicit ParamPackage(const std::string& serialized);
-6
View File
@@ -217,11 +217,6 @@ struct Values {
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core,
Specialization::Default};
// Memory
#ifdef HAS_NCE
SwitchableSetting<bool> lru_cache_enabled{linkage, false, "use_lru_cache", Category::System};
#endif
// Cpu
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,
#ifdef HAS_NCE
@@ -795,7 +790,6 @@ struct Values {
// Per-game overrides
bool use_squashed_iterated_blend;
bool enable_global_overrides{true};
Setting<bool> enable_overlay{linkage, false, "enable_overlay", Category::Core};
};
-78
View File
@@ -1,78 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <thread>
#ifdef _MSC_VER
#include <intrin.h>
#endif
#include "common/x64/cpu_detect.h"
#include "common/x64/cpu_wait.h"
#include "common/x64/rdtsc.h"
namespace Common::X64 {
namespace {
// 100,000 cycles is a reasonable amount of time to wait to save on CPU resources.
// For reference:
// At 1 GHz, 100K cycles is 100us
// At 2 GHz, 100K cycles is 50us
// At 4 GHz, 100K cycles is 25us
constexpr auto PauseCycles = 100'000U;
} // Anonymous namespace
#if defined(_MSC_VER) && !defined(__clang__)
__forceinline static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
_tpause(RequestC02State, FencedRDTSC() + PauseCycles);
}
__forceinline static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
_mm_monitorx(&monitor_var, 0, 0);
_mm_mwaitx(EnableWaitTimeFlag, RequestC1State, PauseCycles);
}
#else
static void TPAUSE() {
static constexpr auto RequestC02State = 0U;
const auto tsc = FencedRDTSC() + PauseCycles;
const auto eax = static_cast<u32>(tsc & 0xFFFFFFFF);
const auto edx = static_cast<u32>(tsc >> 32);
asm volatile("tpause %0" : : "r"(RequestC02State), "d"(edx), "a"(eax));
}
static void MWAITX() {
static constexpr auto EnableWaitTimeFlag = 1U << 1;
static constexpr auto RequestC1State = 0U;
// monitor_var should be aligned to a cache line.
alignas(64) u64 monitor_var{};
asm volatile("monitorx" : : "a"(&monitor_var), "c"(0), "d"(0));
asm volatile("mwaitx" : : "a"(RequestC1State), "b"(PauseCycles), "c"(EnableWaitTimeFlag));
}
#endif
void MicroSleep() {
static const bool has_waitpkg = GetCPUCaps().waitpkg;
static const bool has_monitorx = GetCPUCaps().monitorx;
if (has_waitpkg) {
TPAUSE();
} else if (has_monitorx) {
MWAITX();
} else {
std::this_thread::yield();
}
}
} // namespace Common::X64
-10
View File
@@ -1,10 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
namespace Common::X64 {
void MicroSleep();
} // namespace Common::X64
+6 -6
View File
@@ -17,8 +17,6 @@ add_library(core STATIC
constants.h
core.cpp
core.h
game_overrides.cpp
game_overrides.h
game_settings.cpp
game_settings.h
core_timing.cpp
@@ -1133,7 +1131,6 @@ add_library(core STATIC
internal_network/socket_proxy.cpp
internal_network/socket_proxy.h
internal_network/sockets.h
internal_network/wifi_scanner.cpp
internal_network/wifi_scanner.h
launch_timestamp_cache.cpp
launch_timestamp_cache.h
@@ -1172,9 +1169,12 @@ add_library(core STATIC
tools/renderdoc.h)
if (ENABLE_WIFI_SCAN)
# find_package(libiw REQUIRED)
target_compile_definitions(core PRIVATE ENABLE_WIFI_SCAN)
target_link_libraries(core PRIVATE iw)
target_sources(core PRIVATE internal_network/wifi_scanner.cpp)
if (PLATFORM_LINUX)
target_link_libraries(core PRIVATE iw)
endif()
else()
target_sources(core PRIVATE internal_network/wifi_scanner_dummy.cpp)
endif()
if (WIN32)
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
@@ -8,7 +8,7 @@
#include <atomic>
#include <memory>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include <dynarmic/interface/A64/a64.h>
#include "common/common_types.h"
-187
View File
@@ -1,187 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <list>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
#include <utility>
#include "common/logging/log.h"
template <typename KeyType, typename ValueType>
class LRUCache {
public:
using key_type = KeyType;
using value_type = ValueType;
using size_type = std::size_t;
struct Statistics {
size_type hits = 0;
size_type misses = 0;
void reset() noexcept { hits = misses = 0; }
};
explicit LRUCache(size_type capacity, bool enabled = true)
: enabled_{enabled}, capacity_{capacity} {
cache_map_.reserve(capacity_);
LOG_WARNING(Core, "LRU Cache initialised (state: {} | capacity: {})", enabled_ ? "enabled" : "disabled", capacity_);
}
// Non-movable copy semantics
LRUCache(const LRUCache&) = delete;
LRUCache& operator=(const LRUCache&) = delete;
LRUCache(LRUCache&& other) noexcept { *this = std::move(other); }
LRUCache& operator=(LRUCache&& other) noexcept {
if (this == &other) return *this;
std::unique_lock this_lock(mutex_, std::defer_lock);
std::unique_lock other_lock(other.mutex_, std::defer_lock);
std::lock(this_lock, other_lock);
enabled_ = other.enabled_;
capacity_ = other.capacity_;
cache_list_ = std::move(other.cache_list_);
cache_map_ = std::move(other.cache_map_);
stats_ = other.stats_;
return *this;
}
~LRUCache() = default;
[[nodiscard]] value_type* get(const key_type& key) {
if (!enabled_) [[unlikely]] return nullptr;
std::unique_lock lock(mutex_);
auto it = cache_map_.find(key);
if (it == cache_map_.end()) {
++stats_.misses;
return nullptr;
}
move_to_front(it);
++stats_.hits;
return &it->second.second;
}
[[nodiscard]] value_type* peek(const key_type& key) const {
if (!enabled_) [[unlikely]] return nullptr;
std::shared_lock lock(mutex_);
auto it = cache_map_.find(key);
return it == cache_map_.end() ? nullptr : &it->second.second;
}
template <typename V>
void put(const key_type& key, V&& value) {
if (!enabled_) [[unlikely]] return;
std::unique_lock lock(mutex_);
insert_or_update(key, std::forward<V>(value));
}
template <typename ValueFactory>
value_type& get_or_emplace(const key_type& key, ValueFactory&& factory) {
std::unique_lock lock(mutex_);
auto it = cache_map_.find(key);
if (it != cache_map_.end()) {
move_to_front(it);
return it->second.second;
}
value_type new_value = factory();
insert_or_update(key, std::move(new_value));
return cache_map_.find(key)->second.second;
}
[[nodiscard]] bool contains(const key_type& key) const {
if (!enabled_) return false;
std::shared_lock lock(mutex_);
return cache_map_.find(key) != cache_map_.end();
}
bool erase(const key_type& key) {
if (!enabled_) return false;
std::unique_lock lock(mutex_);
auto it = cache_map_.find(key);
if (it == cache_map_.end()) return false;
cache_list_.erase(it->second.first);
cache_map_.erase(it);
return true;
}
void clear() {
std::unique_lock lock(mutex_);
cache_list_.clear();
cache_map_.clear();
stats_.reset();
}
[[nodiscard]] size_type size() const {
if (!enabled_) return 0;
std::shared_lock lock(mutex_);
return cache_map_.size();
}
[[nodiscard]] size_type get_capacity() const { return capacity_; }
void resize(size_type new_capacity) {
if (!enabled_) return;
std::unique_lock lock(mutex_);
capacity_ = new_capacity;
shrink_if_needed();
cache_map_.reserve(capacity_);
}
void setEnabled(bool state) {
std::unique_lock lock(mutex_);
enabled_ = state;
LOG_WARNING(Core, "LRU Cache state changed to: {}", state ? "enabled" : "disabled");
if (!enabled_) clear();
}
[[nodiscard]] bool isEnabled() const { return enabled_; }
[[nodiscard]] Statistics stats() const {
std::shared_lock lock(mutex_);
return stats_;
}
private:
using list_type = std::list<key_type>;
using list_iterator = typename list_type::iterator;
using map_value_type = std::pair<list_iterator, value_type>;
using map_type = std::unordered_map<key_type, map_value_type>;
template <typename V>
void insert_or_update(const key_type& key, V&& value) {
auto it = cache_map_.find(key);
if (it != cache_map_.end()) {
it->second.second = std::forward<V>(value);
move_to_front(it);
return;
}
// evict LRU if full
if (cache_map_.size() >= capacity_) {
const auto& lru_key = cache_list_.back();
cache_map_.erase(lru_key);
cache_list_.pop_back();
}
cache_list_.push_front(key);
cache_map_[key] = {cache_list_.begin(), std::forward<V>(value)};
}
void move_to_front(typename map_type::iterator it) {
cache_list_.splice(cache_list_.begin(), cache_list_, it->second.first);
it->second.first = cache_list_.begin();
}
void shrink_if_needed() {
while (cache_map_.size() > capacity_) {
const auto& lru_key = cache_list_.back();
cache_map_.erase(lru_key);
cache_list_.pop_back();
}
}
private:
mutable std::shared_mutex mutex_;
bool enabled_{true};
size_type capacity_;
list_type cache_list_;
map_type cache_map_;
mutable Statistics stats_;
};
-23
View File
@@ -17,27 +17,6 @@
namespace Core::NCE {
Patcher::Patcher(Patcher&& other) noexcept
: patch_cache(std::move(other.patch_cache)),
m_patch_instructions(std::move(other.m_patch_instructions)),
m_patch_instructions_pre(std::move(other.m_patch_instructions_pre)),
c(m_patch_instructions),
c_pre(m_patch_instructions_pre),
m_save_context(other.m_save_context),
m_load_context(other.m_load_context),
m_save_context_pre(other.m_save_context_pre),
m_load_context_pre(other.m_load_context_pre),
mode(other.mode),
total_program_size(other.total_program_size),
m_relocate_module_index(other.m_relocate_module_index),
modules(std::move(other.modules)),
curr_patch(nullptr) {
if (!modules.empty()) {
curr_patch = &modules.back();
}
}
using namespace Common::Literals;
using namespace oaknut::util;
@@ -47,8 +26,6 @@ constexpr size_t MaxRelativeBranch = 128_MiB;
constexpr u32 ModuleCodeIndex = 0x24 / sizeof(u32);
Patcher::Patcher() : c(m_patch_instructions), c_pre(m_patch_instructions_pre) {
LOG_WARNING(Core_ARM, "Patcher initialized with LRU cache {}",
patch_cache.isEnabled() ? "enabled" : "disabled");
// The first word of the patch section is always a branch to the first instruction of the
// module.
c.dw(0);
+6 -28
View File
@@ -4,17 +4,17 @@
#pragma once
#include <span>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include <vector>
#include <oaknut/code_block.hpp>
#include <oaknut/oaknut.hpp>
#include "common/logging/log.h"
#include "common/common_types.h"
#include "common/settings.h"
#include "core/hle/kernel/code_set.h"
#include "core/hle/kernel/k_typed_address.h"
#include "core/hle/kernel/physical_memory.h"
#include "lru_cache.h"
#include <utility>
using ModuleID = std::array<u8, 32>; // NSO build ID
struct PatchCacheKey {
@@ -44,19 +44,16 @@ enum class PatchMode : u32 {
Split, ///< Patch sections are inserted before .text and after .data
};
using ModuleTextAddress = u64;
using PatchTextAddress = u64;
using EntryTrampolines = std::unordered_map<ModuleTextAddress, PatchTextAddress>;
using EntryTrampolines = ankerl::unordered_dense::map<ModuleTextAddress, PatchTextAddress>;
class Patcher {
public:
void SetModuleID(const ModuleID& id) {
module_id = id;
}
Patcher(const Patcher&) = delete;
Patcher& operator=(const Patcher&) = delete;
Patcher(Patcher&& other) noexcept;
Patcher& operator=(Patcher&&) noexcept = delete;
explicit Patcher();
~Patcher();
bool PatchText(const Kernel::PhysicalMemory& program_image,
@@ -99,28 +96,9 @@ private:
void WriteCntpctHandler(ModuleDestLabel module_dest, oaknut::XReg dest_reg) { WriteCntpctHandler(module_dest, dest_reg, c); }
private:
static constexpr size_t CACHE_SIZE = 16384; // Cache size for patch entries
LRUCache<PatchCacheKey, PatchTextAddress> patch_cache{CACHE_SIZE, Settings::values.lru_cache_enabled.GetValue()};
void BranchToPatch(uintptr_t module_dest) {
if (patch_cache.isEnabled()) {
PatchCacheKey key{module_id, module_dest};
LOG_DEBUG(Core_ARM, "LRU cache lookup for module={}, offset={:#x}", fmt::ptr(module_id.data()), module_dest);
// Try to get existing patch entry from cache
if (auto* cached_patch = patch_cache.get(key)) {
LOG_WARNING(Core_ARM, "LRU cache hit for module offset {:#x}", module_dest);
curr_patch->m_branch_to_patch_relocations.push_back({c.offset(), *cached_patch});
return;
}
LOG_DEBUG(Core_ARM, "LRU cache miss for module offset {:#x}, creating new patch", module_dest);
// Not in cache: create and store
const auto patch_addr = c.offset();
curr_patch->m_branch_to_patch_relocations.push_back({patch_addr, module_dest});
patch_cache.put(key, patch_addr);
} else {
LOG_DEBUG(Core_ARM, "LRU cache disabled - direct patch for offset {:#x}", module_dest);
curr_patch->m_branch_to_patch_relocations.push_back({c.offset(), module_dest});
}
LOG_DEBUG(Core_ARM, "Patch for offset {:#x}", module_dest);
curr_patch->m_branch_to_patch_relocations.push_back({c.offset(), module_dest});
}
void BranchToPatchPre(uintptr_t module_dest) {
+4 -2
View File
@@ -371,8 +371,7 @@ struct System::Impl {
LOG_ERROR(Core, "Failed to find program id for ROM");
}
GameSettings::LoadOverrides(program_id, gpu_core->Renderer(),
Settings::values.enable_global_overrides);
GameSettings::LoadOverrides(program_id, gpu_core->Renderer());
if (auto room_member = Network::GetRoomMember().lock()) {
Network::GameInfo game_info;
game_info.name = name;
@@ -386,6 +385,9 @@ struct System::Impl {
void ShutdownMainProcess() {
SetShuttingDown(true);
// Reset per-game flags
Settings::values.use_squashed_iterated_blend = false;
is_powered_on = false;
exit_locked = false;
exit_requested = false;
+2 -27
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -13,10 +13,6 @@
#include "common/windows/timer_resolution.h"
#endif
#ifdef ARCHITECTURE_x86_64
#include "common/x64/cpu_wait.h"
#endif
#include "common/settings.h"
#include "core/core_timing.h"
#include "core/hardware_properties.h"
@@ -287,28 +283,7 @@ void CoreTiming::ThreadLoop() {
if (next_time) {
// There are more events left in the queue, wait until the next event.
auto wait_time = *next_time - GetGlobalTimeNs().count();
if (wait_time > 0) {
#ifdef _WIN32
while (!paused && !event.IsSet() && wait_time > 0) {
wait_time = *next_time - GetGlobalTimeNs().count();
if (wait_time >= timer_resolution_ns) {
Common::Windows::SleepForOneTick();
} else {
#ifdef ARCHITECTURE_x86_64
Common::X64::MicroSleep();
#else
std::this_thread::yield();
#endif
}
}
if (event.IsSet()) {
event.Reset();
}
#else
event.WaitFor(std::chrono::nanoseconds(wait_time));
#endif
}
event.WaitFor(std::chrono::nanoseconds(wait_time));
} else {
// Queue is empty, wait until another event is scheduled and signals us to
// continue.
+6 -3
View File
@@ -1,9 +1,12 @@
// 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
#include <algorithm>
#include <set>
#include <unordered_set>
#include <ankerl/unordered_dense.h>
#include <utility>
#include "core/file_sys/vfs/vfs_layered.h"
@@ -60,7 +63,7 @@ std::string LayeredVfsDirectory::GetFullPath() const {
std::vector<VirtualFile> LayeredVfsDirectory::GetFiles() const {
std::vector<VirtualFile> out;
std::unordered_set<std::string> out_names;
ankerl::unordered_dense::set<std::string> out_names;
for (const auto& layer : dirs) {
for (auto& file : layer->GetFiles()) {
@@ -76,7 +79,7 @@ std::vector<VirtualFile> LayeredVfsDirectory::GetFiles() const {
std::vector<VirtualDir> LayeredVfsDirectory::GetSubdirectories() const {
std::vector<VirtualDir> out;
std::unordered_set<std::string> out_names;
ankerl::unordered_dense::set<std::string> out_names;
for (const auto& layer : dirs) {
for (const auto& sd : layer->GetSubdirectories()) {
-615
View File
@@ -1,615 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/game_overrides.h"
#include "core/game_settings.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <charconv>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include "common/fs/path_util.h"
#include "common/logging/log.h"
#include "common/settings.h"
#include "video_core/renderer_base.h"
namespace Core::GameOverrides {
namespace {
std::string Trim(std::string_view str) {
const auto start = str.find_first_not_of(" \t\r\n");
if (start == std::string_view::npos) return "";
const auto end = str.find_last_not_of(" \t\r\n");
return std::string(str.substr(start, end - start + 1));
}
std::string ToLower(std::string str) {
std::transform(str.begin(), str.end(), str.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return str;
}
bool ToBool(const std::string& value) {
return value == "true" || value == "1" || value == "yes" || value == "on";
}
GameSettings::OS DetectOS() {
#if defined(_WIN32)
return GameSettings::OS::Windows;
#elif defined(__FIREOS__)
return GameSettings::OS::FireOS;
#elif defined(__ANDROID__)
return GameSettings::OS::Android;
#elif defined(__OHOS__)
return GameSettings::OS::HarmonyOS;
#elif defined(__HAIKU__)
return GameSettings::OS::HaikuOS;
#elif defined(__DragonFly__)
return GameSettings::OS::DragonFlyBSD;
#elif defined(__NetBSD__)
return GameSettings::OS::NetBSD;
#elif defined(__OpenBSD__)
return GameSettings::OS::OpenBSD;
#elif defined(_AIX)
return GameSettings::OS::AIX;
#elif defined(__managarm__)
return GameSettings::OS::Managarm;
#elif defined(__redox__)
return GameSettings::OS::RedoxOS;
#elif defined(__APPLE__)
return GameSettings::OS::MacOS;
#elif defined(__FreeBSD__)
return GameSettings::OS::FreeBSD;
#elif defined(__sun) && defined(__SVR4)
return GameSettings::OS::Solaris;
#elif defined(__linux__)
return GameSettings::OS::Linux;
#else
return GameSettings::OS::Unknown;
#endif
}
GameSettings::GPUVendor DetectGPUVendor(const std::string& vendor_string) {
std::string gpu = vendor_string;
std::transform(gpu.begin(), gpu.end(), gpu.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (gpu.find("nvidia") != std::string::npos || gpu.find("geforce") != std::string::npos) {
return GameSettings::GPUVendor::Nvidia;
}
if (gpu.find("amd") != std::string::npos || gpu.find("radeon") != std::string::npos ||
gpu.find("radv") != std::string::npos) {
return GameSettings::GPUVendor::AMD;
}
if (gpu.find("intel") != std::string::npos) {
return GameSettings::GPUVendor::Intel;
}
if (gpu.find("apple") != std::string::npos || gpu.find("molten") != std::string::npos) {
return GameSettings::GPUVendor::Apple;
}
if (gpu.find("qualcomm") != std::string::npos || gpu.find("adreno") != std::string::npos ||
gpu.find("turnip") != std::string::npos) {
return GameSettings::GPUVendor::Qualcomm;
}
if (gpu.find("mali") != std::string::npos) {
return GameSettings::GPUVendor::ARM;
}
if (gpu.find("powervr") != std::string::npos || gpu.find("pvr") != std::string::npos) {
return GameSettings::GPUVendor::Imagination;
}
if (gpu.find("microsoft") != std::string::npos) {
return GameSettings::GPUVendor::Microsoft;
}
return GameSettings::GPUVendor::Unknown;
}
std::string OSToString(GameSettings::OS os) {
switch (os) {
case GameSettings::OS::Windows: return "windows";
case GameSettings::OS::Linux: return "linux";
case GameSettings::OS::MacOS: return "macos";
case GameSettings::OS::Android: return "android";
case GameSettings::OS::FireOS: return "fireos";
case GameSettings::OS::HarmonyOS: return "harmonyos";
case GameSettings::OS::FreeBSD: return "freebsd";
case GameSettings::OS::DragonFlyBSD: return "dragonflybsd";
case GameSettings::OS::NetBSD: return "netbsd";
case GameSettings::OS::OpenBSD: return "openbsd";
case GameSettings::OS::HaikuOS: return "haikuos";
case GameSettings::OS::AIX: return "aix";
case GameSettings::OS::Managarm: return "managarm";
case GameSettings::OS::RedoxOS: return "redoxos";
case GameSettings::OS::Solaris: return "solaris";
default: return "unknown";
}
}
std::string VendorToString(GameSettings::GPUVendor vendor) {
switch (vendor) {
case GameSettings::GPUVendor::Nvidia: return "nvidia";
case GameSettings::GPUVendor::AMD: return "amd";
case GameSettings::GPUVendor::Intel: return "intel";
case GameSettings::GPUVendor::Apple: return "apple";
case GameSettings::GPUVendor::Qualcomm: return "qualcomm";
case GameSettings::GPUVendor::ARM: return "arm";
case GameSettings::GPUVendor::Imagination: return "imagination";
case GameSettings::GPUVendor::Microsoft: return "microsoft";
default: return "unknown";
}
}
#define SET_OVERRIDE(setting, new_value) \
do { \
s.setting.SetGlobal(false); \
s.setting.SetValue(new_value); \
} while(0)
void ApplySetting(const std::string& key, const std::string& value) {
const std::string k = ToLower(key);
const std::string v = ToLower(value);
auto& s = Settings::values;
if (k == "backend" || k == "renderer_backend") {
if (v == "vulkan") {
SET_OVERRIDE(renderer_backend, Settings::RendererBackend::Vulkan);
} else if (v == "opengl" || v == "opengl_glsl") {
SET_OVERRIDE(renderer_backend, Settings::RendererBackend::OpenGL_GLSL);
} else if (v == "opengl_glasm") {
SET_OVERRIDE(renderer_backend, Settings::RendererBackend::OpenGL_GLASM);
} else if (v == "opengl_spirv") {
SET_OVERRIDE(renderer_backend, Settings::RendererBackend::OpenGL_SPIRV);
} else if (v == "null") {
SET_OVERRIDE(renderer_backend, Settings::RendererBackend::Null);
}
}
else if (k == "vsync") {
SET_OVERRIDE(vsync_mode, ToBool(v) ? Settings::VSyncMode::Fifo : Settings::VSyncMode::Immediate);
}
else if (k == "vsync_mode") {
if (v == "immediate" || v == "off") {
SET_OVERRIDE(vsync_mode, Settings::VSyncMode::Immediate);
} else if (v == "fifo" || v == "on") {
SET_OVERRIDE(vsync_mode, Settings::VSyncMode::Fifo);
} else if (v == "fifo_relaxed") {
SET_OVERRIDE(vsync_mode, Settings::VSyncMode::FifoRelaxed);
} else if (v == "mailbox") {
SET_OVERRIDE(vsync_mode, Settings::VSyncMode::Mailbox);
}
}
else if (k == "gpu_accuracy" || k == "accuracy_level") {
if (v == "low" || v == "fast") {
SET_OVERRIDE(gpu_accuracy, Settings::GpuAccuracy::Low);
} else if (v == "medium" || v == "balanced" || v == "normal") {
SET_OVERRIDE(gpu_accuracy, Settings::GpuAccuracy::Medium);
} else if (v == "high" || v == "accurate") {
SET_OVERRIDE(gpu_accuracy, Settings::GpuAccuracy::High);
}
}
else if (k == "cpu_accuracy") {
if (v == "auto") {
SET_OVERRIDE(cpu_accuracy, Settings::CpuAccuracy::Auto);
} else if (v == "accurate") {
SET_OVERRIDE(cpu_accuracy, Settings::CpuAccuracy::Accurate);
} else if (v == "unsafe") {
SET_OVERRIDE(cpu_accuracy, Settings::CpuAccuracy::Unsafe);
} else if (v == "paranoid") {
SET_OVERRIDE(cpu_accuracy, Settings::CpuAccuracy::Paranoid);
}
}
else if (k == "astc_decode_mode" || k == "accelerate_astc") {
if (v == "cpu") {
SET_OVERRIDE(accelerate_astc, Settings::AstcDecodeMode::Cpu);
} else if (v == "gpu") {
SET_OVERRIDE(accelerate_astc, Settings::AstcDecodeMode::Gpu);
} else if (v == "cpu_async") {
SET_OVERRIDE(accelerate_astc, Settings::AstcDecodeMode::CpuAsynchronous);
}
}
else if (k == "fast_gpu_time") {
if (v == "off" || v == "false" || v == "0" || v == "normal") {
SET_OVERRIDE(fast_gpu_time, Settings::GpuOverclock::Normal);
} else if (v == "medium" || v == "true" || v == "1") {
SET_OVERRIDE(fast_gpu_time, Settings::GpuOverclock::Medium);
} else if (v == "high") {
SET_OVERRIDE(fast_gpu_time, Settings::GpuOverclock::High);
}
}
else if (k == "resolution" || k == "resolution_setup") {
if (v == "0.25x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res1_4X);
} else if (v == "0.5x" || v == "half") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res1_2X);
} else if (v == "0.75x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res3_4X);
} else if (v == "1x" || v == "native") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res1X);
} else if (v == "1.25x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res5_4X);
} else if (v == "1.5x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res3_2X);
} else if (v == "2x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res2X);
} else if (v == "3x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res3X);
} else if (v == "4x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res4X);
} else if (v == "5x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res5X);
} else if (v == "6x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res6X);
} else if (v == "7x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res7X);
} else if (v == "8x") {
SET_OVERRIDE(resolution_setup, Settings::ResolutionSetup::Res8X);
}
}
else if (k == "async_gpu" || k == "use_asynchronous_gpu_emulation") {
SET_OVERRIDE(use_asynchronous_gpu_emulation, ToBool(v));
}
else if (k == "reactive_flushing" || k == "use_reactive_flushing") {
SET_OVERRIDE(use_reactive_flushing, ToBool(v));
}
else if (k == "multicore" || k == "use_multi_core") {
SET_OVERRIDE(use_multi_core, ToBool(v));
}
else if (k == "async_shaders" || k == "use_asynchronous_shaders") {
SET_OVERRIDE(use_asynchronous_shaders, ToBool(v));
}
else if (k == "pipeline_cache" || k == "use_vulkan_driver_pipeline_cache") {
SET_OVERRIDE(use_vulkan_driver_pipeline_cache, ToBool(v));
}
else if (k == "compute_pipelines" || k == "enable_compute_pipelines") {
SET_OVERRIDE(enable_compute_pipelines, ToBool(v));
}
else if (k == "use_disk_shader_cache") {
SET_OVERRIDE(use_disk_shader_cache, ToBool(v));
}
else if (k == "barrier_feedback_loops") {
SET_OVERRIDE(barrier_feedback_loops, ToBool(v));
}
else if (k == "async_presentation") {
SET_OVERRIDE(async_presentation, ToBool(v));
}
else if (k == "use_squashed_iterated_blend") {
s.use_squashed_iterated_blend = ToBool(v);
}
else if (k == "smo" || k == "sync_memory_operations") {
SET_OVERRIDE(sync_memory_operations, ToBool(v));
}
else if (k == "nvdec" || k == "nvdec_emulation") {
if (v == "off" || v == "disabled") {
SET_OVERRIDE(nvdec_emulation, Settings::NvdecEmulation::Off);
} else if (v == "cpu") {
SET_OVERRIDE(nvdec_emulation, Settings::NvdecEmulation::Cpu);
} else if (v == "gpu") {
SET_OVERRIDE(nvdec_emulation, Settings::NvdecEmulation::Gpu);
}
}
else if (k == "enable_buffer_history" || k == "buffer_history") {
SET_OVERRIDE(enable_buffer_history, ToBool(v));
}
else if (k == "fix_bloom_effects" || k == "fix_bloom") {
SET_OVERRIDE(fix_bloom_effects, ToBool(v));
}
else if (k == "dma_accuracy") {
if (v == "default") {
SET_OVERRIDE(dma_accuracy, Settings::DmaAccuracy::Default);
} else if (v == "unsafe" || v == "fast") {
SET_OVERRIDE(dma_accuracy, Settings::DmaAccuracy::Unsafe);
} else if (v == "safe" || v == "stable") {
SET_OVERRIDE(dma_accuracy, Settings::DmaAccuracy::Safe);
}
}
else if (k == "airplane_mode" || k == "airplane") {
SET_OVERRIDE(airplane_mode, ToBool(v));
}
else if (k == "controller_applet_mode" || k == "controller_applet") {
if (v == "lle" || v == "real") {
SET_OVERRIDE(controller_applet_mode, Settings::AppletMode::LLE);
} else if (v == "hle" || v == "custom") {
SET_OVERRIDE(controller_applet_mode, Settings::AppletMode::HLE);
}
}
else {
LOG_WARNING(Core, "Unknown game override setting: {}={}", key, value);
}
}
#undef SET_OVERRIDE
std::optional<Section> ParseSectionHeader(std::string_view header) {
if (header.size() < 2 || header.front() != '[' || header.back() != ']') {
return std::nullopt;
}
header = header.substr(1, header.size() - 2);
Section section{};
const auto pipe_pos = header.find('|');
std::string_view title_part = header;
std::string_view conditions_part;
if (pipe_pos != std::string_view::npos) {
title_part = header.substr(0, pipe_pos);
conditions_part = header.substr(pipe_pos + 1);
}
std::string title_str = Trim(title_part);
if (title_str.starts_with("0x") || title_str.starts_with("0X")) {
title_str = title_str.substr(2);
}
std::uint64_t title_id = 0;
auto [ptr, ec] = std::from_chars(title_str.data(), title_str.data() + title_str.size(),
title_id, 16);
if (ec != std::errc{}) {
return std::nullopt;
}
section.title_id = title_id;
if (!conditions_part.empty()) {
std::string cond_str(conditions_part);
std::istringstream stream(cond_str);
std::string token;
while (std::getline(stream, token, ',')) {
token = Trim(token);
const auto colon_pos = token.find(':');
if (colon_pos == std::string::npos) continue;
std::string cond_key = ToLower(Trim(token.substr(0, colon_pos)));
std::string cond_value = ToLower(Trim(token.substr(colon_pos + 1)));
if (cond_key == "vendor") {
section.condition.vendor = cond_value;
} else if (cond_key == "os") {
section.condition.os.push_back(cond_value);
} else if (cond_key == "cpu" || cond_key == "cpu_backend") {
section.condition.cpu_backend = cond_value;
}
}
}
return section;
}
std::string CpuBackendToString(Settings::CpuBackend backend) {
switch (backend) {
case Settings::CpuBackend::Dynarmic: return "jit";
case Settings::CpuBackend::Nce: return "nce";
default: return "unknown";
}
}
bool ConditionMatches(const Condition& cond, const GameSettings::EnvironmentInfo& env) {
if (cond.vendor.has_value() && cond.vendor.value() != VendorToString(env.vendor)) {
return false;
}
if (!cond.os.empty()) {
const std::string current_os = OSToString(env.os);
bool os_match = std::any_of(cond.os.begin(), cond.os.end(),
[&current_os](const std::string& os) { return os == current_os; });
if (!os_match) {
return false;
}
}
if (cond.cpu_backend.has_value()) {
const std::string current_cpu = CpuBackendToString(Settings::values.cpu_backend.GetValue());
const std::string& required = cond.cpu_backend.value();
if (required == "jit" || required == "dynarmic") {
if (current_cpu != "jit") return false;
} else if (required == "nce") {
if (current_cpu != "nce") return false;
} else {
return false;
}
}
return true;
}
std::vector<Section> ParseOverridesFile(const std::filesystem::path& path) {
std::vector<Section> sections;
std::ifstream file(path);
if (!file.is_open()) {
return sections;
}
Section* current_section = nullptr;
std::string line;
while (std::getline(file, line)) {
line = Trim(line);
if (line.empty() || line.front() == ';' || line.front() == '#') {
continue;
}
if (line.front() == '[' && line.back() == ']') {
auto section = ParseSectionHeader(line);
if (section) {
sections.push_back(std::move(*section));
current_section = &sections.back();
} else {
current_section = nullptr;
LOG_WARNING(Core, "Invalid section header in overrides.ini: {}", line);
}
continue;
}
if (current_section) {
const auto eq_pos = line.find('=');
if (eq_pos != std::string::npos) {
std::string setting_key = Trim(line.substr(0, eq_pos));
std::string setting_value = Trim(line.substr(eq_pos + 1));
current_section->settings.emplace_back(std::move(setting_key), std::move(setting_value));
}
}
}
return sections;
}
bool IsEarlySetting(const std::string& key) {
std::array early_settings = {
"backend",
"renderer_backend",
"multicore",
"use_multi_core",
};
const std::string k = ToLower(key);
return std::ranges::any_of(early_settings,
[&k](const char* s) { return k == s; });
}
bool EarlyConditionMatches(const Condition& cond, GameSettings::OS current_os,
const std::optional<GameSettings::GPUVendor>& detected_vendor) {
if (!cond.os.empty()) {
const std::string current_os_str = OSToString(current_os);
bool os_match = std::any_of(cond.os.begin(), cond.os.end(),
[&current_os_str](const std::string& os) { return os == current_os_str; });
if (!os_match) {
return false;
}
}
if (cond.vendor.has_value()) {
if (!detected_vendor.has_value()) {
return false;
}
if (cond.vendor.value() != VendorToString(*detected_vendor)) {
return false;
}
}
if (cond.cpu_backend.has_value()) {
const std::string current_cpu = CpuBackendToString(Settings::values.cpu_backend.GetValue());
const std::string& required = cond.cpu_backend.value();
if (required == "jit" || required == "dynarmic") {
if (current_cpu != "jit") return false;
} else if (required == "nce" /* || required == "native" */) {
if (current_cpu != "nce") return false;
} else {
return false;
}
}
return true;
}
int GetSpecificity(const Section& s) {
int score = 0;
if (s.condition.vendor.has_value()) score++;
if (!s.condition.os.empty()) score++;
if (s.condition.cpu_backend.has_value()) score++;
return score;
}
} // anonymous namespace
std::filesystem::path GetOverridesPath() {
return Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "overrides.ini";
}
bool OverridesFileExists() {
return std::filesystem::exists(GetOverridesPath());
}
std::optional<std::uint32_t> GetOverridesFileVersion() {
const auto path = GetOverridesPath();
std::ifstream file(path);
if (!file.is_open()) {
return std::nullopt;
}
std::string line;
if (std::getline(file, line)) {
line = Trim(line);
if (line.starts_with(kVersionPrefix)) {
const auto version_str = line.substr(std::strlen(kVersionPrefix));
std::uint32_t version = 0;
auto [ptr, ec] = std::from_chars(version_str.data(),
version_str.data() + version_str.size(), version);
if (ec == std::errc{}) {
return version;
}
}
}
return std::nullopt;
}
void ApplyEarlyOverrides(std::uint64_t program_id, const std::string& gpu_vendor) {
const auto path = GetOverridesPath();
if (!std::filesystem::exists(path)) {
return;
}
const auto current_os = DetectOS();
std::optional<GameSettings::GPUVendor> detected_vendor;
if (!gpu_vendor.empty()) {
detected_vendor = DetectGPUVendor(gpu_vendor);
}
auto sections = ParseOverridesFile(path);
// Filter matching sections
std::vector<Section> matching;
for (auto& section : sections) {
if (section.title_id != program_id) continue;
if (!EarlyConditionMatches(section.condition, current_os, detected_vendor)) continue;
matching.push_back(std::move(section));
}
// Sort by base first, then os, then vendor, then both
std::sort(matching.begin(), matching.end(),
[](const Section& a, const Section& b) {
return GetSpecificity(a) < GetSpecificity(b);
});
// Apply only early settings, only apply before renderer overrides
for (const auto& section : matching) {
for (const auto& [setting_key, setting_value] : section.settings) {
if (IsEarlySetting(setting_key)) {
LOG_INFO(Core, "Game override {:016X}: {}={}",
program_id, setting_key, setting_value);
ApplySetting(setting_key, setting_value);
}
}
}
}
void ApplyLateOverrides(std::uint64_t program_id, const VideoCore::RendererBase& renderer) {
const auto path = GetOverridesPath();
if (!std::filesystem::exists(path)) {
return;
}
const auto env = GameSettings::DetectEnvironment(renderer);
auto sections = ParseOverridesFile(path);
// Filter matching sections
std::vector<Section> matching;
for (auto& section : sections) {
if (section.title_id != program_id) continue;
if (!ConditionMatches(section.condition, env)) continue;
matching.push_back(std::move(section));
}
// Sort by base first, then single condition, then multiple
std::sort(matching.begin(), matching.end(),
[](const Section& a, const Section& b) {
return GetSpecificity(a) < GetSpecificity(b);
});
// Apply settings (skip early ones, they were already applied, like renderr_backend)
for (const auto& section : matching) {
for (const auto& [setting_key, setting_value] : section.settings) {
if (!IsEarlySetting(setting_key)) {
LOG_INFO(Core, "Game override {:016X}: {}={}",
program_id, setting_key, setting_value);
ApplySetting(setting_key, setting_value);
}
}
}
}
} // namespace Core::GameOverrides
-40
View File
@@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
#include <filesystem>
#include <optional>
#include <string>
#include <vector>
namespace VideoCore {
class RendererBase;
}
namespace Core::GameOverrides {
// used to check against GitHub Release Version
// repo at: https://github.com/eden-emulator/eden-overrides
inline constexpr std::uint32_t kOverridesVersion = 1;
inline constexpr const char* kVersionPrefix = "; version=";
struct Condition {
std::optional<std::string> vendor;
std::vector<std::string> os;
std::optional<std::string> cpu_backend;
};
struct Section {
std::uint64_t title_id{};
Condition condition;
std::vector<std::pair<std::string, std::string>> settings;
};
std::filesystem::path GetOverridesPath();
bool OverridesFileExists();
std::optional<std::uint32_t> GetOverridesFileVersion();
void ApplyEarlyOverrides(std::uint64_t program_id, const std::string& gpu_vendor);
void ApplyLateOverrides(std::uint64_t program_id, const VideoCore::RendererBase& renderer);
} // namespace Core::GameOverrides
+17 -23
View File
@@ -1,8 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include "core/game_settings.h"
#include "core/game_overrides.h"
#include <algorithm>
#include <cctype>
@@ -61,26 +60,15 @@ static GPUVendor GetGPU(const std::string& gpu_vendor_string) {
}
}
// legacy (shouldn't be needed anymore, but just in case)
std::string gpu = gpu_vendor_string;
std::transform(gpu.begin(), gpu.end(), gpu.begin(), [](unsigned char c){ return (char)std::tolower(c); });
if (gpu.find("geforce") != std::string::npos) {
return GPUVendor::Nvidia;
}
if (gpu.find("amd") != std::string::npos || gpu.find("radeon") != std::string::npos || gpu.find("ati") != std::string::npos) {
if (gpu.find("radeon") != std::string::npos || gpu.find("ati") != std::string::npos) {
return GPUVendor::AMD;
}
if (gpu.find("intel") != std::string::npos) {
return GPUVendor::Intel;
}
if (gpu.find("apple") != std::string::npos) {
return GPUVendor::Apple;
}
if (gpu.find("qualcomm") != std::string::npos || gpu.find("adreno") != std::string::npos) {
return GPUVendor::Qualcomm;
}
if (gpu.find("mali") != std::string::npos) {
return GPUVendor::ARM;
}
return GPUVendor::Unknown;
}
@@ -131,16 +119,22 @@ EnvironmentInfo DetectEnvironment(const VideoCore::RendererBase& renderer) {
return env;
}
void LoadEarlyOverrides(std::uint64_t program_id, const std::string& gpu_vendor, bool enabled) {
if (enabled && GameOverrides::OverridesFileExists()) {
GameOverrides::ApplyEarlyOverrides(program_id, gpu_vendor);
}
}
void LoadOverrides(std::uint64_t program_id, const VideoCore::RendererBase& renderer) {
const auto env = DetectEnvironment(renderer);
void LoadOverrides(std::uint64_t program_id, const VideoCore::RendererBase& renderer, bool enabled) {
if (enabled && GameOverrides::OverridesFileExists()) {
GameOverrides::ApplyLateOverrides(program_id, renderer);
switch (static_cast<TitleID>(program_id)) {
case TitleID::NinjaGaidenRagebound:
Settings::values.use_squashed_iterated_blend = true;
break;
default:
break;
}
LOG_INFO(Core, "Applied game settings for title ID {:016X} on OS {}, GPU vendor {} ({})",
program_id,
static_cast<int>(env.os),
static_cast<int>(env.vendor),
env.vendor_string);
}
} // namespace Core::GameSettings
+2 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -55,8 +55,6 @@ struct EnvironmentInfo {
EnvironmentInfo DetectEnvironment(const VideoCore::RendererBase& renderer);
void LoadEarlyOverrides(std::uint64_t program_id, const std::string& gpu_vendor, bool enabled = true);
void LoadOverrides(std::uint64_t program_id, const VideoCore::RendererBase& renderer, bool enabled = true);
void LoadOverrides(std::uint64_t program_id, const VideoCore::RendererBase& renderer);
} // namespace Core::GameSettings
+3 -3
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
@@ -78,7 +78,7 @@ private:
std::array<DebugWatchpoint, Core::Hardware::NUM_WATCHPOINTS> m_watchpoints{};
std::map<KProcessAddress, u64> m_debug_page_refcounts{};
#ifdef HAS_NCE
std::unordered_map<u64, u64> m_post_handlers{};
ankerl::unordered_dense::map<u64, u64> m_post_handlers{};
#endif
std::unique_ptr<Core::ExclusiveMonitor> m_exclusive_monitor;
Core::Memory::Memory m_memory;
@@ -493,7 +493,7 @@ public:
static void Switch(KProcess* cur_process, KProcess* next_process);
#ifdef HAS_NCE
std::unordered_map<u64, u64>& GetPostHandlers() noexcept {
ankerl::unordered_dense::map<u64, u64>& GetPostHandlers() noexcept {
return m_post_handlers;
}
#endif
+4 -4
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -10,7 +10,7 @@
#include <functional>
#include <memory>
#include <thread>
#include <unordered_set>
#include <ankerl/unordered_dense.h>
#include <utility>
#include "common/assert.h"
@@ -786,8 +786,8 @@ struct KernelCore::Impl {
std::optional<KObjectNameGlobalData> object_name_global_data;
std::unordered_set<KAutoObject*> registered_objects;
std::unordered_set<KAutoObject*> registered_in_use_objects;
ankerl::unordered_dense::set<KAutoObject*> registered_objects;
ankerl::unordered_dense::set<KAutoObject*> registered_in_use_objects;
std::mutex server_lock;
std::vector<std::unique_ptr<Service::ServerManager>> server_managers;
+2 -2
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
@@ -11,7 +11,7 @@
#include <list>
#include <memory>
#include <string>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include <vector>
#include "common/polyfill_thread.h"
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
@@ -7,7 +7,7 @@
#pragma once
#include <array>
#include <unordered_map>
#include <ankerl/unordered_dense.h>
#include <vector>
#include "common/common_funcs.h"
@@ -176,6 +176,6 @@ struct WebCommonReturnValue {
};
static_assert(sizeof(WebCommonReturnValue) == 0x1010, "WebCommonReturnValue has incorrect size.");
using WebArgInputTLVMap = std::unordered_map<WebArgInputTLVType, std::vector<u8>>;
using WebArgInputTLVMap = ankerl::unordered_dense::map<WebArgInputTLVType, std::vector<u8>>;
} // namespace Service::AM::Frontend
@@ -44,8 +44,8 @@ std::vector<u8> default_logo_small;
std::vector<u8> default_logo_large;
bool default_logos_loaded = false;
std::unordered_map<std::string, std::vector<u8>> news_images_small;
std::unordered_map<std::string, std::vector<u8>> news_images_large;
ankerl::unordered_dense::map<std::string, std::vector<u8>> news_images_small;
ankerl::unordered_dense::map<std::string, std::vector<u8>> news_images_large;
std::mutex images_mutex;

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