diff --git a/CMakeLists.txt b/CMakeLists.txt index 4490df21cb..4ab08739f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -196,7 +196,7 @@ option(YUZU_USE_BUNDLED_SIRIT "Download bundled sirit" ${BUNDLED_SIRIT_DEFAULT}) # FreeBSD 15+ has libusb, versions below should disable it cmake_dependent_option(ENABLE_LIBUSB "Enable the use of LibUSB" ON "WIN32 OR PLATFORM_LINUX OR PLATFORM_FREEBSD OR APPLE" OFF) -cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT WIN32 OR NOT ARCHITECTURE_arm64" OFF) +cmake_dependent_option(ENABLE_OPENGL "Enable OpenGL" ON "NOT (WIN32 AND ARCHITECTURE_arm64) AND NOT APPLE" OFF) mark_as_advanced(FORCE ENABLE_OPENGL) option(ENABLE_WEB_SERVICE "Enable web services (telemetry, etc.)" ON) diff --git a/docs/Options.md b/docs/Options.md index 55aead805f..3eb6effe92 100644 --- a/docs/Options.md +++ b/docs/Options.md @@ -70,8 +70,8 @@ These options control executables and build flavors. The following options are desktop only. -- `ENABLE_LIBUSB` (ON) Enable the use of the libusb input frontend (HIGHLY RECOMMENDED) -- `ENABLE_OPENGL` (ON) Enable the OpenGL graphics frontend +- `ENABLE_LIBUSB` (ON) Enable the use of the libusb input backend (HIGHLY RECOMMENDED) +- `ENABLE_OPENGL` (ON) Enable the OpenGL graphics backend - Unavailable on Windows/ARM64 - You probably shouldn't turn this off. diff --git a/docs/user/AlterDateTime.md b/docs/user/AlterDateTime.md index 43bd3ed7b1..aeffa1a548 100644 --- a/docs/user/AlterDateTime.md +++ b/docs/user/AlterDateTime.md @@ -1,6 +1,6 @@ # Setting a Custom Date/Time in Eden -Use this guide whenever you want to modify the Date or Time that Eden reports to games. This can be useful for modifying RNG elements, skipping wait times in games, etc. +Use this guide whenever you want to modify the Date or Time that Eden reports to games. This can be useful for modifying RNG elements, skipping wait times in games, etc. **Click [Here](https://evilperson1337.notion.site/Setting-a-Custom-Date-Time-in-Eden-2b357c2edaf680acb8d4e63ccc126564) for a version of this guide with images & visual elements.** @@ -16,5 +16,5 @@ Use this guide whenever you want to modify the Date or Time that Eden reports to 1. Navigate to *Emulation → Configure*. 2. Click on the **System** item on the left-hand side navigation, then check the *Custom RTC Date* box. -3. The Date/Time option now becomes editable. Set it to the value you want and hit **OK**. -4. GREAT SCOTT! We have time traveled! You can of course go forward or backward in time (as long as it is not before the year 1970) and your game should update accordingly (e.g. certain *Super Mario Odyssey* moons that take time for flowers to grow will now be fully grown.). \ No newline at end of file +3. The Date/Time option now becomes editable. Set it to the value you want and hit **OK**. +4. GREAT SCOTT! We have time traveled! You can of course go forward or backward in time (as long as it is not before the year 1970) and your game should update accordingly (e.g. certain *Super Mario Odyssey* moons that take time for flowers to grow will now be fully grown.). \ No newline at end of file diff --git a/docs/user/Basics.md b/docs/user/Basics.md index 5101f4d9c3..794a0935d5 100644 --- a/docs/user/Basics.md +++ b/docs/user/Basics.md @@ -16,6 +16,15 @@ The CPU must support FMA for an optimal gameplay experience. The GPU needs to su If your GPU doesn't support or is just behind by a minor version, see Mesa environment variables below (*nix only). +## Releases and versions + +- Stable releases/Versioned releases: Has a version number and it's the versions we expect 3rd party repositories to host (package managers and such), these are, well, stable, have low amount of regressions (wrt. to master and nightlies) and generally focus on "keeping things without regressions", recommended for the average user. + - RC releases: Release candidate, generally "less stable but still stable" versions. + - Full release: "The stablest possible you could get". +- Nightly: Builds done around 2PM UTC (if there are any changes), generally stable, but not recommended for the average user. These contain daily updates and may contain critical fixes for some games. +- Master: Unstable builds, can lead from a game working exceptionally fine to absolute crashing in some systems because someone forgot to check if NixOS or Solaris worked. These contain straight from the oven fixes, please don't use them unless you plan to contribute something! They're very experimental! Still 95% of the time it will work just fine. +- PR builds: Highly experimental builds, testers may grab from these. The average user should treat them the same as master builds, except sometimes they straight up don't build/work. + ## User configuration ### Configuration directories diff --git a/docs/user/CommandLine.md b/docs/user/CommandLine.md index 4b3a5bdf58..cd98d88b19 100644 --- a/docs/user/CommandLine.md +++ b/docs/user/CommandLine.md @@ -7,6 +7,7 @@ There are two main applications, an SDL2 based app (`eden-cli`) and a Qt based a - `-g `: Alternate way to specify what to load, overrides. However let it be noted that arguments that use `-` will be treated as options/ignored, if your game, for some reason, starts with `-`, in order to safely handle it you may need to specify it as an argument. - `-f`: Use fullscreen. - `-u `: Select the index of the user to load as. +- `-input-profile `: Specifies input profile name to use (for player #0 only). - `-qlaunch`: Launch QLaunch. - `-setup`: Launch setup applet. @@ -20,3 +21,4 @@ There are two main applications, an SDL2 based app (`eden-cli`) and a Qt based a - `--program/-p`: Specify the program arguments to pass (optional). - `--user/-u`: Specify the user index. - `--version/-v`: Display version and quit. +- `--input-profile/-i`: Specifies input profile name to use (for player #0 only). diff --git a/docs/user/ControllerProfiles.md b/docs/user/ControllerProfiles.md deleted file mode 100644 index 3b4898f447..0000000000 --- a/docs/user/ControllerProfiles.md +++ /dev/null @@ -1,49 +0,0 @@ -# Configuring Controller Profiles - -Use this guide for when you want to configure specific controller settings to be reused. - -**Click [Here](https://evilperson1337.notion.site/Configuring-Controller-Profiles-2be57c2edaf680eabc3ac8c333ec75c4) for a version of this guide with images & visual elements.** - ---- - -### Pre-Requisites - -- Eden Set Up and Configured - ---- - -### Steps -1. Launch Eden and wait for it to load. -2. Navigate to *Emulation > Configure…* -3. Select **Controls** from the left-hand menu and configure your controller for the way you want it to be in game. -4. Select **New** and enter a name for the profile in the box that appears. Press **OK** to save the profile settings. -5. Select **OK** to close the settings menu. - -## Setting Controller Profiles By Game - -Use this guide when you want to set up specific controller profiles for specific games. This can be useful for certain games like *Captain Toad Treasure Tracker* where a blue dot appears in the middle of the screen when you have docked mode enabled, but not handheld mode. - -**Click [Here](https://evilperson1337.notion.site/Setting-Controller-Profiles-By-Game-2b057c2edaf681658a57f0c199cb6083) for a version of this guide with images & visual elements.** - ---- - -### Pre-Requisites - -- Eden Emulator set up and fully configured -- Controller Profile Created - - See [*Configuring Controller Profiles*](./ControllerProfiles.md) for instructions on how to do this if needed. - ---- - -### Steps - -1. *Right-Click* the game you want to apply the profile to in the main window and select **Properties.** -2. Navigate to the **Input Profiles** tab in the window that appears. Drop down on *Player 1 profile* (or whatever player profile you want to apply it to) and select the profile you want. - - -1. Click **OK** to apply the profile mapping. -2. Launch the game and confirm that the profile is applied, regardless of what the global configuration is. diff --git a/docs/user/Controllers.md b/docs/user/Controllers.md new file mode 100644 index 0000000000..6aac9056ff --- /dev/null +++ b/docs/user/Controllers.md @@ -0,0 +1,65 @@ +# User Handbook - Controllers + +Most of the controls should work out of the box. If not, please use a joystick calibrator to ensure it's not an issue with your own controller, for example: + +- https://github.com/dkosmari/calibrate-joystick + +## Using external controllers on the Steamdeck + +In desktop mode ignore your pro controller/xbox contoller external controller and use **Steam Virtual Gamepad 0 as Player 1**. If you have multiple external controllers set **Player 2 to Steam Virtual Gamepad 1**. Steam app must not be closed on desktop mode. + +Here's the annoying part of it. When waking up the steam deck from sleep try not to touch any button on the Steamdeck and turn on your external controller. Then open the Eden.AppImage. If you're lucky you can get your external controller to be position 0 and also Steam Virtual Gamepad 0 in desktop mode. If not that is ok too unless you need to configure player 1 to have gyro. You might need to repeat this to get your external controller as Steam Virtual Gamepad 0 so you can config Player 1 having gyro. You might be able to config player 1 to have gyro with the Steamdeck itself. Or you can also config player 1, 2, 3, etc, to have gyro somehow. Make sure they are all using Virtual Gamepads though. + +Turn off controller then go to gaming mode. Try not to touch any buttons on the physical Steamdeck. When in gaming mode turn on the external controller. If lucky it will be assigned as Steam Virtual Gamepad 0. If not just use steam Gamemode feature to rearrange controller positions order. + +Basically the Steamdeck or the external controller is fighting for position 0 and it depends on what is touched first after waking from sleep. + +## Configuring Controller Profiles + +Use this guide for when you want to configure specific controller settings to be reused. + +**Click [Here](https://evilperson1337.notion.site/Configuring-Controller-Profiles-2be57c2edaf680eabc3ac8c333ec75c4) for a version of this guide with images & visual elements.** + +--- + +#### Pre-Requisites + +- Eden Set Up and Configured + +--- + +#### Steps +1. Launch Eden and wait for it to load. +2. Navigate to *Emulation > Configure…* +3. Select **Controls** from the left-hand menu and configure your controller for the way you want it to be in game. +4. Select **New** and enter a name for the profile in the box that appears. Press **OK** to save the profile settings. +5. Select **OK** to close the settings menu. + +### Setting Controller Profiles By Game + +Use this guide when you want to set up specific controller profiles for specific games. This can be useful for certain games like *Captain Toad Treasure Tracker* where a blue dot appears in the middle of the screen when you have docked mode enabled, but not handheld mode. + +**Click [Here](https://evilperson1337.notion.site/Setting-Controller-Profiles-By-Game-2b057c2edaf681658a57f0c199cb6083) for a version of this guide with images & visual elements.** + +--- + +#### Pre-Requisites + +- Eden Emulator set up and fully configured +- Controller Profile Created + - See [*Configuring Controller Profiles*](./ControllerProfiles.md) for instructions on how to do this if needed. + +--- + +#### Steps + +1. *Right-Click* the game you want to apply the profile to in the main window and select **Properties.** +2. Navigate to the **Input Profiles** tab in the window that appears. Drop down on *Player 1 profile* (or whatever player profile you want to apply it to) and select the profile you want. + + +1. Click **OK** to apply the profile mapping. +2. Launch the game and confirm that the profile is applied, regardless of what the global configuration is. diff --git a/docs/user/README.md b/docs/user/README.md index 9804f4d62f..c1c4cd200a 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -11,10 +11,12 @@ A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/ - **[The Basics](Basics.md)** - **[Quickstart](./QuickStart.md)** - **[Settings](./Settings.md)** -- **[Installing Mods](./Mods.md)** -- **[Run On macOS](./RunOnMacOS.md)** +- **[Controllers](./Controllers.md)** + - **[Controller profiles](./Controllers.md#configuring-controller-profiles)** - **[Audio](Audio.md)** - **[Graphics](Graphics.md)** +- **[Installing Mods](./Mods.md)** +- **[Run On macOS](./RunOnMacOS.md)** - **[Data, Savefiles and Storage](Storage.md)** - **[Orphaned Profiles](Orphaned.md)** - **[Troubleshooting](./Troubleshoot.md)** @@ -23,7 +25,6 @@ A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/ - **[Importing Saves](./ImportingSaves.md)** - **[Installing Atmosphere Mods](./InstallingAtmosphereMods.md)** - **[Installing Updates & DLCs](./InstallingUpdatesDLC.md)** -- **[Controller Profiles](./ControllerProfiles.md)** - **[Alter Date & Time](./AlterDateTime.md)** ## 3rd-party Integration @@ -35,6 +36,7 @@ A copy of this handbook is [available online](https://git.eden-emu.dev/eden-emu/ - **[Obtainium](./ThirdParty.md#configuring-obtainium)** - **[ES-DE](./ThirdParty.md#configuring-es-de)** - **[Mirrors](./ThirdParty.md#mirrors)** + - **[GameMode](./ThirdParty.md#configuring-gamemode)** ## Advanced diff --git a/docs/user/RunOnMacOS.md b/docs/user/RunOnMacOS.md index e01bf0253d..2729e13ced 100644 --- a/docs/user/RunOnMacOS.md +++ b/docs/user/RunOnMacOS.md @@ -1,4 +1,12 @@ -# Allowing Eden to Run on MacOS +# User Handbook - Run on macOS + +Current macOS support is still experimental and very reliant on MoltenVK developments, plans have shifted to properly provide support for KosmicKrisp and similar new GPU endeavours, but macOS users still are bound to MoltenVK itself. + +Users of macOS may wish to use [Asahi Linux](https://wiki.gentoo.org/wiki/Project:Asahi/Guide) for the rising KosmicKrisp support. + +As of writing, neither macOS nor Asahi has support for NCE; additionally Asahi has extraneous paging bugs with fastmem. + +## Allowing Eden to Run on MacOS Use this guide when you need to allow Eden to run on a Mac system, but are being blocked by Apple Security policy. @@ -6,19 +14,19 @@ Use this guide when you need to allow Eden to run on a Mac system, but are being --- -### Pre-Requisites +#### Pre-Requisites - Permissions to modify settings in MacOS --- -## Why am I Seeing This? +### Why am I Seeing This? Recent versions of MacOS (Catalina & newer) introduced the **Gatekeeper** security functionality, requiring software to be signed by Apple or a trusted (aka - paying) developer. If the signature isn’t on the list of trusted ones, it will stop the program from executing and display the message above. --- -## Steps +### Steps 1. Open the *System Settings* panel. 2. Navigate to *Privacy & Security*. diff --git a/docs/user/Settings.md b/docs/user/Settings.md index 35fcd0c9ef..9153a27e4d 100644 --- a/docs/user/Settings.md +++ b/docs/user/Settings.md @@ -50,5 +50,4 @@ See also [an extended breakdown of some options](./Graphics.md). ## Controls -Most of the controls should work out of the box. If not, please use a joystick calibrator to ensure it's not an issue with your own controller, for example: -- https://github.com/dkosmari/calibrate-joystick +See [controllers](./Controllers.md). diff --git a/docs/user/Testing.md b/docs/user/Testing.md index 6c3b7d3e15..eb3beeb37c 100644 --- a/docs/user/Testing.md +++ b/docs/user/Testing.md @@ -1,39 +1,99 @@ -# User Handbook - Testing - -While this is mainly aimed for testers - normal users can benefit from these guidelines to make their life easier when trying to outline and/or report an issue. - -## Getting logs - -In order to get more information, you can find logs in the following location: - - -## How to Test a PR Against the Based Master When Issues Arise +# Testing When you're testing a pull request (PR) and encounter unexpected behavior, it's important to determine whether the issue was introduced by the PR or if it already exists in the base code. To do this, compare the behavior against the based master branch. Even before an issue occurs, it is best practice to keep the same settings and delete the shader cache. Using an already made shader cache can make the PR look like it is having a regression in some rare cases. -### What to Do When Something Seems Off +Try not to test PRs which are for documentation or extremely trivial changes (like a PR that changes the app icon), unless you really want to; generally avoid any PRs marked `[docs]`. + +If a PR specifies it is for a given platform (i.e `linux`) then just test on Linux. If it says `NCE` then test on Android and Linux ARM64 (Raspberry Pi and such). macOS fixes may also affect Asahi, test that if you can too. + +You may also build artifacts yourself, be aware that the resulting builds are NOT the same as those from CI, because of package versioning and build environment differences. One famous example is FFmpeg randomly breaking on many Arch distros due to packaging differences. + +## Quickstart + +Think of the source code as a "tree", with the "trunk" of that tree being our `master` branch, any other branches are PRs or separate development branches, only our stable releases pull from `master` - all other branches are considered unstable and aren't recommended to pull from unless you're testing multiple branches at once. + +Here's some terminology you may want to familiarize yourself with: + +- PR: Pull request, a change in the codebase; from which the author of said change (the programmer) requests a pull of that branch into master (make it so the new code makes it into a release basically). +- Bisect: Bilinear method of searching regressions, some regressions may be sporadic and can't be bisected, but the overwhelming majority are. +- WIP: Work-in-progress. +- Regression: A new bug/glitch caused by new code, i.e "Zelda broke in android after commit xyz". +- Master: The "root" branch, this is where all merged code goes to, traditionally called `main`, `trunk` or just `master`, it contains all the code that eventually make it to stable releases. +- `HEAD`: Latest commit in a given branch, `HEAD` of `master` is the latest commit on branch `master`. +- `origin`: The default "remote", basically the URL from where git is located at, for most of the time that location is https://git.eden-emu.dev/eden-emu/eden. + +## Testing checklist + +For regressions/bugs from PRs or commits: + +- [ ] Occurs in master? + - If it occurs on master: + - [ ] Occurs on previous stable release? (before this particular PR). + - If it occurs on previous stable release: + - [ ] Occurs on previous-previous stable release? + - And so on and so forth... some bugs come from way before Eden was even conceived. + - Otherwise, try bisecting between the previous stable release AND the latest `HEAD` of master + - [ ] Occurs in given commit? +- [ ] Occurs in PR? + - If it occurs on PR: + - [ ] Bisected PR? (if it has commits) + - [ ] Found bisected commit? + +If an issue sporadically appears, try to do multiple runs, try if possible, to count the number of times it has failed and the number of times it has "worked just fine"; say it worked 3 times but failed 1. then there is a 1/4th chance every run that the issue is replicated - so every bisect step would require 4 runs to ensure there is atleast a chance of triggering the bug. + +## What to do when something seems off + If you notice something odd during testing: + - Reproduce the issue using the based master branch. - Observe whether the same behavior occurs. -### Two Possible Outcomes +From there onwards there can be two possible outcomes: + - If the issue exists in the based master: This means the problem was already present before the PR. The PR most likely did not introduce the regression. - If the issue does not exist in the based master: This suggests the PR most likely introduced the regression and needs further investigation. -### Report your findings +## Reporting Your Findings + When you report your results: + - Clearly state whether the behavior was observed in the based master. -- Indicate whether the result is good (expected behavior) or bad (unexpected or broken behavior). Without mentioning if your post/report/log is good or bad it may confuse the Developer of the PR. -- Example: -``` -1. "Tested on based master — issue not present. Bad result for PR, likely regression introduced." -2. "Tested on based master — issue already present. Good result for PR, not a regression." -``` +- Indicate whether the result is good (expected behavior) or bad (unexpected or broken behavior). Without mentioning if your post/report/log is good or bad it may confuse the developer of the PR. + +For example: + +1. "Bad result for PR: Tested on based master - issue not present. Likely regression introduced." +2. "Good result for PR: Tested on based master - issue already present. Not a regression." + +This approach helps maintain clarity and accountability in the testing process and ensures regressions are caught and addressed efficiently. + +If the behavior seems normal for a certain game/feature then it may not be always required to check against the based master. This approach helps maintain clarity and accountability in the testing process and ensures regressions are caught and addressed efficiently. If the behavior seems normal for a certain game/feature then it may not be always required to check against the based master. If a master build for the PR' based master does not exist. It will be helpful to just test past and future builds nearby. That would help with gathering more information about the problem. -**Always include [debugging info](../Debug.md) as needed**. \ No newline at end of file +**Always include [debugging info](../Debug.md) as needed**. + +## Bisecting + +One happy reminder, when testing, *know how to bisect!* + +Say you're trying to find an issue between 1st of Jan and 8th of Jan, you can search by dividing "in half" the time between each commit: +- Check for 4th of Jan +- If 4th of Jan is "working" then the issue must be in the future +- So then check 6th of Jan +- If 6th of Jan isn't working then the issue must be in the past +- So then check 5th of Jan +- If 5th of Jan worked, then the issue starts at 6th of Jan + +The faulty commit then, is 6th of Jan. This is called bisection https://git-scm.com/docs/git-bisect + +## Notes + +- PR's marked with **WIP** do NOT need to be tested unless explicitly asked (check the git in case) +- Sometimes license checks may fail, hover over the build icon to see if builds did succeed, as the CI will push builds even if license checks fail. +- All open PRs can be viewed [here](https://git.eden-emu.dev/eden-emu/eden/pulls/). +- If site is down use one of the [mirrors](./user/ThirdParty.md#mirrors). diff --git a/docs/user/ThirdParty.md b/docs/user/ThirdParty.md index 083542cd3e..5bd72ebe72 100644 --- a/docs/user/ThirdParty.md +++ b/docs/user/ThirdParty.md @@ -4,7 +4,6 @@ The Eden emulator by itself lacks some functionality - or otherwise requires ext While most of the links mentioned in this guide are relatively "safe"; we urge users to use their due diligence and appropriatedly verify the integrity of all files downloaded and ensure they're not compromised. -- [Nightly Eden builds](https://github.com/pflyly/eden-nightly) - [NixOS Eden Flake](https://github.com/Grantimatter/eden-flake) - [ES-DE Frontend Support](https://github.com/GlazedBelmont/es-de-android-custom-systems) @@ -66,3 +65,9 @@ Note: Even though the site isn't Codeberg, it uses the same Forgejo/Gitea backen ```xml %EMULATOR_EDEN% %ACTION%=android.nfc.action.TECH_DISCOVERED %DATA%=%ROMPROVIDER% ``` + +## Configuring GameMode + +There is a checkbox to enable gamemode automatically. The `libgamemode.so` library must be findable on the standard `LD_LIBRARY_PATH` otherwise it will not properly be enabled. If for whatever reason it doesn't work, see [Arch wiki: GameMode](https://wiki.archlinux.org/title/GameMode) for more info. + +You may launch the emulator directly via the wrapper `gamemode `, and things should work out of the box. diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index c642dbdcda..b899ca07fa 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -73,6 +73,11 @@ SPDX-License-Identifier: GPL-3.0-or-later android:theme="@style/Theme.Yuzu.Main" android:label="@string/preferences_settings"/> + + ) : title = YuzuApplication.appContext.getString(applet.titleId), path = appletPath ) - val action = HomeNavigationDirections.actionGlobalEmulationActivity(appletGame) - binding.root.findNavController().navigate(action) + binding.root.findNavController().navigate( + R.id.action_global_emulationActivity, + bundleOf( + "game" to appletGame, + "custom" to false + ) + ) } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 11be703536..d33bbc3d7d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -111,18 +111,10 @@ class SettingsActivity : AppCompatActivity() { if (navHostFragment.childFragmentManager.backStackEntryCount > 0) { navHostFragment.navController.popBackStack() } else { - finishWithFragmentLikeAnimation() + finish() } } - private fun finishWithFragmentLikeAnimation() { - finish() - overridePendingTransition( - androidx.navigation.ui.R.anim.nav_default_pop_enter_anim, - androidx.navigation.ui.R.anim.nav_default_pop_exit_anim - ) - } - override fun onStart() { super.onStart() if (!DirectoryInitialization.areDirectoriesReady) { @@ -178,7 +170,7 @@ class SettingsActivity : AppCompatActivity() { getString(R.string.settings_reset), Toast.LENGTH_LONG ).show() - finishWithFragmentLikeAnimation() + finish() } private fun setInsets() { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt new file mode 100644 index 0000000000..11ecd355fb --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsSubscreenActivity.kt @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.features.settings.ui + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.bundleOf +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.navArgs +import com.google.android.material.color.MaterialColors +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.InsetsHelper +import org.yuzu.yuzu_emu.utils.ThemeHelper + +enum class SettingsSubscreen { + PROFILE_MANAGER, + DRIVER_MANAGER, + DRIVER_FETCHER, + FREEDRENO_SETTINGS, + APPLET_LAUNCHER, + INSTALLABLE, + GAME_FOLDERS, + ABOUT, + LICENSES, + GAME_INFO, + ADDONS, +} + +class SettingsSubscreenActivity : AppCompatActivity() { + private lateinit var binding: ActivitySettingsBinding + + private val args by navArgs() + + override fun attachBaseContext(base: Context) { + super.attachBaseContext(YuzuApplication.applyLanguage(base)) + } + + override fun onCreate(savedInstanceState: Bundle?) { + ThemeHelper.setTheme(this) + + super.onCreate(savedInstanceState) + + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + if (savedInstanceState == null) { + val navController = navHostFragment.navController + val navGraph = navController.navInflater.inflate( + R.navigation.settings_subscreen_navigation + ) + navGraph.setStartDestination(resolveStartDestination()) + navController.setGraph(navGraph, createStartDestinationArgs()) + } + + WindowCompat.setDecorFitsSystemWindows(window, false) + + if (InsetsHelper.getSystemGestureType(applicationContext) != + InsetsHelper.GESTURE_NAVIGATION + ) { + binding.navigationBarShade.setBackgroundColor( + ThemeHelper.getColorWithOpacity( + MaterialColors.getColor( + binding.navigationBarShade, + com.google.android.material.R.attr.colorSurface + ), + ThemeHelper.SYSTEM_BAR_ALPHA + ) + ) + } + + onBackPressedDispatcher.addCallback( + this, + object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() = navigateBack() + } + ) + + setInsets() + } + + override fun onStart() { + super.onStart() + if (!DirectoryInitialization.areDirectoriesReady) { + DirectoryInitialization.start() + } + } + + fun navigateBack() { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment + if (!navHostFragment.navController.popBackStack()) { + finish() + } + } + + private fun resolveStartDestination(): Int = + when (args.destination) { + SettingsSubscreen.PROFILE_MANAGER -> R.id.profileManagerFragment + SettingsSubscreen.DRIVER_MANAGER -> R.id.driverManagerFragment + SettingsSubscreen.DRIVER_FETCHER -> R.id.driverFetcherFragment + SettingsSubscreen.FREEDRENO_SETTINGS -> R.id.freedrenoSettingsFragment + SettingsSubscreen.APPLET_LAUNCHER -> R.id.appletLauncherFragment + SettingsSubscreen.INSTALLABLE -> R.id.installableFragment + SettingsSubscreen.GAME_FOLDERS -> R.id.gameFoldersFragment + SettingsSubscreen.ABOUT -> R.id.aboutFragment + SettingsSubscreen.LICENSES -> R.id.licensesFragment + SettingsSubscreen.GAME_INFO -> R.id.gameInfoFragment + SettingsSubscreen.ADDONS -> R.id.addonsFragment + } + + private fun createStartDestinationArgs(): Bundle = + when (args.destination) { + SettingsSubscreen.DRIVER_MANAGER, + SettingsSubscreen.FREEDRENO_SETTINGS -> bundleOf("game" to args.game) + + SettingsSubscreen.GAME_INFO, + SettingsSubscreen.ADDONS -> bundleOf( + "game" to requireNotNull(args.game) { + "Game is required for ${args.destination}" + } + ) + + else -> Bundle() + } + + private fun setInsets() { + ViewCompat.setOnApplyWindowInsetsListener( + binding.navigationBarShade + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + val mlpNavShade = binding.navigationBarShade.layoutParams as MarginLayoutParams + mlpNavShade.height = barInsets.bottom + binding.navigationBarShade.layoutParams = mlpNavShade + + windowInsets + } + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt index 7fec413b66..aa2b3c7df2 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt @@ -21,9 +21,10 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.findNavController import com.google.android.material.transition.MaterialSharedAxis -import org.yuzu.yuzu_emu.BuildConfig +import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding +import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.NativeLibrary @@ -54,7 +55,7 @@ class AboutFragment : Fragment() { super.onViewCreated(view, savedInstanceState) homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarAbout.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } binding.imageLogo.setOnLongClickListener { @@ -72,8 +73,11 @@ class AboutFragment : Fragment() { ) } binding.buttonLicenses.setOnClickListener { - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - binding.root.findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.LICENSES, + null + ) + binding.root.findNavController().navigate(action) } val buildName = getString(R.string.app_name_suffixed) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index 96b7a8cce2..b20d75ef0a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt @@ -15,7 +15,6 @@ import androidx.core.view.updatePadding import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.transition.MaterialSharedAxis @@ -61,7 +60,7 @@ class AddonsFragment : Fragment() { homeViewModel.setStatusBarShadeVisibility(false) binding.toolbarAddons.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt index 3ab171a8d4..49237cb756 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AppletLauncherFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -12,7 +12,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.R @@ -50,7 +49,7 @@ class AppletLauncherFragment : Fragment() { homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarApplets.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } val applets = listOf( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt index e2b652dc60..eacc64a63e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverFetcherFragment.kt @@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -142,7 +141,7 @@ class DriverFetcherFragment : Fragment() { super.onViewCreated(view, savedInstanceState) homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarDrivers.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } binding.listDrivers.layoutManager = LinearLayoutManager(context) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index 23334f05eb..89a6362dc6 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -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 @@ -19,6 +19,7 @@ import androidx.navigation.fragment.navArgs import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis +import org.yuzu.yuzu_emu.HomeNavigationDirections import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -27,6 +28,7 @@ import org.yuzu.yuzu_emu.adapters.DriverAdapter import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.StringSetting +import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen import org.yuzu.yuzu_emu.model.Driver.Companion.toDriver import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.HomeViewModel @@ -105,7 +107,7 @@ class DriverManagerFragment : Fragment() { } binding.toolbarDrivers.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } binding.buttonInstall.setOnClickListener { @@ -113,9 +115,11 @@ class DriverManagerFragment : Fragment() { } binding.buttonFetch.setOnClickListener { - binding.root.findNavController().navigate( - R.id.action_driverManagerFragment_to_driverFetcherFragment + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.DRIVER_FETCHER, + null ) + binding.root.findNavController().navigate(action) } binding.listDrivers.apply { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt index 9c43d2c6e1..6e05df799b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt @@ -4,20 +4,21 @@ package org.yuzu.yuzu_emu.fragments import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialSharedAxis -import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.FolderAdapter import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding @@ -25,7 +26,6 @@ import org.yuzu.yuzu_emu.model.DirectoryType import org.yuzu.yuzu_emu.model.GameDir import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel -import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.collect @@ -36,6 +36,20 @@ class GameFoldersFragment : Fragment() { private val homeViewModel: HomeViewModel by activityViewModels() private val gamesViewModel: GamesViewModel by activityViewModels() + private val getGamesDirectory = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result != null) { + processGamesDir(result) + } + } + + private val getExternalContentDirectory = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result != null) { + processExternalContentDir(result) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) @@ -59,7 +73,7 @@ class GameFoldersFragment : Fragment() { homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarFolders.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } binding.listFolders.apply { @@ -74,7 +88,6 @@ class GameFoldersFragment : Fragment() { (binding.listFolders.adapter as FolderAdapter).submitList(it) } - val mainActivity = requireActivity() as MainActivity binding.buttonAdd.setOnClickListener { // Show a model to choose between Game and External Content val options = arrayOf( @@ -87,10 +100,10 @@ class GameFoldersFragment : Fragment() { .setItems(options) { _, which -> when (which) { 0 -> { // Game Folder - mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) + getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) } 1 -> { // External Content Folder - mainActivity.getExternalContentDirectory.launch(null) + getExternalContentDirectory.launch(null) } } } @@ -105,6 +118,50 @@ class GameFoldersFragment : Fragment() { gamesViewModel.onCloseGameFoldersFragment() } + private fun processGamesDir(result: Uri) { + requireContext().contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + val uriString = result.toString() + val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString } + if (folder != null) { + Toast.makeText( + requireContext().applicationContext, + R.string.folder_already_added, + Toast.LENGTH_SHORT + ).show() + return + } + + AddGameFolderDialogFragment.newInstance(uriString, calledFromGameFragment = false) + .show(parentFragmentManager, AddGameFolderDialogFragment.TAG) + } + + private fun processExternalContentDir(result: Uri) { + requireContext().contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + val uriString = result.toString() + val folder = gamesViewModel.folders.value.firstOrNull { + it.uriString == uriString && it.type == DirectoryType.EXTERNAL_CONTENT + } + if (folder != null) { + Toast.makeText( + requireContext().applicationContext, + R.string.folder_already_added, + Toast.LENGTH_SHORT + ).show() + return + } + + val externalContentDir = GameDir(uriString, deepScan = false, DirectoryType.EXTERNAL_CONTENT) + gamesViewModel.addFolder(externalContentDir, savedFromGameFragment = false) + } + private fun setInsets() = ViewCompat.setOnApplyWindowInsetsListener( binding.root diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt index 7863e40ff5..5d6238e5a1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -18,7 +18,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.NativeLibrary @@ -64,7 +63,7 @@ class GameInfoFragment : Fragment() { binding.apply { toolbarInfo.title = args.game.title toolbarInfo.setNavigationOnClickListener { - view.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } val pathString = Uri.parse(args.game.path).path ?: "" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index 9e55297846..46b75197d5 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -35,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.GameProperty import org.yuzu.yuzu_emu.model.GamesViewModel @@ -250,8 +251,10 @@ class GamePropertiesFragment : Fragment() { R.string.info_description, R.drawable.ic_info_outline, action = { - val action = GamePropertiesFragmentDirections - .actionPerGamePropertiesFragmentToGameInfoFragment(args.game) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.GAME_INFO, + args.game + ) binding.root.findNavController().navigate(action) } ) @@ -317,8 +320,11 @@ class GamePropertiesFragment : Fragment() { R.string.add_ons_description, R.drawable.ic_edit, action = { - val action = GamePropertiesFragmentDirections - .actionPerGamePropertiesFragmentToAddonsFragment(args.game) + val action = + HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.ADDONS, + args.game + ) binding.root.findNavController().navigate(action) } ) @@ -333,8 +339,11 @@ class GamePropertiesFragment : Fragment() { R.drawable.ic_build, detailsFlow = driverViewModel.selectedDriverTitle, action = { - val action = GamePropertiesFragmentDirections - .actionPerGamePropertiesFragmentToDriverManagerFragment(args.game) + val action = + HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.DRIVER_MANAGER, + args.game + ) binding.root.findNavController().navigate(action) } ) @@ -347,8 +356,11 @@ class GamePropertiesFragment : Fragment() { R.string.freedreno_per_game_description, R.drawable.ic_graphics, action = { - val action = GamePropertiesFragmentDirections - .actionPerGamePropertiesFragmentToFreedrenoSettingsFragment(args.game) + val action = + HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.FREEDRENO_SETTINGS, + args.game + ) binding.root.findNavController().navigate(action) } ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index 6f4bf858ea..37eda22c69 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -36,6 +36,7 @@ import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.fetcher.SpacingItemDecoration import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.features.settings.ui.SettingsSubscreen import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel @@ -126,8 +127,11 @@ class HomeSettingsFragment : Fragment() { R.string.profile_manager_description, R.drawable.ic_account_circle, { - binding.root.findNavController() - .navigate(R.id.action_homeSettingsFragment_to_profileManagerFragment) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.PROFILE_MANAGER, + null + ) + binding.root.findNavController().navigate(action) } ) ) @@ -137,8 +141,10 @@ class HomeSettingsFragment : Fragment() { R.string.install_gpu_driver_description, R.drawable.ic_build, { - val action = HomeSettingsFragmentDirections - .actionHomeSettingsFragmentToDriverManagerFragment(null) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.DRIVER_MANAGER, + null + ) binding.root.findNavController().navigate(action) }, { true }, @@ -154,7 +160,12 @@ class HomeSettingsFragment : Fragment() { R.string.gpu_driver_settings, R.drawable.ic_graphics, { - binding.root.findNavController().navigate(R.id.freedrenoSettingsFragment) + val action = + HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.FREEDRENO_SETTINGS, + null + ) + binding.root.findNavController().navigate(action) } ) ) @@ -175,8 +186,11 @@ class HomeSettingsFragment : Fragment() { R.string.applets_description, R.drawable.ic_applet, { - binding.root.findNavController() - .navigate(R.id.action_homeSettingsFragment_to_appletLauncherFragment) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.APPLET_LAUNCHER, + null + ) + binding.root.findNavController().navigate(action) }, { NativeLibrary.isFirmwareAvailable() }, R.string.applets_error_firmware, @@ -189,8 +203,11 @@ class HomeSettingsFragment : Fragment() { R.string.manage_yuzu_data_description, R.drawable.ic_install, { - binding.root.findNavController() - .navigate(R.id.action_homeSettingsFragment_to_installableFragment) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.INSTALLABLE, + null + ) + binding.root.findNavController().navigate(action) } ) ) @@ -200,8 +217,11 @@ class HomeSettingsFragment : Fragment() { R.string.select_games_folder_description, R.drawable.ic_add, { - binding.root.findNavController() - .navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.GAME_FOLDERS, + null + ) + binding.root.findNavController().navigate(action) } ) ) @@ -284,9 +304,11 @@ class HomeSettingsFragment : Fragment() { R.string.about_description, R.drawable.ic_info_outline, { - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - parentFragmentManager.primaryNavigationFragment?.findNavController() - ?.navigate(R.id.action_homeSettingsFragment_to_aboutFragment) + val action = HomeNavigationDirections.actionGlobalSettingsSubscreenActivity( + SettingsSubscreen.ABOUT, + null + ) + binding.root.findNavController().navigate(action) } ) ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 1b94d5f1a6..10862c37b4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -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 @@ -14,23 +14,23 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.transition.MaterialSharedAxis import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.adapters.InstallableAdapter import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding +import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.Installable import org.yuzu.yuzu_emu.model.TaskState -import org.yuzu.yuzu_emu.ui.main.MainActivity -import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.InstallableActions import org.yuzu.yuzu_emu.utils.NativeConfig import org.yuzu.yuzu_emu.utils.ViewUtils.updateMargins import org.yuzu.yuzu_emu.utils.collect @@ -45,6 +45,9 @@ class InstallableFragment : Fragment() { private val binding get() = _binding!! private val homeViewModel: HomeViewModel by activityViewModels() + private val gamesViewModel: GamesViewModel by activityViewModels() + private val addonViewModel: AddonViewModel by activityViewModels() + private val driverViewModel: DriverViewModel by activityViewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -65,12 +68,10 @@ class InstallableFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val mainActivity = requireActivity() as MainActivity - homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarInstallables.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } homeViewModel.openImportSaves.collect(viewLifecycleOwner) { @@ -84,8 +85,8 @@ class InstallableFragment : Fragment() { Installable( R.string.user_data, R.string.user_data_description, - install = { mainActivity.importUserData.launch(arrayOf("application/zip")) }, - export = { mainActivity.exportUserData.launch("export.zip") } + install = { importUserDataLauncher.launch(arrayOf("application/zip")) }, + export = { exportUserDataLauncher.launch("export.zip") } ), Installable( R.string.manage_save_data, @@ -127,27 +128,33 @@ class InstallableFragment : Fragment() { Installable( R.string.install_game_content, R.string.install_game_content_description, - install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) } + install = { installGameUpdateLauncher.launch(arrayOf("*/*")) } ), Installable( R.string.install_firmware, R.string.install_firmware_description, - install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } + install = { getFirmwareLauncher.launch(arrayOf("application/zip")) } ), Installable( R.string.uninstall_firmware, R.string.uninstall_firmware_description, - install = { mainActivity.uninstallFirmware() } + install = { + InstallableActions.uninstallFirmware( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + homeViewModel = homeViewModel + ) + } ), Installable( R.string.install_prod_keys, R.string.install_prod_keys_description, - install = { mainActivity.getProdKey.launch(arrayOf("*/*")) } + install = { getProdKeyLauncher.launch(arrayOf("*/*")) } ), Installable( R.string.install_amiibo_keys, R.string.install_amiibo_keys_description, - install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) } + install = { getAmiiboKeyLauncher.launch(arrayOf("*/*")) } ) ) @@ -180,6 +187,132 @@ class InstallableFragment : Fragment() { windowInsets } + private val getProdKeyLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + InstallableActions.processKey( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + gamesViewModel = gamesViewModel, + result = result, + extension = "keys" + ) + } + } + + private val getAmiiboKeyLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + InstallableActions.processKey( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + gamesViewModel = gamesViewModel, + result = result, + extension = "bin" + ) + } + } + + private val getFirmwareLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + InstallableActions.processFirmware( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + homeViewModel = homeViewModel, + result = result + ) + } + } + + private val installGameUpdateLauncher = + registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { documents -> + if (documents.isEmpty()) { + return@registerForActivityResult + } + + if (addonViewModel.game == null) { + InstallableActions.installContent( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + addonViewModel = addonViewModel, + documents = documents + ) + return@registerForActivityResult + } + + ProgressDialogFragment.newInstance( + requireActivity(), + R.string.verifying_content, + false + ) { _, _ -> + var updatesMatchProgram = true + for (document in documents) { + val valid = NativeLibrary.doesUpdateMatchProgram( + addonViewModel.game!!.programId, + document.toString() + ) + if (!valid) { + updatesMatchProgram = false + break + } + } + + if (updatesMatchProgram) { + requireActivity().runOnUiThread { + InstallableActions.installContent( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + addonViewModel = addonViewModel, + documents = documents + ) + } + } else { + requireActivity().runOnUiThread { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.content_install_notice, + descriptionId = R.string.content_install_notice_description, + positiveAction = { + InstallableActions.installContent( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + addonViewModel = addonViewModel, + documents = documents + ) + }, + negativeAction = {} + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + } + return@newInstance Any() + }.show(parentFragmentManager, ProgressDialogFragment.TAG) + } + + private val importUserDataLauncher = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result != null) { + InstallableActions.importUserData( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + gamesViewModel = gamesViewModel, + driverViewModel = driverViewModel, + result = result + ) + } + } + + private val exportUserDataLauncher = + registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { result -> + if (result != null) { + InstallableActions.exportUserData( + activity = requireActivity(), + fragmentManager = parentFragmentManager, + result = result + ) + } + } + private val importSaves = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> if (result == null) { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt index aa18aa2482..32b72fe38f 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LicensesFragment.kt @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later package org.yuzu.yuzu_emu.fragments @@ -13,7 +13,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.navigation.findNavController import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.transition.MaterialSharedAxis import org.yuzu.yuzu_emu.R @@ -48,7 +47,7 @@ class LicensesFragment : Fragment() { homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarLicenses.setNavigationOnClickListener { - binding.root.findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } val licenses = listOf( diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt index 6ee34105e7..2786906f6b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProfileManagerFragment.kt @@ -51,7 +51,7 @@ class ProfileManagerFragment : Fragment() { homeViewModel.setStatusBarShadeVisibility(visible = false) binding.toolbarProfiles.setNavigationOnClickListener { - findNavController().popBackStack() + requireActivity().onBackPressedDispatcher.onBackPressed() } setupRecyclerView() diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 74a171cf1f..f0806df786 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt @@ -26,7 +26,6 @@ import androidx.preference.PreferenceManager import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.io.File -import java.io.FilenameFilter import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivityMainBinding @@ -39,16 +38,10 @@ import org.yuzu.yuzu_emu.model.AddonViewModel import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel -import org.yuzu.yuzu_emu.model.InstallResult import android.os.Build -import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible -import java.io.BufferedInputStream -import java.io.BufferedOutputStream -import java.util.zip.ZipEntry -import java.util.zip.ZipInputStream import androidx.core.content.edit import org.yuzu.yuzu_emu.activities.EmulationActivity import kotlin.text.compareTo @@ -453,35 +446,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } fun processKey(result: Uri, extension: String = "keys") { - contentResolver.takePersistableUriPermission( - result, - Intent.FLAG_GRANT_READ_URI_PERMISSION + InstallableActions.processKey( + activity = this, + fragmentManager = supportFragmentManager, + gamesViewModel = gamesViewModel, + result = result, + extension = extension ) - - val resultCode: Int = NativeLibrary.installKeys(result.toString(), extension) - - if (resultCode == 0) { - // TODO(crueter): It may be worth it to switch some of these Toasts to snackbars, - // since most of it is foreground-only anyways. - Toast.makeText( - applicationContext, - R.string.keys_install_success, - Toast.LENGTH_SHORT - ).show() - - gamesViewModel.reloadGames(true) - - return - } - - val resultString: String = - resources.getStringArray(R.array.installKeysResults)[resultCode] - - MessageDialogFragment.newInstance( - titleId = R.string.keys_failed, - descriptionString = resultString, - helpLinkId = R.string.keys_missing_help - ).show(supportFragmentManager, MessageDialogFragment.TAG) } val getFirmware = registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> @@ -491,75 +462,21 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } fun processFirmware(result: Uri, onComplete: (() -> Unit)? = null) { - val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } - - val firmwarePath = - File(NativeConfig.getNandDir() + "/system/Contents/registered/") - val cacheFirmwareDir = File("${cacheDir.path}/registered/") - - ProgressDialogFragment.newInstance( - this, - R.string.firmware_installing - ) { progressCallback, _ -> - var messageToShow: Any - try { - FileUtil.unzipToInternalStorage( - result.toString(), - cacheFirmwareDir, - progressCallback - ) - val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 - val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 - messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { - MessageDialogFragment.newInstance( - this, - titleId = R.string.firmware_installed_failure, - descriptionId = R.string.firmware_installed_failure_description - ) - } else { - firmwarePath.deleteRecursively() - cacheFirmwareDir.copyRecursively(firmwarePath, true) - NativeLibrary.initializeSystem(true) - homeViewModel.setCheckKeys(true) - getString(R.string.save_file_imported_success) - } - } catch (e: Exception) { - Log.error("[MainActivity] Firmware install failed - ${e.message}") - messageToShow = getString(R.string.fatal_error) - } finally { - cacheFirmwareDir.deleteRecursively() - } - messageToShow - }.apply { - onDialogComplete = onComplete - }.show(supportFragmentManager, ProgressDialogFragment.TAG) + InstallableActions.processFirmware( + activity = this, + fragmentManager = supportFragmentManager, + homeViewModel = homeViewModel, + result = result, + onComplete = onComplete + ) } fun uninstallFirmware() { - val firmwarePath = - File(NativeConfig.getNandDir() + "/system/Contents/registered/") - ProgressDialogFragment.newInstance( - this, - R.string.firmware_uninstalling - ) { progressCallback, _ -> - var messageToShow: Any - try { - // Ensure the firmware directory exists before attempting to delete - if (firmwarePath.exists()) { - firmwarePath.deleteRecursively() - // Optionally reinitialize the system or perform other necessary steps - NativeLibrary.initializeSystem(true) - homeViewModel.setCheckKeys(true) - messageToShow = getString(R.string.firmware_uninstalled_success) - } else { - messageToShow = getString(R.string.firmware_uninstalled_failure) - } - } catch (e: Exception) { - Log.error("[MainActivity] Firmware uninstall failed - ${e.message}") - messageToShow = getString(R.string.fatal_error) - } - messageToShow - }.show(supportFragmentManager, ProgressDialogFragment.TAG) + InstallableActions.uninstallFirmware( + activity = this, + fragmentManager = supportFragmentManager, + homeViewModel = homeViewModel + ) } val installGameUpdate = registerForActivityResult( @@ -606,101 +523,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } private fun installContent(documents: List) { - ProgressDialogFragment.newInstance( - this@MainActivity, - R.string.installing_game_content - ) { progressCallback, messageCallback -> - var installSuccess = 0 - var installOverwrite = 0 - var errorBaseGame = 0 - var error = 0 - documents.forEach { - messageCallback.invoke(FileUtil.getFilename(it)) - when ( - InstallResult.from( - NativeLibrary.installFileToNand( - it.toString(), - progressCallback - ) - ) - ) { - InstallResult.Success -> { - installSuccess += 1 - } - - InstallResult.Overwrite -> { - installOverwrite += 1 - } - - InstallResult.BaseInstallAttempted -> { - errorBaseGame += 1 - } - - InstallResult.Failure -> { - error += 1 - } - } - } - - addonViewModel.refreshAddons(force = true) - - val separator = System.lineSeparator() ?: "\n" - val installResult = StringBuilder() - if (installSuccess > 0) { - installResult.append( - getString( - R.string.install_game_content_success_install, - installSuccess - ) - ) - installResult.append(separator) - } - if (installOverwrite > 0) { - installResult.append( - getString( - R.string.install_game_content_success_overwrite, - installOverwrite - ) - ) - installResult.append(separator) - } - val errorTotal: Int = errorBaseGame + error - if (errorTotal > 0) { - installResult.append(separator) - installResult.append( - getString( - R.string.install_game_content_failed_count, - errorTotal - ) - ) - installResult.append(separator) - if (errorBaseGame > 0) { - installResult.append(separator) - installResult.append( - getString(R.string.install_game_content_failure_base) - ) - installResult.append(separator) - } - if (error > 0) { - installResult.append( - getString(R.string.install_game_content_failure_description) - ) - installResult.append(separator) - } - return@newInstance MessageDialogFragment.newInstance( - this, - titleId = R.string.install_game_content_failure, - descriptionString = installResult.toString().trim(), - helpLinkId = R.string.install_game_content_help_link - ) - } else { - return@newInstance MessageDialogFragment.newInstance( - this, - titleId = R.string.install_game_content_success, - descriptionString = installResult.toString().trim() - ) - } - }.show(supportFragmentManager, ProgressDialogFragment.TAG) + InstallableActions.installContent( + activity = this, + fragmentManager = supportFragmentManager, + addonViewModel = addonViewModel, + documents = documents + ) } val exportUserData = registerForActivityResult( @@ -709,25 +537,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (result == null) { return@registerForActivityResult } - - ProgressDialogFragment.newInstance( - this, - R.string.exporting_user_data, - true - ) { progressCallback, _ -> - val zipResult = FileUtil.zipFromInternalStorage( - File(DirectoryInitialization.userDirectory!!), - DirectoryInitialization.userDirectory!!, - BufferedOutputStream(contentResolver.openOutputStream(result)), - progressCallback, - compression = false - ) - return@newInstance when (zipResult) { - TaskState.Completed -> getString(R.string.user_data_export_success) - TaskState.Failed -> R.string.export_failed - TaskState.Cancelled -> R.string.user_data_export_cancelled - } - }.show(supportFragmentManager, ProgressDialogFragment.TAG) + InstallableActions.exportUserData( + activity = this, + fragmentManager = supportFragmentManager, + result = result + ) } val importUserData = @@ -735,58 +549,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider { if (result == null) { return@registerForActivityResult } - - ProgressDialogFragment.newInstance( - this, - R.string.importing_user_data - ) { progressCallback, _ -> - val checkStream = - ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) - var isYuzuBackup = false - checkStream.use { stream -> - var ze: ZipEntry? = null - while (stream.nextEntry?.also { ze = it } != null) { - val itemName = ze!!.name.trim() - if (itemName == "/config/config.ini" || itemName == "config/config.ini") { - isYuzuBackup = true - return@use - } - } - } - if (!isYuzuBackup) { - return@newInstance MessageDialogFragment.newInstance( - this, - titleId = R.string.invalid_yuzu_backup, - descriptionId = R.string.user_data_import_failed_description - ) - } - - // Clear existing user data - NativeConfig.unloadGlobalConfig() - File(DirectoryInitialization.userDirectory!!).deleteRecursively() - - // Copy archive to internal storage - try { - FileUtil.unzipToInternalStorage( - result.toString(), - File(DirectoryInitialization.userDirectory!!), - progressCallback - ) - } catch (e: Exception) { - return@newInstance MessageDialogFragment.newInstance( - this, - titleId = R.string.import_failed, - descriptionId = R.string.user_data_import_failed_description - ) - } - - // Reinitialize relevant data - NativeLibrary.initializeSystem(true) - NativeConfig.initializeGlobalConfig() - gamesViewModel.reloadGames(false) - driverViewModel.reloadDriverData() - - return@newInstance getString(R.string.user_data_import_success) - }.show(supportFragmentManager, ProgressDialogFragment.TAG) + InstallableActions.importUserData( + activity = this, + fragmentManager = supportFragmentManager, + gamesViewModel = gamesViewModel, + driverViewModel = driverViewModel, + result = result + ) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InstallableActions.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InstallableActions.kt new file mode 100644 index 0000000000..d385e2a095 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InstallableActions.kt @@ -0,0 +1,327 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +package org.yuzu.yuzu_emu.utils + +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.fragments.MessageDialogFragment +import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment +import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.InstallResult +import org.yuzu.yuzu_emu.model.TaskState +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FilenameFilter +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +object InstallableActions { + fun processKey( + activity: FragmentActivity, + fragmentManager: FragmentManager, + gamesViewModel: GamesViewModel, + result: Uri, + extension: String = "keys" + ) { + activity.contentResolver.takePersistableUriPermission( + result, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + + val resultCode = NativeLibrary.installKeys(result.toString(), extension) + if (resultCode == 0) { + Toast.makeText( + activity.applicationContext, + R.string.keys_install_success, + Toast.LENGTH_SHORT + ).show() + gamesViewModel.reloadGames(true) + return + } + + val resultString = activity.resources.getStringArray(R.array.installKeysResults)[resultCode] + MessageDialogFragment.newInstance( + titleId = R.string.keys_failed, + descriptionString = resultString, + helpLinkId = R.string.keys_missing_help + ).show(fragmentManager, MessageDialogFragment.TAG) + } + + fun processFirmware( + activity: FragmentActivity, + fragmentManager: FragmentManager, + homeViewModel: HomeViewModel, + result: Uri, + onComplete: (() -> Unit)? = null + ) { + val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } + val firmwarePath = File(NativeConfig.getNandDir() + "/system/Contents/registered/") + val cacheFirmwareDir = File("${activity.cacheDir.path}/registered/") + + ProgressDialogFragment.newInstance( + activity, + R.string.firmware_installing + ) { progressCallback, _ -> + var messageToShow: Any + try { + FileUtil.unzipToInternalStorage( + result.toString(), + cacheFirmwareDir, + progressCallback + ) + val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 + val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 + messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { + MessageDialogFragment.newInstance( + activity, + titleId = R.string.firmware_installed_failure, + descriptionId = R.string.firmware_installed_failure_description + ) + } else { + firmwarePath.deleteRecursively() + cacheFirmwareDir.copyRecursively(firmwarePath, overwrite = true) + NativeLibrary.initializeSystem(true) + homeViewModel.setCheckKeys(true) + activity.getString(R.string.save_file_imported_success) + } + } catch (_: Exception) { + messageToShow = activity.getString(R.string.fatal_error) + } finally { + cacheFirmwareDir.deleteRecursively() + } + messageToShow + }.apply { + onDialogComplete = onComplete + }.show(fragmentManager, ProgressDialogFragment.TAG) + } + + fun uninstallFirmware( + activity: FragmentActivity, + fragmentManager: FragmentManager, + homeViewModel: HomeViewModel + ) { + val firmwarePath = File(NativeConfig.getNandDir() + "/system/Contents/registered/") + ProgressDialogFragment.newInstance( + activity, + R.string.firmware_uninstalling + ) { _, _ -> + val messageToShow: Any = try { + if (firmwarePath.exists()) { + firmwarePath.deleteRecursively() + NativeLibrary.initializeSystem(true) + homeViewModel.setCheckKeys(true) + activity.getString(R.string.firmware_uninstalled_success) + } else { + activity.getString(R.string.firmware_uninstalled_failure) + } + } catch (_: Exception) { + activity.getString(R.string.fatal_error) + } + messageToShow + }.show(fragmentManager, ProgressDialogFragment.TAG) + } + + fun installContent( + activity: FragmentActivity, + fragmentManager: FragmentManager, + addonViewModel: AddonViewModel, + documents: List + ) { + ProgressDialogFragment.newInstance( + activity, + R.string.installing_game_content + ) { progressCallback, messageCallback -> + var installSuccess = 0 + var installOverwrite = 0 + var errorBaseGame = 0 + var error = 0 + documents.forEach { + messageCallback.invoke(FileUtil.getFilename(it)) + when ( + InstallResult.from( + NativeLibrary.installFileToNand( + it.toString(), + progressCallback + ) + ) + ) { + InstallResult.Success -> installSuccess += 1 + InstallResult.Overwrite -> installOverwrite += 1 + InstallResult.BaseInstallAttempted -> errorBaseGame += 1 + InstallResult.Failure -> error += 1 + } + } + + addonViewModel.refreshAddons(force = true) + + val separator = System.lineSeparator() ?: "\n" + val installResult = StringBuilder() + if (installSuccess > 0) { + installResult.append( + activity.getString( + R.string.install_game_content_success_install, + installSuccess + ) + ) + installResult.append(separator) + } + if (installOverwrite > 0) { + installResult.append( + activity.getString( + R.string.install_game_content_success_overwrite, + installOverwrite + ) + ) + installResult.append(separator) + } + val errorTotal = errorBaseGame + error + if (errorTotal > 0) { + installResult.append(separator) + installResult.append( + activity.getString( + R.string.install_game_content_failed_count, + errorTotal + ) + ) + installResult.append(separator) + if (errorBaseGame > 0) { + installResult.append(separator) + installResult.append(activity.getString(R.string.install_game_content_failure_base)) + installResult.append(separator) + } + if (error > 0) { + installResult.append( + activity.getString(R.string.install_game_content_failure_description) + ) + installResult.append(separator) + } + return@newInstance MessageDialogFragment.newInstance( + activity, + titleId = R.string.install_game_content_failure, + descriptionString = installResult.toString().trim(), + helpLinkId = R.string.install_game_content_help_link + ) + } else { + return@newInstance MessageDialogFragment.newInstance( + activity, + titleId = R.string.install_game_content_success, + descriptionString = installResult.toString().trim() + ) + } + }.show(fragmentManager, ProgressDialogFragment.TAG) + } + + fun exportUserData( + activity: FragmentActivity, + fragmentManager: FragmentManager, + result: Uri + ) { + val userDirectory = DirectoryInitialization.userDirectory + if (userDirectory == null) { + Toast.makeText( + activity.applicationContext, + R.string.fatal_error, + Toast.LENGTH_SHORT + ).show() + return + } + + ProgressDialogFragment.newInstance( + activity, + R.string.exporting_user_data, + true + ) { progressCallback, _ -> + val zipResult = FileUtil.zipFromInternalStorage( + File(userDirectory), + userDirectory, + BufferedOutputStream(activity.contentResolver.openOutputStream(result)), + progressCallback, + compression = false + ) + return@newInstance when (zipResult) { + TaskState.Completed -> activity.getString(R.string.user_data_export_success) + TaskState.Failed -> R.string.export_failed + TaskState.Cancelled -> R.string.user_data_export_cancelled + } + }.show(fragmentManager, ProgressDialogFragment.TAG) + } + + fun importUserData( + activity: FragmentActivity, + fragmentManager: FragmentManager, + gamesViewModel: GamesViewModel, + driverViewModel: DriverViewModel, + result: Uri + ) { + val userDirectory = DirectoryInitialization.userDirectory + if (userDirectory == null) { + Toast.makeText( + activity.applicationContext, + R.string.fatal_error, + Toast.LENGTH_SHORT + ).show() + return + } + + ProgressDialogFragment.newInstance( + activity, + R.string.importing_user_data + ) { progressCallback, _ -> + val checkStream = ZipInputStream( + BufferedInputStream(activity.contentResolver.openInputStream(result)) + ) + var isYuzuBackup = false + checkStream.use { stream -> + var ze: ZipEntry? = null + while (stream.nextEntry?.also { ze = it } != null) { + val itemName = ze!!.name.trim() + if (itemName == "/config/config.ini" || itemName == "config/config.ini") { + isYuzuBackup = true + return@use + } + } + } + if (!isYuzuBackup) { + return@newInstance MessageDialogFragment.newInstance( + activity, + titleId = R.string.invalid_yuzu_backup, + descriptionId = R.string.user_data_import_failed_description + ) + } + + NativeConfig.unloadGlobalConfig() + File(userDirectory).deleteRecursively() + + try { + FileUtil.unzipToInternalStorage( + result.toString(), + File(userDirectory), + progressCallback + ) + } catch (_: Exception) { + return@newInstance MessageDialogFragment.newInstance( + activity, + titleId = R.string.import_failed, + descriptionId = R.string.user_data_import_failed_description + ) + } + + NativeLibrary.initializeSystem(true) + NativeConfig.initializeGlobalConfig() + gamesViewModel.reloadGames(false) + driverViewModel.reloadDriverData() + + return@newInstance activity.getString(R.string.user_data_import_success) + }.show(fragmentManager, ProgressDialogFragment.TAG) + } +} diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_about.xml b/src/android/app/src/main/res/layout-w600dp/fragment_about.xml index ae2b3e3637..cc8d26dd58 100644 --- a/src/android/app/src/main/res/layout-w600dp/fragment_about.xml +++ b/src/android/app/src/main/res/layout-w600dp/fragment_about.xml @@ -10,12 +10,11 @@ + android:touchscreenBlocksFocus="false"> + android:paddingBottom="24dp" + android:paddingStart="24dp" + android:paddingTop="0dp" + android:paddingEnd="24dp"> - + android:gravity="center_horizontal" + android:orientation="vertical"> + + + + + + + + - - - - - - - - - - diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml index da823bcc25..b1b896f169 100644 --- a/src/android/app/src/main/res/layout/fragment_about.xml +++ b/src/android/app/src/main/res/layout/fragment_about.xml @@ -10,12 +10,11 @@ + android:touchscreenBlocksFocus="false"> - - - - + + + + + android:text="@string/app_name" + android:textAlignment="center" /> - + - - - - + @@ -220,7 +206,9 @@ app:icon="@drawable/ic_discord" app:iconSize="24dp" app:iconGravity="textStart" - app:iconPadding="0dp" /> + app:iconPadding="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" /> + app:iconPadding="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" /> + app:iconPadding="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" /> + app:iconPadding="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" /> + app:iconPadding="0dp" + app:strokeColor="?attr/colorOutline" + app:strokeWidth="1dp" /> diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml index 5e6a49501d..2adc60a47c 100644 --- a/src/android/app/src/main/res/navigation/emulation_navigation.xml +++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml @@ -40,10 +40,6 @@ + app:destination="@id/settingsActivity" /> diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 7d04a19f36..dd567abc1a 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -20,26 +20,7 @@ - - - - - - - + android:label="HomeSettingsFragment" /> - - + android:label="AboutFragment" /> + app:destination="@id/settingsActivity" /> + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 7d094effcb..b8f4e0d7dc 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -3,7 +3,7 @@ Eden - This software will run games for the Nintendo Switch game console. No game titles or keys are included.<br /><br />Before you begin, please locate your prod.keys ]]> file on your device storage.<br /><br />Learn more]]> + This software will run games for the Nintendo Switch game console. No game titles or keys are included.<br /><br />Before you begin, please locate your prod.keys ]]> file on your device storage.<br /><br />Learn more]]> Notices and errors noticesAndErrors Shows notifications when something goes wrong. @@ -400,7 +400,7 @@ Copied to clipboard An open-source Switch emulator Contributors - Contributors who made Eden for Android possible + People who made Eden for Android possible https://git.eden-emu.dev/eden-emu/eden/activity/contributors Projects that make Eden for Android possible Build @@ -858,7 +858,7 @@ ROM file does not exist Game Requires Firmware - dump and install firmware, or press \"OK\" to launch anyways.]]> + Searching for game... diff --git a/src/qt_common/CMakeLists.txt b/src/qt_common/CMakeLists.txt index f0522c07d2..904b03d288 100644 --- a/src/qt_common/CMakeLists.txt +++ b/src/qt_common/CMakeLists.txt @@ -84,7 +84,7 @@ target_link_libraries(qt_common PRIVATE core Qt6::Core Qt6::Concurrent SimpleIni target_link_libraries(qt_common PUBLIC frozen::frozen-headers) target_link_libraries(qt_common PRIVATE gamemode::headers frontend_common) -if (NOT APPLE AND ENABLE_OPENGL) +if (ENABLE_OPENGL) target_compile_definitions(qt_common PUBLIC HAS_OPENGL) endif() diff --git a/src/qt_common/util/content.cpp b/src/qt_common/util/content.cpp index 4d9b324608..b100a3ac9d 100644 --- a/src/qt_common/util/content.cpp +++ b/src/qt_common/util/content.cpp @@ -27,9 +27,8 @@ bool CheckGameFirmware(u64 program_id) { !FirmwareManager::CheckFirmwarePresence(*system)) { auto result = QtCommon::Frontend::Warning( tr("Game Requires Firmware"), - tr("The game you are trying to launch requires firmware to boot or to get past the " - "opening menu. Please " - "dump and install firmware, or press \"OK\" to launch anyways."), + tr("The game you are trying to launch requires firmware to work. " + "Press \"OK\" to launch anyways."), QtCommon::Frontend::Ok | QtCommon::Frontend::Cancel); return result == QtCommon::Frontend::Ok; diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index a58a73cd3c..3324682639 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -111,64 +111,14 @@ add_library(video_core STATIC rasterizer_interface.h renderer_base.cpp renderer_base.h + + # Null renderer_null/null_rasterizer.cpp renderer_null/null_rasterizer.h renderer_null/renderer_null.cpp renderer_null/renderer_null.h - renderer_opengl/present/filters.cpp - renderer_opengl/present/filters.h - renderer_opengl/present/fsr.cpp - renderer_opengl/present/fsr.h - renderer_opengl/present/fxaa.cpp - renderer_opengl/present/fxaa.h - renderer_opengl/present/layer.cpp - renderer_opengl/present/layer.h - renderer_opengl/present/present_uniforms.h - renderer_opengl/present/smaa.cpp - renderer_opengl/present/smaa.h - renderer_opengl/present/util.h - renderer_opengl/present/window_adapt_pass.cpp - renderer_opengl/present/window_adapt_pass.h - renderer_opengl/blit_image.cpp - renderer_opengl/blit_image.h - renderer_opengl/gl_blit_screen.cpp - renderer_opengl/gl_blit_screen.h - renderer_opengl/gl_buffer_cache_base.cpp - renderer_opengl/gl_buffer_cache.cpp - renderer_opengl/gl_buffer_cache.h - renderer_opengl/gl_compute_pipeline.cpp - renderer_opengl/gl_compute_pipeline.h - renderer_opengl/gl_device.cpp - renderer_opengl/gl_device.h - renderer_opengl/gl_fence_manager.cpp - renderer_opengl/gl_fence_manager.h - renderer_opengl/gl_graphics_pipeline.cpp - renderer_opengl/gl_graphics_pipeline.h - renderer_opengl/gl_rasterizer.cpp - renderer_opengl/gl_rasterizer.h - renderer_opengl/gl_resource_manager.cpp - renderer_opengl/gl_resource_manager.h - renderer_opengl/gl_shader_cache.cpp - renderer_opengl/gl_shader_cache.h - renderer_opengl/gl_shader_manager.cpp - renderer_opengl/gl_shader_manager.h - renderer_opengl/gl_shader_context.h - renderer_opengl/gl_shader_util.cpp - renderer_opengl/gl_shader_util.h - renderer_opengl/gl_state_tracker.cpp - renderer_opengl/gl_state_tracker.h - renderer_opengl/gl_staging_buffer_pool.cpp - renderer_opengl/gl_staging_buffer_pool.h - renderer_opengl/gl_texture_cache.cpp - renderer_opengl/gl_texture_cache.h - renderer_opengl/gl_texture_cache_base.cpp - renderer_opengl/gl_query_cache.cpp - renderer_opengl/gl_query_cache.h - renderer_opengl/maxwell_to_gl.h - renderer_opengl/renderer_opengl.cpp - renderer_opengl/renderer_opengl.h - renderer_opengl/util_shaders.cpp - renderer_opengl/util_shaders.h + + # Vulkan renderer_vulkan/present/anti_alias_pass.h renderer_vulkan/present/filters.cpp renderer_vulkan/present/filters.h @@ -244,6 +194,25 @@ add_library(video_core STATIC renderer_vulkan/vk_turbo_mode.h renderer_vulkan/vk_update_descriptor.cpp renderer_vulkan/vk_update_descriptor.h + vulkan_common/vulkan_debug_callback.cpp + vulkan_common/vulkan_debug_callback.h + vulkan_common/vulkan_device.cpp + vulkan_common/vulkan_device.h + vulkan_common/vulkan_instance.cpp + vulkan_common/vulkan_instance.h + vulkan_common/vulkan_library.cpp + vulkan_common/vulkan_library.h + vulkan_common/vulkan_memory_allocator.cpp + vulkan_common/vulkan_memory_allocator.h + vulkan_common/vulkan_surface.cpp + vulkan_common/vulkan_surface.h + vulkan_common/vulkan_wrapper.cpp + vulkan_common/vulkan_wrapper.h + vulkan_common/nsight_aftermath_tracker.cpp + vulkan_common/nsight_aftermath_tracker.h + vulkan_common/vma.h + vulkan_common/vulkan.h + shader_cache.cpp shader_cache.h shader_environment.cpp @@ -293,26 +262,67 @@ add_library(video_core STATIC transform_feedback.h video_core.cpp video_core.h - vulkan_common/vulkan_debug_callback.cpp - vulkan_common/vulkan_debug_callback.h - vulkan_common/vulkan_device.cpp - vulkan_common/vulkan_device.h - vulkan_common/vulkan_instance.cpp - vulkan_common/vulkan_instance.h - vulkan_common/vulkan_library.cpp - vulkan_common/vulkan_library.h - vulkan_common/vulkan_memory_allocator.cpp - vulkan_common/vulkan_memory_allocator.h - vulkan_common/vulkan_surface.cpp - vulkan_common/vulkan_surface.h - vulkan_common/vulkan_wrapper.cpp - vulkan_common/vulkan_wrapper.h - vulkan_common/nsight_aftermath_tracker.cpp - vulkan_common/nsight_aftermath_tracker.h - vulkan_common/vma.h - vulkan_common/vulkan.h ) +if (ENABLE_OPENGL) + target_sources(video_core PRIVATE + renderer_opengl/present/filters.cpp + renderer_opengl/present/filters.h + renderer_opengl/present/fsr.cpp + renderer_opengl/present/fsr.h + renderer_opengl/present/fxaa.cpp + renderer_opengl/present/fxaa.h + renderer_opengl/present/layer.cpp + renderer_opengl/present/layer.h + renderer_opengl/present/present_uniforms.h + renderer_opengl/present/smaa.cpp + renderer_opengl/present/smaa.h + renderer_opengl/present/util.h + renderer_opengl/present/window_adapt_pass.cpp + renderer_opengl/present/window_adapt_pass.h + renderer_opengl/blit_image.cpp + renderer_opengl/blit_image.h + renderer_opengl/gl_blit_screen.cpp + renderer_opengl/gl_blit_screen.h + renderer_opengl/gl_buffer_cache_base.cpp + renderer_opengl/gl_buffer_cache.cpp + renderer_opengl/gl_buffer_cache.h + renderer_opengl/gl_compute_pipeline.cpp + renderer_opengl/gl_compute_pipeline.h + renderer_opengl/gl_device.cpp + renderer_opengl/gl_device.h + renderer_opengl/gl_fence_manager.cpp + renderer_opengl/gl_fence_manager.h + renderer_opengl/gl_graphics_pipeline.cpp + renderer_opengl/gl_graphics_pipeline.h + renderer_opengl/gl_rasterizer.cpp + renderer_opengl/gl_rasterizer.h + renderer_opengl/gl_resource_manager.cpp + renderer_opengl/gl_resource_manager.h + renderer_opengl/gl_shader_cache.cpp + renderer_opengl/gl_shader_cache.h + renderer_opengl/gl_shader_manager.cpp + renderer_opengl/gl_shader_manager.h + renderer_opengl/gl_shader_context.h + renderer_opengl/gl_shader_util.cpp + renderer_opengl/gl_shader_util.h + renderer_opengl/gl_state_tracker.cpp + renderer_opengl/gl_state_tracker.h + renderer_opengl/gl_staging_buffer_pool.cpp + renderer_opengl/gl_staging_buffer_pool.h + renderer_opengl/gl_texture_cache.cpp + renderer_opengl/gl_texture_cache.h + renderer_opengl/gl_texture_cache_base.cpp + renderer_opengl/gl_query_cache.cpp + renderer_opengl/gl_query_cache.h + renderer_opengl/maxwell_to_gl.h + renderer_opengl/renderer_opengl.cpp + renderer_opengl/renderer_opengl.h + renderer_opengl/util_shaders.cpp + renderer_opengl/util_shaders.h + ) +endif() + target_link_libraries(video_core PUBLIC common core) target_link_libraries(video_core PUBLIC glad shader_recompiler stb bc_decoder gpu_logging) @@ -371,6 +381,10 @@ else() endif() endif() +if (ENABLE_OPENGL) + target_compile_definitions(video_core PUBLIC HAS_OPENGL) +endif() + if (ARCHITECTURE_x86_64) target_link_libraries(video_core PUBLIC xbyak::xbyak) endif() diff --git a/src/video_core/video_core.cpp b/src/video_core/video_core.cpp index 4a9751e208..fd91ee1667 100644 --- a/src/video_core/video_core.cpp +++ b/src/video_core/video_core.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2014 Citra Emulator Project @@ -9,26 +9,29 @@ #include "common/logging/log.h" #include "common/settings.h" #include "core/core.h" +#include "core/frontend/emu_window.h" +#include "core/frontend/graphics_context.h" #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/host1x/host1x.h" #include "video_core/renderer_base.h" #include "video_core/renderer_null/renderer_null.h" +#ifdef HAS_OPENGL #include "video_core/renderer_opengl/renderer_opengl.h" +#endif #include "video_core/renderer_vulkan/renderer_vulkan.h" #include "video_core/video_core.h" namespace { -std::unique_ptr CreateRenderer( - Core::System& system, Core::Frontend::EmuWindow& emu_window, Tegra::GPU& gpu, - std::unique_ptr context) { - auto& device_memory = system.Host1x().MemoryManager(); - +std::unique_ptr CreateRenderer(Core::System& system, Core::Frontend::EmuWindow& emu_window, Tegra::GPU& gpu, std::unique_ptr context) { + [[maybe_unused]] auto& device_memory = system.Host1x().MemoryManager(); switch (Settings::values.renderer_backend.GetValue()) { +#ifdef HAS_OPENGL case Settings::RendererBackend::OpenGL_GLSL: case Settings::RendererBackend::OpenGL_GLASM: case Settings::RendererBackend::OpenGL_SPIRV: return std::make_unique(emu_window, device_memory, gpu, std::move(context)); +#endif case Settings::RendererBackend::Vulkan: return std::make_unique(emu_window, device_memory, gpu, std::move(context)); case Settings::RendererBackend::Null: diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 69b3681d0a..0c0ce6e90c 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -1020,8 +1020,7 @@ bool GRenderWindow::InitializeOpenGL() { return true; #else - QMessageBox::warning(this, tr("OpenGL not available!"), - tr("Eden has not been compiled with OpenGL support.")); + QMessageBox::warning(this, tr("OpenGL not available!"), tr("Eden has not been compiled with OpenGL support.")); return false; #endif } @@ -1031,7 +1030,6 @@ bool GRenderWindow::InitializeVulkan() { child_widget = child; child_widget->windowHandle()->create(); main_context = std::make_unique(); - return true; } diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index 59aab0ef93..59fb5f3d9a 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -232,7 +232,6 @@ - @@ -417,11 +416,6 @@ Open &Mods Page - - - Open &Quickstart Guide - - &FAQ diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index a251451bea..1042133d48 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -670,6 +670,9 @@ MainWindow::MainWindow(bool has_broken_vulkan) // Launch game at path game_path = args[++i]; has_gamepath = true; + } else if (args[i] == QStringLiteral("-input-profile") && i < args.size() - 1) { + auto& players = Settings::values.players.GetValue(); + players[0].profile_name = args[++i].toStdString(); } else if (args[i] == QStringLiteral("-qlaunch")) { should_launch_qlaunch = true; } else if (args[i] == QStringLiteral("-setup")) { @@ -1621,7 +1624,6 @@ void MainWindow::ConnectMenuEvents() { connect_menu(ui->action_Stop, &MainWindow::OnStopGame); connect_menu(ui->action_Report_Compatibility, &MainWindow::OnMenuReportCompatibility); connect_menu(ui->action_Open_Mods_Page, &MainWindow::OnOpenModsPage); - connect_menu(ui->action_Open_Quickstart_Guide, &MainWindow::OnOpenQuickstartGuide); connect_menu(ui->action_Open_FAQ, &MainWindow::OnOpenFAQ); connect_menu(ui->action_Restart, &MainWindow::OnRestartGame); connect_menu(ui->action_Configure, &MainWindow::OnConfigure); @@ -1960,12 +1962,10 @@ bool MainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPar case Core::SystemResultStatus::ErrorVideoCore: QMessageBox::critical( this, tr("An error occurred initializing the video core."), - tr("Eden has encountered an error while running the video core. " - "This is usually caused by outdated GPU drivers, including integrated ones. " - "Please see the log for more details. " - "For more information on accessing the log, please see the following page: " - "" - "How to Upload the Log File. ")); + tr("This is usually caused by outdated GPU drivers, including integrated ones. " + "Please see the log for more details. See: " + "" + "How to access log files.")); break; default: if (result > Core::SystemResultStatus::ErrorLoader) { @@ -3317,12 +3317,8 @@ void MainWindow::OnOpenModsPage() { OpenURL(QUrl(QStringLiteral("https://github.com/eden-emulator/yuzu-mod-archive"))); } -void MainWindow::OnOpenQuickstartGuide() { - OpenURL(QUrl(QStringLiteral("https://yuzu-mirror.github.io/help/quickstart/"))); -} - void MainWindow::OnOpenFAQ() { - OpenURL(QUrl(QStringLiteral("https://yuzu-mirror.github.io/help"))); + OpenURL(QUrl(QStringLiteral("https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/README.md"))); } void MainWindow::ToggleFullscreen() { @@ -3784,14 +3780,30 @@ void MainWindow::OnToggleAdaptingFilter() { void MainWindow::OnToggleGraphicsAPI() { auto api = Settings::values.renderer_backend.GetValue(); - if (api != Settings::RendererBackend::Vulkan) { - api = Settings::RendererBackend::Vulkan; - } else { + switch (api) { #ifdef HAS_OPENGL + case Settings::RendererBackend::Vulkan: api = Settings::RendererBackend::OpenGL_GLSL; -#else + break; + case Settings::RendererBackend::OpenGL_GLSL: + api = Settings::RendererBackend::OpenGL_GLSL; + break; + case Settings::RendererBackend::OpenGL_SPIRV: + api = Settings::RendererBackend::OpenGL_GLASM; + break; + case Settings::RendererBackend::OpenGL_GLASM: api = Settings::RendererBackend::Null; + break; +#else + case Settings::RendererBackend::Vulkan: + api = Settings::RendererBackend::Null; + break; #endif + case Settings::RendererBackend::Null: + api = Settings::RendererBackend::Vulkan; + break; + default: + break; } Settings::values.renderer_backend.SetValue(api); renderer_status_button->setChecked(api == Settings::RendererBackend::Vulkan); diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h index e85aadaa3d..9a2a3fef04 100644 --- a/src/yuzu/main_window.h +++ b/src/yuzu/main_window.h @@ -344,7 +344,6 @@ private slots: void OnPrepareForSleep(bool prepare_sleep); void OnMenuReportCompatibility(); void OnOpenModsPage(); - void OnOpenQuickstartGuide(); void OnOpenFAQ(); /// Called whenever a user selects a game in the game list widget. diff --git a/src/yuzu/startup_checks.cpp b/src/yuzu/startup_checks.cpp index 2e77c7cd06..f54992d19a 100644 --- a/src/yuzu/startup_checks.cpp +++ b/src/yuzu/startup_checks.cpp @@ -1,6 +1,10 @@ +// 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 +// Don't move this! put it below and MSVC will get angry! #include "video_core/vulkan_common/vulkan_wrapper.h" #ifdef _WIN32 diff --git a/src/yuzu/vk_device_info.cpp b/src/yuzu/vk_device_info.cpp index 2b87ccd3ca..53dacdc090 100644 --- a/src/yuzu/vk_device_info.cpp +++ b/src/yuzu/vk_device_info.cpp @@ -77,4 +77,5 @@ void PopulateRecords(std::vector& records, QWindow* window) try { } catch (const Vulkan::vk::Exception& exception) { LOG_ERROR(Frontend, "Failed to enumerate devices with error: {}", exception.what()); } + } // namespace VkDeviceInfo diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp index b292b4886b..8124c91ea1 100644 --- a/src/yuzu_cmd/yuzu.cpp +++ b/src/yuzu_cmd/yuzu.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2014 Citra Emulator Project @@ -208,6 +208,7 @@ int main(int argc, char** argv) { std::string nickname{}; std::string password{}; std::string address{}; + std::string input_profile{}; u16 port = Network::DefaultRoomPort; static struct option long_options[] = { @@ -221,12 +222,13 @@ int main(int argc, char** argv) { {"program", optional_argument, 0, 'p'}, {"user", required_argument, 0, 'u'}, {"version", no_argument, 0, 'v'}, + {"input-profile", no_argument, 0, 'i'}, {0, 0, 0, 0}, // clang-format on }; while (optind < argc) { - int arg = getopt_long(argc, argv, "g:fhvp::c:u:d:", long_options, &option_index); + int arg = getopt_long(argc, argv, "g:fhvcip::c:u:d:", long_options, &option_index); if (arg != -1) { switch (char(arg)) { case 'd': @@ -245,6 +247,10 @@ int main(int argc, char** argv) { case 'g': filepath = std::string(optarg); break; + case 'i': { + input_profile = std::string(optarg); + break; + } case 'm': { use_multiplayer = true; const std::string str_arg(optarg); @@ -311,6 +317,11 @@ int main(int argc, char** argv) { Settings::values.program_args = program_args; } + if (!input_profile.empty()) { + auto& players = Settings::values.players.GetValue(); + players[0].profile_name = input_profile; + } + if (selected_user.has_value()) { Settings::values.current_user = std::clamp(*selected_user, 0, 7); }