summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt1
-rw-r--r--CMakeModules/FindSimpleIni.cmake19
-rw-r--r--externals/CMakeLists.txt8
-rw-r--r--externals/gamemode/CMakeLists.txt11
-rw-r--r--externals/gamemode/include/gamemode_client.h379
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt76
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt24
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt53
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt72
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt128
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt61
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt19
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt33
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt21
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt39
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt38
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt20
-rw-r--r--src/android/app/src/main/jni/android_config.cpp50
-rw-r--r--src/android/app/src/main/jni/android_config.h8
-rw-r--r--src/android/app/src/main/jni/android_settings.h8
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp16
-rw-r--r--src/android/app/src/main/jni/id_cache.h2
-rw-r--r--src/android/app/src/main/jni/native_config.cpp52
-rw-r--r--src/android/app/src/main/res/layout/card_folder.xml70
-rw-r--r--src/android/app/src/main/res/layout/dialog_add_folder.xml45
-rw-r--r--src/android/app/src/main/res/layout/dialog_folder_properties.xml30
-rw-r--r--src/android/app/src/main/res/layout/fragment_folders.xml48
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml7
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/strings.xml7
-rw-r--r--src/common/CMakeLists.txt9
-rw-r--r--src/common/linux/gamemode.cpp40
-rw-r--r--src/common/linux/gamemode.h24
-rw-r--r--src/common/settings.cpp2
-rw-r--r--src/common/settings.h5
-rw-r--r--src/common/settings_common.h1
-rw-r--r--src/core/frontend/emu_window.h10
-rw-r--r--src/core/hid/emulated_controller.cpp50
-rw-r--r--src/core/hid/emulated_controller.h1
-rw-r--r--src/core/hle/service/am/applets/applet_cabinet.cpp5
-rw-r--r--src/core/hle/service/hid/controllers/touchscreen.cpp12
-rw-r--r--src/core/hle/service/hid/controllers/touchscreen.h4
-rw-r--r--src/core/hle/service/hid/hid_server.cpp16
-rw-r--r--src/core/hle/service/hid/hid_server.h1
-rw-r--r--src/core/hle/service/mii/types/ver3_store_data.cpp2
-rw-r--r--src/core/hle/service/nfc/common/device.cpp68
-rw-r--r--src/core/hle/service/nfc/common/device.h1
-rw-r--r--src/core/hle/service/set/set_sys.cpp70
-rw-r--r--src/core/hle/service/set/set_sys.h24
-rw-r--r--src/core/hle/service/time/clock_types.h5
-rw-r--r--src/frontend_common/CMakeLists.txt2
-rw-r--r--src/frontend_common/config.cpp2
-rw-r--r--src/frontend_common/config.h1
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm_memory.cpp9
-rw-r--r--src/shader_recompiler/backend/glsl/glsl_emit_context.cpp5
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.cpp4
-rw-r--r--src/shader_recompiler/frontend/maxwell/translate_program.cpp2
-rw-r--r--src/shader_recompiler/host_translate_info.h1
-rw-r--r--src/shader_recompiler/ir_opt/global_memory_to_storage_buffer_pass.cpp13
-rw-r--r--src/shader_recompiler/ir_opt/passes.h2
-rw-r--r--src/shader_recompiler/profile.h2
-rw-r--r--src/video_core/CMakeLists.txt1
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h117
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h4
-rw-r--r--src/video_core/buffer_cache/usage_tracker.h79
-rw-r--r--src/video_core/renderer_opengl/gl_buffer_cache.cpp7
-rw-r--r--src/video_core/renderer_opengl/gl_buffer_cache.h21
-rw-r--r--src/video_core/renderer_opengl/gl_device.cpp18
-rw-r--r--src/video_core/renderer_opengl/gl_shader_cache.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.cpp40
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.h23
-rw-r--r--src/video_core/renderer_vulkan/vk_master_semaphore.cpp29
-rw-r--r--src/video_core/renderer_vulkan/vk_master_semaphore.h14
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vk_scheduler.cpp28
-rw-r--r--src/video_core/renderer_vulkan/vk_scheduler.h21
-rw-r--r--src/video_core/renderer_vulkan/vk_smaa.cpp4
-rw-r--r--src/video_core/renderer_vulkan/vk_staging_buffer_pool.h4
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp18
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h1
-rw-r--r--src/video_core/texture_cache/slot_vector.h4
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp37
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h4
-rw-r--r--src/yuzu/CMakeLists.txt2
-rw-r--r--src/yuzu/bootmanager.cpp56
-rw-r--r--src/yuzu/bootmanager.h6
-rw-r--r--src/yuzu/configuration/configure_debug.cpp3
-rw-r--r--src/yuzu/configuration/configure_debug.ui86
-rw-r--r--src/yuzu/configuration/configure_general.cpp43
-rw-r--r--src/yuzu/configuration/configure_general.ui27
-rw-r--r--src/yuzu/configuration/configure_system.ui2
-rw-r--r--src/yuzu/configuration/shared_translation.cpp3
-rw-r--r--src/yuzu/main.cpp113
-rw-r--r--src/yuzu/main.h6
-rw-r--r--src/yuzu_cmd/yuzu.cpp12
102 files changed, 2221 insertions, 433 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 325f39e1e..599f7c831 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -295,6 +295,7 @@ find_package(lz4 REQUIRED)
find_package(nlohmann_json 3.8 REQUIRED)
find_package(Opus 1.3 MODULE)
find_package(RenderDoc MODULE)
+find_package(SimpleIni MODULE)
find_package(stb MODULE)
find_package(VulkanMemoryAllocator CONFIG)
find_package(ZLIB 1.2 REQUIRED)
diff --git a/CMakeModules/FindSimpleIni.cmake b/CMakeModules/FindSimpleIni.cmake
new file mode 100644
index 000000000..ce75d7690
--- /dev/null
+++ b/CMakeModules/FindSimpleIni.cmake
@@ -0,0 +1,19 @@
+# SPDX-FileCopyrightText: 2023 Alexandre Bouvier <contact@amb.tf>
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+find_path(SimpleIni_INCLUDE_DIR SimpleIni.h)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(SimpleIni
+ REQUIRED_VARS SimpleIni_INCLUDE_DIR
+)
+
+if (SimpleIni_FOUND AND NOT TARGET SimpleIni::SimpleIni)
+ add_library(SimpleIni::SimpleIni INTERFACE IMPORTED)
+ set_target_properties(SimpleIni::SimpleIni PROPERTIES
+ INTERFACE_INCLUDE_DIRECTORIES "${SimpleIni_INCLUDE_DIR}"
+ )
+endif()
+
+mark_as_advanced(SimpleIni_INCLUDE_DIR)
diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt
index 8f83d4991..8e1fc696a 100644
--- a/externals/CMakeLists.txt
+++ b/externals/CMakeLists.txt
@@ -193,6 +193,10 @@ if (ANDROID)
endif()
endif()
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ add_subdirectory(gamemode)
+endif()
+
# Breakpad
# https://github.com/microsoft/vcpkg/blob/master/ports/breakpad/CMakeLists.txt
if (YUZU_CRASH_DUMPS AND NOT TARGET libbreakpad_client)
@@ -296,4 +300,6 @@ if (YUZU_CRASH_DUMPS AND NOT TARGET libbreakpad_client)
endif()
# SimpleIni
-add_subdirectory(simpleini)
+if (NOT TARGET SimpleIni::SimpleIni)
+ add_subdirectory(simpleini)
+endif()
diff --git a/externals/gamemode/CMakeLists.txt b/externals/gamemode/CMakeLists.txt
new file mode 100644
index 000000000..87095642e
--- /dev/null
+++ b/externals/gamemode/CMakeLists.txt
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+project(gamemode LANGUAGES CXX C)
+
+add_library(gamemode include/gamemode_client.h)
+
+target_link_libraries(gamemode PRIVATE common)
+
+target_include_directories(gamemode PUBLIC include)
+set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C)
diff --git a/externals/gamemode/include/gamemode_client.h b/externals/gamemode/include/gamemode_client.h
new file mode 100644
index 000000000..184812334
--- /dev/null
+++ b/externals/gamemode/include/gamemode_client.h
@@ -0,0 +1,379 @@
+// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive
+// SPDX-License-Identifier: BSD-3-Clause
+
+/*
+
+Copyright (c) 2017-2019, Feral Interactive
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Feral Interactive nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+ */
+#ifndef CLIENT_GAMEMODE_H
+#define CLIENT_GAMEMODE_H
+/*
+ * GameMode supports the following client functions
+ * Requests are refcounted in the daemon
+ *
+ * int gamemode_request_start() - Request gamemode starts
+ * 0 if the request was sent successfully
+ * -1 if the request failed
+ *
+ * int gamemode_request_end() - Request gamemode ends
+ * 0 if the request was sent successfully
+ * -1 if the request failed
+ *
+ * GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
+ * destruction, as appropriate. In this configuration, errors will be printed to stderr
+ *
+ * int gamemode_query_status() - Query the current status of gamemode
+ * 0 if gamemode is inactive
+ * 1 if gamemode is active
+ * 2 if gamemode is active and this client is registered
+ * -1 if the query failed
+ *
+ * int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
+ * 0 if the request was sent successfully
+ * -1 if the request failed
+ * -2 if the request was rejected
+ *
+ * int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
+ * 0 if the request was sent successfully
+ * -1 if the request failed
+ * -2 if the request was rejected
+ *
+ * int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
+ * 0 if gamemode is inactive
+ * 1 if gamemode is active
+ * 2 if gamemode is active and this client is registered
+ * -1 if the query failed
+ *
+ * const char* gamemode_error_string() - Get an error string
+ * returns a string describing any of the above errors
+ *
+ * Note: All the above requests can be blocking - dbus requests can and will block while the daemon
+ * handles the request. It is not recommended to make these calls in performance critical code
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+
+#include <dlfcn.h>
+#include <string.h>
+
+#include <assert.h>
+
+#include <sys/types.h>
+
+static char internal_gamemode_client_error_string[512] = { 0 };
+
+/**
+ * Load libgamemode dynamically to dislodge us from most dependencies.
+ * This allows clients to link and/or use this regardless of runtime.
+ * See SDL2 for an example of the reasoning behind this in terms of
+ * dynamic versioning as well.
+ */
+static volatile int internal_libgamemode_loaded = 1;
+
+/* Typedefs for the functions to load */
+typedef int (*api_call_return_int)(void);
+typedef const char *(*api_call_return_cstring)(void);
+typedef int (*api_call_pid_return_int)(pid_t);
+
+/* Storage for functors */
+static api_call_return_int REAL_internal_gamemode_request_start = NULL;
+static api_call_return_int REAL_internal_gamemode_request_end = NULL;
+static api_call_return_int REAL_internal_gamemode_query_status = NULL;
+static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
+static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
+static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
+static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
+
+/**
+ * Internal helper to perform the symbol binding safely.
+ *
+ * Returns 0 on success and -1 on failure
+ */
+__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(
+ void *handle, const char *name, void **out_func, size_t func_size, bool required)
+{
+ void *symbol_lookup = NULL;
+ char *dl_error = NULL;
+
+ /* Safely look up the symbol */
+ symbol_lookup = dlsym(handle, name);
+ dl_error = dlerror();
+ if (required && (dl_error || !symbol_lookup)) {
+ snprintf(internal_gamemode_client_error_string,
+ sizeof(internal_gamemode_client_error_string),
+ "dlsym failed - %s",
+ dl_error);
+ return -1;
+ }
+
+ /* Have the symbol correctly, copy it to make it usable */
+ memcpy(out_func, &symbol_lookup, func_size);
+ return 0;
+}
+
+/**
+ * Loads libgamemode and needed functions
+ *
+ * Returns 0 on success and -1 on failure
+ */
+__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
+{
+ /* We start at 1, 0 is a success and -1 is a fail */
+ if (internal_libgamemode_loaded != 1) {
+ return internal_libgamemode_loaded;
+ }
+
+ /* Anonymous struct type to define our bindings */
+ struct binding {
+ const char *name;
+ void **functor;
+ size_t func_size;
+ bool required;
+ } bindings[] = {
+ { "real_gamemode_request_start",
+ (void **)&REAL_internal_gamemode_request_start,
+ sizeof(REAL_internal_gamemode_request_start),
+ true },
+ { "real_gamemode_request_end",
+ (void **)&REAL_internal_gamemode_request_end,
+ sizeof(REAL_internal_gamemode_request_end),
+ true },
+ { "real_gamemode_query_status",
+ (void **)&REAL_internal_gamemode_query_status,
+ sizeof(REAL_internal_gamemode_query_status),
+ false },
+ { "real_gamemode_error_string",
+ (void **)&REAL_internal_gamemode_error_string,
+ sizeof(REAL_internal_gamemode_error_string),
+ true },
+ { "real_gamemode_request_start_for",
+ (void **)&REAL_internal_gamemode_request_start_for,
+ sizeof(REAL_internal_gamemode_request_start_for),
+ false },
+ { "real_gamemode_request_end_for",
+ (void **)&REAL_internal_gamemode_request_end_for,
+ sizeof(REAL_internal_gamemode_request_end_for),
+ false },
+ { "real_gamemode_query_status_for",
+ (void **)&REAL_internal_gamemode_query_status_for,
+ sizeof(REAL_internal_gamemode_query_status_for),
+ false },
+ };
+
+ void *libgamemode = NULL;
+
+ /* Try and load libgamemode */
+ libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
+ if (!libgamemode) {
+ /* Attempt to load unversioned library for compatibility with older
+ * versions (as of writing, there are no ABI changes between the two -
+ * this may need to change if ever ABI-breaking changes are made) */
+ libgamemode = dlopen("libgamemode.so", RTLD_NOW);
+ if (!libgamemode) {
+ snprintf(internal_gamemode_client_error_string,
+ sizeof(internal_gamemode_client_error_string),
+ "dlopen failed - %s",
+ dlerror());
+ internal_libgamemode_loaded = -1;
+ return -1;
+ }
+ }
+
+ /* Attempt to bind all symbols */
+ for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
+ struct binding *binder = &bindings[i];
+
+ if (internal_bind_libgamemode_symbol(libgamemode,
+ binder->name,
+ binder->functor,
+ binder->func_size,
+ binder->required)) {
+ internal_libgamemode_loaded = -1;
+ return -1;
+ };
+ }
+
+ /* Success */
+ internal_libgamemode_loaded = 0;
+ return 0;
+}
+
+/**
+ * Redirect to the real libgamemode
+ */
+__attribute__((always_inline)) static inline const char *gamemode_error_string(void)
+{
+ /* If we fail to load the system gamemode, or we have an error string already, return our error
+ * string instead of diverting to the system version */
+ if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
+ return internal_gamemode_client_error_string;
+ }
+
+ /* Assert for static analyser that the function is not NULL */
+ assert(REAL_internal_gamemode_error_string != NULL);
+
+ return REAL_internal_gamemode_error_string();
+}
+
+/**
+ * Redirect to the real libgamemode
+ * Allow automatically requesting game mode
+ * Also prints errors as they happen.
+ */
+#ifdef GAMEMODE_AUTO
+__attribute__((constructor))
+#else
+__attribute__((always_inline)) static inline
+#endif
+int gamemode_request_start(void)
+{
+ /* Need to load gamemode */
+ if (internal_load_libgamemode() < 0) {
+#ifdef GAMEMODE_AUTO
+ fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
+#endif
+ return -1;
+ }
+
+ /* Assert for static analyser that the function is not NULL */
+ assert(REAL_internal_gamemode_request_start != NULL);
+
+ if (REAL_internal_gamemode_request_start() < 0) {
+#ifdef GAMEMODE_AUTO
+ fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
+#endif
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Redirect to the real libgamemode */
+#ifdef GAMEMODE_AUTO
+__attribute__((destructor))
+#else
+__attribute__((always_inline)) static inline
+#endif
+int gamemode_request_end(void)
+{
+ /* Need to load gamemode */
+ if (internal_load_libgamemode() < 0) {
+#ifdef GAMEMODE_AUTO
+ fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
+#endif
+ return -1;
+ }
+
+ /* Assert for static analyser that the function is not NULL */
+ assert(REAL_internal_gamemode_request_end != NULL);
+
+ if (REAL_internal_gamemode_request_end() < 0) {
+#ifdef GAMEMODE_AUTO
+ fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
+#endif
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Redirect to the real libgamemode */
+__attribute__((always_inline)) static inline int gamemode_query_status(void)
+{
+ /* Need to load gamemode */
+ if (internal_load_libgamemode() < 0) {
+ return -1;
+ }
+
+ if (REAL_internal_gamemode_query_status == NULL) {
+ snprintf(internal_gamemode_client_error_string,
+ sizeof(internal_gamemode_client_error_string),
+ "gamemode_query_status missing (older host?)");
+ return -1;
+ }
+
+ return REAL_internal_gamemode_query_status();
+}
+
+/* Redirect to the real libgamemode */
+__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
+{
+ /* Need to load gamemode */
+ if (internal_load_libgamemode() < 0) {
+ return -1;
+ }
+
+ if (REAL_internal_gamemode_request_start_for == NULL) {
+ snprintf(internal_gamemode_client_error_string,
+ sizeof(internal_gamemode_client_error_string),
+ "gamemode_request_start_for missing (older host?)");
+ return -1;
+ }
+
+ return REAL_internal_gamemode_request_start_for(pid);
+}
+
+/* Redirect to the real libgamemode */
+__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
+{
+ /* Need to load gamemode */
+ if (internal_load_libgamemode() < 0) {
+ return -1;
+ }
+
+ if (REAL_internal_gamemode_request_end_for == NULL) {
+ snprintf(internal_gamemode_client_error_string,
+ sizeof(internal_gamemode_client_error_string),
+ "gamemode_request_end_for missing (older host?)");
+ return -1;
+ }
+
+ return REAL_internal_gamemode_request_end_for(pid);
+}
+
+/* Redirect to the real libgamemode */
+__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
+{
+ /* Need to load gamemode */
+ if (internal_load_libgamemode() < 0) {
+ return -1;
+ }
+
+ if (REAL_internal_gamemode_query_status_for == NULL) {
+ snprintf(internal_gamemode_client_error_string,
+ sizeof(internal_gamemode_client_error_string),
+ "gamemode_query_status_for missing (older host?)");
+ return -1;
+ }
+
+ return REAL_internal_gamemode_query_status_for(pid);
+}
+
+#endif // CLIENT_GAMEMODE_H
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
new file mode 100644
index 000000000..ab657a7b9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/FolderAdapter.kt
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.net.Uri
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.CardFolderBinding
+import org.yuzu.yuzu_emu.fragments.GameFolderPropertiesDialogFragment
+import org.yuzu.yuzu_emu.model.GameDir
+import org.yuzu.yuzu_emu.model.GamesViewModel
+
+class FolderAdapter(val activity: FragmentActivity, val gamesViewModel: GamesViewModel) :
+ ListAdapter<GameDir, FolderAdapter.FolderViewHolder>(
+ AsyncDifferConfig.Builder(DiffCallback()).build()
+ ) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int
+ ): FolderAdapter.FolderViewHolder {
+ CardFolderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ .also { return FolderViewHolder(it) }
+ }
+
+ override fun onBindViewHolder(holder: FolderAdapter.FolderViewHolder, position: Int) =
+ holder.bind(currentList[position])
+
+ inner class FolderViewHolder(val binding: CardFolderBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ private lateinit var gameDir: GameDir
+
+ fun bind(gameDir: GameDir) {
+ this.gameDir = gameDir
+
+ binding.apply {
+ path.text = Uri.parse(gameDir.uriString).path
+ path.postDelayed(
+ {
+ path.isSelected = true
+ path.ellipsize = TextUtils.TruncateAt.MARQUEE
+ },
+ 3000
+ )
+
+ buttonEdit.setOnClickListener {
+ GameFolderPropertiesDialogFragment.newInstance(this@FolderViewHolder.gameDir)
+ .show(
+ activity.supportFragmentManager,
+ GameFolderPropertiesDialogFragment.TAG
+ )
+ }
+
+ buttonDelete.setOnClickListener {
+ gamesViewModel.removeFolder(this@FolderViewHolder.gameDir)
+ }
+ }
+ }
+ }
+
+ private class DiffCallback : DiffUtil.ItemCallback<GameDir>() {
+ override fun areItemsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
+ return oldItem == newItem
+ }
+
+ override fun areContentsTheSame(oldItem: GameDir, newItem: GameDir): Boolean {
+ return oldItem == newItem
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index d005c656e..e3cd66185 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -3,33 +3,9 @@
package org.yuzu.yuzu_emu.features.settings.model
-import android.text.TextUtils
-import android.widget.Toast
import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.utils.NativeConfig
object Settings {
- private val context get() = YuzuApplication.appContext
-
- fun saveSettings(gameId: String = "") {
- if (TextUtils.isEmpty(gameId)) {
- Toast.makeText(
- context,
- context.getString(R.string.ini_saved),
- Toast.LENGTH_SHORT
- ).show()
- NativeConfig.saveSettings()
- } else {
- // TODO: Save custom game settings
- Toast.makeText(
- context,
- context.getString(R.string.gameid_saved, gameId),
- Toast.LENGTH_SHORT
- ).show()
- }
- }
-
enum class Category {
Android,
Audio,
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 48bdbdd75..64bfc6dd0 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
@@ -19,12 +19,13 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
-import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.fragments.ResetSettingsDialogFragment
import org.yuzu.yuzu_emu.model.SettingsViewModel
@@ -53,10 +54,6 @@ class SettingsActivity : AppCompatActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false)
- if (savedInstanceState != null) {
- settingsViewModel.shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
- }
-
if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION
) {
@@ -127,12 +124,6 @@ class SettingsActivity : AppCompatActivity() {
}
}
- override fun onSaveInstanceState(outState: Bundle) {
- // Critical: If super method is not called, rotations will be busted.
- super.onSaveInstanceState(outState)
- outState.putBoolean(KEY_SHOULD_SAVE, settingsViewModel.shouldSave)
- }
-
override fun onStart() {
super.onStart()
// TODO: Load custom settings contextually
@@ -141,16 +132,10 @@ class SettingsActivity : AppCompatActivity() {
}
}
- /**
- * If this is called, the user has left the settings screen (potentially through the
- * home button) and will expect their changes to be persisted. So we kick off an
- * IntentService which will do so on a background thread.
- */
override fun onStop() {
super.onStop()
- if (isFinishing && settingsViewModel.shouldSave) {
- Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
- Settings.saveSettings()
+ CoroutineScope(Dispatchers.IO).launch {
+ NativeConfig.saveSettings()
}
}
@@ -160,9 +145,6 @@ class SettingsActivity : AppCompatActivity() {
}
fun onSettingsReset() {
- // Prevents saving to a non-existent settings file
- settingsViewModel.shouldSave = false
-
// Delete settings file because the user may have changed values that do not exist in the UI
NativeConfig.unloadConfig()
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
@@ -194,8 +176,4 @@ class SettingsActivity : AppCompatActivity() {
windowInsets
}
}
-
- companion object {
- private const val KEY_SHOULD_SAVE = "should_save"
- }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index a7a029fc1..af2c1e582 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -105,7 +105,6 @@ class SettingsAdapter(
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
item.checked = checked
settingsViewModel.setShouldReloadSettingsList(true)
- settingsViewModel.shouldSave = true
}
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
@@ -161,7 +160,6 @@ class SettingsAdapter(
epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60
if (item.value != epochTime) {
- settingsViewModel.shouldSave = true
notifyItemChanged(position)
item.value = epochTime
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
new file mode 100644
index 000000000..dec2b7cf1
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddGameFolderDialogFragment.kt
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
+import org.yuzu.yuzu_emu.model.GameDir
+import org.yuzu.yuzu_emu.model.GamesViewModel
+
+class AddGameFolderDialogFragment : DialogFragment() {
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val binding = DialogAddFolderBinding.inflate(layoutInflater)
+ val folderUriString = requireArguments().getString(FOLDER_URI_STRING)
+ if (folderUriString == null) {
+ dismiss()
+ }
+ binding.path.text = Uri.parse(folderUriString).path
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(R.string.add_game_folder)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+ val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
+ gamesViewModel.addFolder(newGameDir)
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .setView(binding.root)
+ .show()
+ }
+
+ companion object {
+ const val TAG = "AddGameFolderDialogFragment"
+
+ private const val FOLDER_URI_STRING = "FolderUriString"
+
+ fun newInstance(folderUriString: String): AddGameFolderDialogFragment {
+ val args = Bundle()
+ args.putString(FOLDER_URI_STRING, folderUriString)
+ val fragment = AddGameFolderDialogFragment()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
new file mode 100644
index 000000000..b6c2e4635
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
+import org.yuzu.yuzu_emu.model.GameDir
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
+
+class GameFolderPropertiesDialogFragment : DialogFragment() {
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ private var deepScan = false
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val binding = DialogFolderPropertiesBinding.inflate(layoutInflater)
+ val gameDir = requireArguments().parcelable<GameDir>(GAME_DIR)!!
+
+ // Restore checkbox state
+ binding.deepScanSwitch.isChecked =
+ savedInstanceState?.getBoolean(DEEP_SCAN) ?: gameDir.deepScan
+
+ // Ensure that we can get the checkbox state even if the view is destroyed
+ deepScan = binding.deepScanSwitch.isChecked
+ binding.deepScanSwitch.setOnClickListener {
+ deepScan = binding.deepScanSwitch.isChecked
+ }
+
+ return MaterialAlertDialogBuilder(requireContext())
+ .setView(binding.root)
+ .setTitle(R.string.game_folder_properties)
+ .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+ val folderIndex = gamesViewModel.folders.value.indexOf(gameDir)
+ if (folderIndex != -1) {
+ gamesViewModel.folders.value[folderIndex].deepScan =
+ binding.deepScanSwitch.isChecked
+ gamesViewModel.updateGameDirs()
+ }
+ }
+ .setNegativeButton(android.R.string.cancel, null)
+ .show()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putBoolean(DEEP_SCAN, deepScan)
+ }
+
+ companion object {
+ const val TAG = "GameFolderPropertiesDialogFragment"
+
+ private const val GAME_DIR = "GameDir"
+
+ private const val DEEP_SCAN = "DeepScan"
+
+ fun newInstance(gameDir: GameDir): GameFolderPropertiesDialogFragment {
+ val args = Bundle()
+ args.putParcelable(GAME_DIR, gameDir)
+ val fragment = GameFolderPropertiesDialogFragment()
+ fragment.arguments = args
+ return fragment
+ }
+ }
+}
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
new file mode 100644
index 000000000..341a37fdb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFoldersFragment.kt
@@ -0,0 +1,128 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.FolderAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentFoldersBinding
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+
+class GameFoldersFragment : Fragment() {
+ private var _binding: FragmentFoldersBinding? = null
+ private val binding get() = _binding!!
+
+ private val homeViewModel: HomeViewModel by activityViewModels()
+ private val gamesViewModel: GamesViewModel by activityViewModels()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+ returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+ reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+
+ gamesViewModel.onOpenGameFoldersFragment()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentFoldersBinding.inflate(inflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ homeViewModel.setNavigationVisibility(visible = false, animated = true)
+ homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+ binding.toolbarFolders.setNavigationOnClickListener {
+ binding.root.findNavController().popBackStack()
+ }
+
+ binding.listFolders.apply {
+ layoutManager = GridLayoutManager(
+ requireContext(),
+ resources.getInteger(R.integer.grid_columns)
+ )
+ adapter = FolderAdapter(requireActivity(), gamesViewModel)
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ gamesViewModel.folders.collect {
+ (binding.listFolders.adapter as FolderAdapter).submitList(it)
+ }
+ }
+ }
+
+ val mainActivity = requireActivity() as MainActivity
+ binding.buttonAdd.setOnClickListener {
+ mainActivity.getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
+ }
+
+ setInsets()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ gamesViewModel.onCloseGameFoldersFragment()
+ }
+
+ private fun setInsets() =
+ ViewCompat.setOnApplyWindowInsetsListener(
+ binding.root
+ ) { _: View, windowInsets: WindowInsetsCompat ->
+ val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+ val leftInsets = barInsets.left + cutoutInsets.left
+ val rightInsets = barInsets.right + cutoutInsets.right
+
+ val mlpToolbar = binding.toolbarFolders.layoutParams as ViewGroup.MarginLayoutParams
+ mlpToolbar.leftMargin = leftInsets
+ mlpToolbar.rightMargin = rightInsets
+ binding.toolbarFolders.layoutParams = mlpToolbar
+
+ val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+ val mlpFab =
+ binding.buttonAdd.layoutParams as ViewGroup.MarginLayoutParams
+ mlpFab.leftMargin = leftInsets + fabSpacing
+ mlpFab.rightMargin = rightInsets + fabSpacing
+ mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+ binding.buttonAdd.layoutParams = mlpFab
+
+ val mlpListFolders = binding.listFolders.layoutParams as ViewGroup.MarginLayoutParams
+ mlpListFolders.leftMargin = leftInsets
+ mlpListFolders.rightMargin = rightInsets
+ binding.listFolders.layoutParams = mlpListFolders
+
+ binding.listFolders.updatePadding(
+ bottom = barInsets.bottom +
+ resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+ )
+
+ windowInsets
+ }
+}
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 4720daec4..3addc2e63 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
@@ -127,18 +127,13 @@ class HomeSettingsFragment : Fragment() {
)
add(
HomeSetting(
- R.string.select_games_folder,
+ R.string.manage_game_folders,
R.string.select_games_folder_description,
R.drawable.ic_add,
{
- mainActivity.getGamesDirectory.launch(
- Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
- )
- },
- { true },
- 0,
- 0,
- homeViewModel.gamesDir
+ binding.root.findNavController()
+ .navigate(R.id.action_homeSettingsFragment_to_gameFoldersFragment)
+ }
)
)
add(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
index d18ec6974..b88d2c038 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
@@ -52,7 +52,6 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
settingsViewModel.clickedItem!!.setting.reset()
settingsViewModel.setAdapterItemChanged(position)
- settingsViewModel.shouldSave = true
}
.setNegativeButton(android.R.string.cancel, null)
.create()
@@ -137,24 +136,17 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
- if (scSetting.selectedValue != value) {
- settingsViewModel.shouldSave = true
- }
scSetting.selectedValue = value
}
is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
- if (scSetting.selectedValue != value) settingsViewModel.shouldSave = true
scSetting.selectedValue = value
}
is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
- if (sliderSetting.selectedValue != settingsViewModel.sliderProgress.value) {
- settingsViewModel.shouldSave = true
- }
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
}
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index c66bb635a..c4277735d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -42,7 +42,7 @@ import org.yuzu.yuzu_emu.model.SetupPage
import org.yuzu.yuzu_emu.model.StepState
import org.yuzu.yuzu_emu.ui.main.MainActivity
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
-import org.yuzu.yuzu_emu.utils.GameHelper
+import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.ViewUtils
class SetupFragment : Fragment() {
@@ -184,11 +184,7 @@ class SetupFragment : Fragment() {
R.string.add_games_warning_description,
R.string.add_games_warning_help,
{
- val preferences =
- PreferenceManager.getDefaultSharedPreferences(
- YuzuApplication.appContext
- )
- if (preferences.getString(GameHelper.KEY_GAME_PATH, "")!!.isNotEmpty()) {
+ if (NativeConfig.getGameDirs().isNotEmpty()) {
StepState.COMPLETE
} else {
StepState.INCOMPLETE
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt
new file mode 100644
index 000000000..274bc1c7b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameDir.kt
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class GameDir(
+ val uriString: String,
+ var deepScan: Boolean
+) : Parcelable
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index 8512ed17c..752d98c10 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -12,6 +12,7 @@ import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
@@ -20,6 +21,7 @@ import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.GameMetadata
+import org.yuzu.yuzu_emu.utils.NativeConfig
class GamesViewModel : ViewModel() {
val games: StateFlow<List<Game>> get() = _games
@@ -40,6 +42,9 @@ class GamesViewModel : ViewModel() {
val searchFocused: StateFlow<Boolean> get() = _searchFocused
private val _searchFocused = MutableStateFlow(false)
+ private val _folders = MutableStateFlow(mutableListOf<GameDir>())
+ val folders = _folders.asStateFlow()
+
init {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
@@ -50,6 +55,7 @@ class GamesViewModel : ViewModel() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
+ getGameDirs()
if (storedGames!!.isNotEmpty()) {
val deserializedGames = mutableSetOf<Game>()
storedGames.forEach {
@@ -104,7 +110,7 @@ class GamesViewModel : ViewModel() {
_searchFocused.value = searchFocused
}
- fun reloadGames(directoryChanged: Boolean) {
+ fun reloadGames(directoriesChanged: Boolean) {
if (isReloading.value) {
return
}
@@ -116,10 +122,61 @@ class GamesViewModel : ViewModel() {
setGames(GameHelper.getGames())
_isReloading.value = false
- if (directoryChanged) {
+ if (directoriesChanged) {
setShouldSwapData(true)
}
}
}
}
+
+ fun addFolder(gameDir: GameDir) =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ NativeConfig.addGameDir(gameDir)
+ getGameDirs()
+ }
+ }
+
+ fun removeFolder(gameDir: GameDir) =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ val gameDirs = _folders.value.toMutableList()
+ val removedDirIndex = gameDirs.indexOf(gameDir)
+ if (removedDirIndex != -1) {
+ gameDirs.removeAt(removedDirIndex)
+ NativeConfig.setGameDirs(gameDirs.toTypedArray())
+ getGameDirs()
+ }
+ }
+ }
+
+ fun updateGameDirs() =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ NativeConfig.setGameDirs(_folders.value.toTypedArray())
+ getGameDirs()
+ }
+ }
+
+ fun onOpenGameFoldersFragment() =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ getGameDirs()
+ }
+ }
+
+ fun onCloseGameFoldersFragment() =
+ viewModelScope.launch {
+ withContext(Dispatchers.IO) {
+ getGameDirs(true)
+ }
+ }
+
+ private fun getGameDirs(reloadList: Boolean = false) {
+ val gameDirs = NativeConfig.getGameDirs()
+ _folders.value = gameDirs.toMutableList()
+ if (reloadList) {
+ reloadGames(true)
+ }
+ }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
index 756f76721..251b5a667 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -3,15 +3,9 @@
package org.yuzu.yuzu_emu.model
-import android.net.Uri
-import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import androidx.preference.PreferenceManager
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.utils.GameHelper
class HomeViewModel : ViewModel() {
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
@@ -23,14 +17,6 @@ class HomeViewModel : ViewModel() {
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
private val _shouldPageForward = MutableStateFlow(false)
- val gamesDir: StateFlow<String> get() = _gamesDir
- private val _gamesDir = MutableStateFlow(
- Uri.parse(
- PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
- .getString(GameHelper.KEY_GAME_PATH, "")
- ).path ?: ""
- )
-
var navigatedToSetup = false
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@@ -50,9 +36,4 @@ class HomeViewModel : ViewModel() {
fun setShouldPageForward(pageForward: Boolean) {
_shouldPageForward.value = pageForward
}
-
- fun setGamesDir(activity: FragmentActivity, dir: String) {
- ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
- _gamesDir.value = dir
- }
}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
index 6f947674e..ccc981e95 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
@@ -13,8 +13,6 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
class SettingsViewModel : ViewModel() {
var game: Game? = null
- var shouldSave = false
-
var clickedItem: SettingsItem? = null
val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate
@@ -73,6 +71,5 @@ class SettingsViewModel : ViewModel() {
fun clear() {
game = null
- shouldSave = false
}
}
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 bd2f4cd25..16323a316 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
@@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.getPublicFilesDir
@@ -252,6 +253,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
super.onResume()
}
+ override fun onStop() {
+ super.onStop()
+ CoroutineScope(Dispatchers.IO).launch {
+ NativeConfig.saveSettings()
+ }
+ }
+
override fun onDestroy() {
EmulationActivity.stopForegroundService(this)
super.onDestroy()
@@ -293,20 +301,19 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
- // When a new directory is picked, we currently will reset the existing games
- // database. This effectively means that only one game directory is supported.
- PreferenceManager.getDefaultSharedPreferences(applicationContext).edit()
- .putString(GameHelper.KEY_GAME_PATH, result.toString())
- .apply()
-
- Toast.makeText(
- applicationContext,
- R.string.games_dir_selected,
- Toast.LENGTH_LONG
- ).show()
+ val uriString = result.toString()
+ val folder = gamesViewModel.folders.value.firstOrNull { it.uriString == uriString }
+ if (folder != null) {
+ Toast.makeText(
+ applicationContext,
+ R.string.folder_already_added,
+ Toast.LENGTH_SHORT
+ ).show()
+ return
+ }
- gamesViewModel.reloadGames(true)
- homeViewModel.setGamesDir(this, result.path!!)
+ AddGameFolderDialogFragment.newInstance(uriString)
+ .show(supportFragmentManager, AddGameFolderDialogFragment.TAG)
}
val getProdKey =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index 8c3268e9c..bbe7bfa92 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -364,6 +364,27 @@ object FileUtil {
.lowercase()
}
+ fun isTreeUriValid(uri: Uri): Boolean {
+ val resolver = context.contentResolver
+ val columns = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE
+ )
+ return try {
+ val docId: String = if (isRootTreeUri(uri)) {
+ DocumentsContract.getTreeDocumentId(uri)
+ } else {
+ DocumentsContract.getDocumentId(uri)
+ }
+ val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, docId)
+ resolver.query(childrenUri, columns, null, null, null)
+ true
+ } catch (_: Exception) {
+ false
+ }
+ }
+
@Throws(IOException::class)
fun getStringFromFile(file: File): String =
String(file.readBytes(), StandardCharsets.UTF_8)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index e6aca6b44..55010dc59 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -11,10 +11,11 @@ import kotlinx.serialization.json.Json
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.MinimalDocumentFile
object GameHelper {
- const val KEY_GAME_PATH = "game_path"
+ private const val KEY_OLD_GAME_PATH = "game_path"
const val KEY_GAMES = "Games"
private lateinit var preferences: SharedPreferences
@@ -22,15 +23,43 @@ object GameHelper {
fun getGames(): List<Game> {
val games = mutableListOf<Game>()
val context = YuzuApplication.appContext
- val gamesDir =
- PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_GAME_PATH, "")
- val gamesUri = Uri.parse(gamesDir)
preferences = PreferenceManager.getDefaultSharedPreferences(context)
+ val gameDirs = mutableListOf<GameDir>()
+ val oldGamesDir = preferences.getString(KEY_OLD_GAME_PATH, "") ?: ""
+ if (oldGamesDir.isNotEmpty()) {
+ gameDirs.add(GameDir(oldGamesDir, true))
+ preferences.edit().remove(KEY_OLD_GAME_PATH).apply()
+ }
+ gameDirs.addAll(NativeConfig.getGameDirs())
+
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
- addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
+ val badDirs = mutableListOf<Int>()
+ gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
+ val gameDirUri = Uri.parse(gameDir.uriString)
+ val isValid = FileUtil.isTreeUriValid(gameDirUri)
+ if (isValid) {
+ addGamesRecursive(
+ games,
+ FileUtil.listFiles(gameDirUri),
+ if (gameDir.deepScan) 3 else 1
+ )
+ } else {
+ badDirs.add(index)
+ }
+ }
+
+ // Remove all game dirs with insufficient permissions from config
+ if (badDirs.isNotEmpty()) {
+ var offset = 0
+ badDirs.forEach {
+ gameDirs.removeAt(it - offset)
+ offset++
+ }
+ }
+ NativeConfig.setGameDirs(gameDirs.toTypedArray())
// Cache list of games found on disk
val serializedGames = mutableSetOf<String>()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
index 47bde5081..e63382e1d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/InputHandler.kt
@@ -27,6 +27,8 @@ object InputHandler {
0x054C -> getInputDS5ButtonKey(event.keyCode)
0x057E -> getInputJoyconButtonKey(event.keyCode)
0x1532 -> getInputRazerButtonKey(event.keyCode)
+ 0x3537 -> getInputRedmagicButtonKey(event.keyCode)
+ 0x358A -> getInputBackboneLabsButtonKey(event.keyCode)
else -> getInputGenericButtonKey(event.keyCode)
}
@@ -227,6 +229,42 @@ object InputHandler {
}
}
+ private fun getInputRedmagicButtonKey(key: Int): Int {
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
+ KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
+ private fun getInputBackboneLabsButtonKey(key: Int): Int {
+ return when (key) {
+ KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_B
+ KeyEvent.KEYCODE_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_A
+ KeyEvent.KEYCODE_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_Y
+ KeyEvent.KEYCODE_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_X
+ KeyEvent.KEYCODE_BUTTON_L1 -> NativeLibrary.ButtonType.TRIGGER_L
+ KeyEvent.KEYCODE_BUTTON_R1 -> NativeLibrary.ButtonType.TRIGGER_R
+ KeyEvent.KEYCODE_BUTTON_L2 -> NativeLibrary.ButtonType.TRIGGER_ZL
+ KeyEvent.KEYCODE_BUTTON_R2 -> NativeLibrary.ButtonType.TRIGGER_ZR
+ KeyEvent.KEYCODE_BUTTON_THUMBL -> NativeLibrary.ButtonType.STICK_L
+ KeyEvent.KEYCODE_BUTTON_THUMBR -> NativeLibrary.ButtonType.STICK_R
+ KeyEvent.KEYCODE_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_PLUS
+ KeyEvent.KEYCODE_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_MINUS
+ else -> -1
+ }
+ }
+
private fun getInputGenericButtonKey(key: Int): Int {
return when (key) {
KeyEvent.KEYCODE_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index 87e579fa7..f4e1bb13f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -3,6 +3,8 @@
package org.yuzu.yuzu_emu.utils
+import org.yuzu.yuzu_emu.model.GameDir
+
object NativeConfig {
/**
* Creates a Config object and opens the emulation config.
@@ -54,4 +56,22 @@ object NativeConfig {
external fun getConfigHeader(category: Int): String
external fun getPairedSettingKey(key: String): String
+
+ /**
+ * Gets every [GameDir] in AndroidSettings::values.game_dirs
+ */
+ @Synchronized
+ external fun getGameDirs(): Array<GameDir>
+
+ /**
+ * Clears the AndroidSettings::values.game_dirs array and replaces them with the provided array
+ */
+ @Synchronized
+ external fun setGameDirs(dirs: Array<GameDir>)
+
+ /**
+ * Adds a single [GameDir] to the AndroidSettings::values.game_dirs array
+ */
+ @Synchronized
+ external fun addGameDir(dir: GameDir)
}
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index 3041c25c9..767d8ea83 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -34,6 +34,7 @@ void AndroidConfig::SaveAllValues() {
void AndroidConfig::ReadAndroidValues() {
if (global) {
ReadAndroidUIValues();
+ ReadUIValues();
}
}
@@ -45,9 +46,35 @@ void AndroidConfig::ReadAndroidUIValues() {
EndGroup();
}
+void AndroidConfig::ReadUIValues() {
+ BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
+
+ ReadPathValues();
+
+ EndGroup();
+}
+
+void AndroidConfig::ReadPathValues() {
+ BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
+
+ const int gamedirs_size = BeginArray(std::string("gamedirs"));
+ for (int i = 0; i < gamedirs_size; ++i) {
+ SetArrayIndex(i);
+ AndroidSettings::GameDir game_dir;
+ game_dir.path = ReadStringSetting(std::string("path"));
+ game_dir.deep_scan =
+ ReadBooleanSetting(std::string("deep_scan"), std::make_optional(false));
+ AndroidSettings::values.game_dirs.push_back(game_dir);
+ }
+ EndArray();
+
+ EndGroup();
+}
+
void AndroidConfig::SaveAndroidValues() {
if (global) {
SaveAndroidUIValues();
+ SaveUIValues();
}
WriteToIni();
@@ -61,6 +88,29 @@ void AndroidConfig::SaveAndroidUIValues() {
EndGroup();
}
+void AndroidConfig::SaveUIValues() {
+ BeginGroup(Settings::TranslateCategory(Settings::Category::Ui));
+
+ SavePathValues();
+
+ EndGroup();
+}
+
+void AndroidConfig::SavePathValues() {
+ BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
+
+ BeginArray(std::string("gamedirs"));
+ for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
+ SetArrayIndex(i);
+ const auto& game_dir = AndroidSettings::values.game_dirs[i];
+ WriteSetting(std::string("path"), game_dir.path);
+ WriteSetting(std::string("deep_scan"), game_dir.deep_scan, std::make_optional(false));
+ }
+ EndArray();
+
+ EndGroup();
+}
+
std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
auto& map = Settings::values.linkage.by_category;
if (map.contains(category)) {
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h
index e679392fd..f490be016 100644
--- a/src/android/app/src/main/jni/android_config.h
+++ b/src/android/app/src/main/jni/android_config.h
@@ -19,9 +19,9 @@ protected:
void ReadAndroidUIValues();
void ReadHidbusValues() override {}
void ReadDebugControlValues() override {}
- void ReadPathValues() override {}
+ void ReadPathValues() override;
void ReadShortcutValues() override {}
- void ReadUIValues() override {}
+ void ReadUIValues() override;
void ReadUIGamelistValues() override {}
void ReadUILayoutValues() override {}
void ReadMultiplayerValues() override {}
@@ -30,9 +30,9 @@ protected:
void SaveAndroidUIValues();
void SaveHidbusValues() override {}
void SaveDebugControlValues() override {}
- void SavePathValues() override {}
+ void SavePathValues() override;
void SaveShortcutValues() override {}
- void SaveUIValues() override {}
+ void SaveUIValues() override;
void SaveUIGamelistValues() override {}
void SaveUILayoutValues() override {}
void SaveMultiplayerValues() override {}
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h
index 37bc33918..fc0523206 100644
--- a/src/android/app/src/main/jni/android_settings.h
+++ b/src/android/app/src/main/jni/android_settings.h
@@ -9,9 +9,17 @@
namespace AndroidSettings {
+struct GameDir {
+ std::string path;
+ bool deep_scan = false;
+};
+
struct Values {
Settings::Linkage linkage;
+ // Path settings
+ std::vector<GameDir> game_dirs;
+
// Android
Settings::Setting<bool> picture_in_picture{linkage, false, "picture_in_picture",
Settings::Category::Android};
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index 960abf95a..a56ed5662 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -13,6 +13,8 @@ static JavaVM* s_java_vm;
static jclass s_native_library_class;
static jclass s_disk_cache_progress_class;
static jclass s_load_callback_stage_class;
+static jclass s_game_dir_class;
+static jmethodID s_game_dir_constructor;
static jmethodID s_exit_emulation_activity;
static jmethodID s_disk_cache_load_progress;
static jmethodID s_on_emulation_started;
@@ -53,6 +55,14 @@ jclass GetDiskCacheLoadCallbackStageClass() {
return s_load_callback_stage_class;
}
+jclass GetGameDirClass() {
+ return s_game_dir_class;
+}
+
+jmethodID GetGameDirConstructor() {
+ return s_game_dir_constructor;
+}
+
jmethodID GetExitEmulationActivity() {
return s_exit_emulation_activity;
}
@@ -90,6 +100,11 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
s_load_callback_stage_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass(
"org/yuzu/yuzu_emu/disk_shader_cache/DiskShaderCacheProgress$LoadCallbackStage")));
+ const jclass game_dir_class = env->FindClass("org/yuzu/yuzu_emu/model/GameDir");
+ s_game_dir_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_dir_class));
+ s_game_dir_constructor = env->GetMethodID(game_dir_class, "<init>", "(Ljava/lang/String;Z)V");
+ env->DeleteLocalRef(game_dir_class);
+
// Initialize methods
s_exit_emulation_activity =
env->GetStaticMethodID(s_native_library_class, "exitEmulationActivity", "(I)V");
@@ -120,6 +135,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
env->DeleteGlobalRef(s_native_library_class);
env->DeleteGlobalRef(s_disk_cache_progress_class);
env->DeleteGlobalRef(s_load_callback_stage_class);
+ env->DeleteGlobalRef(s_game_dir_class);
// UnInitialize applets
SoftwareKeyboard::CleanupJNI(env);
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index b76158928..855649efa 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -13,6 +13,8 @@ JNIEnv* GetEnvForThread();
jclass GetNativeLibraryClass();
jclass GetDiskCacheProgressClass();
jclass GetDiskCacheLoadCallbackStageClass();
+jclass GetGameDirClass();
+jmethodID GetGameDirConstructor();
jmethodID GetExitEmulationActivity();
jmethodID GetDiskCacheLoadProgress();
jmethodID GetOnEmulationStarted();
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 8e81816e5..763b2164c 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -11,6 +11,7 @@
#include "common/settings.h"
#include "frontend_common/config.h"
#include "jni/android_common/android_common.h"
+#include "jni/id_cache.h"
std::unique_ptr<AndroidConfig> config;
@@ -253,4 +254,55 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
return ToJString(env, setting->PairedSetting()->GetLabel());
}
+jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
+ jclass gameDirClass = IDCache::GetGameDirClass();
+ jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
+ jobjectArray jgameDirArray =
+ env->NewObjectArray(AndroidSettings::values.game_dirs.size(), gameDirClass, nullptr);
+ for (size_t i = 0; i < AndroidSettings::values.game_dirs.size(); ++i) {
+ jobject jgameDir =
+ env->NewObject(gameDirClass, gameDirConstructor,
+ ToJString(env, AndroidSettings::values.game_dirs[i].path),
+ static_cast<jboolean>(AndroidSettings::values.game_dirs[i].deep_scan));
+ env->SetObjectArrayElement(jgameDirArray, i, jgameDir);
+ }
+ return jgameDirArray;
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGameDirs(JNIEnv* env, jobject obj,
+ jobjectArray gameDirs) {
+ AndroidSettings::values.game_dirs.clear();
+ int size = env->GetArrayLength(gameDirs);
+
+ if (size == 0) {
+ return;
+ }
+
+ jobject dir = env->GetObjectArrayElement(gameDirs, 0);
+ jclass gameDirClass = IDCache::GetGameDirClass();
+ jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
+ jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
+ for (int i = 0; i < size; ++i) {
+ dir = env->GetObjectArrayElement(gameDirs, i);
+ jstring juriString = static_cast<jstring>(env->GetObjectField(dir, uriStringField));
+ jboolean jdeepScanBoolean = env->GetBooleanField(dir, deepScanBooleanField);
+ std::string uriString = GetJString(env, juriString);
+ AndroidSettings::values.game_dirs.push_back(
+ AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
+ }
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject obj,
+ jobject gameDir) {
+ jclass gameDirClass = IDCache::GetGameDirClass();
+ jfieldID uriStringField = env->GetFieldID(gameDirClass, "uriString", "Ljava/lang/String;");
+ jfieldID deepScanBooleanField = env->GetFieldID(gameDirClass, "deepScan", "Z");
+
+ jstring juriString = static_cast<jstring>(env->GetObjectField(gameDir, uriStringField));
+ jboolean jdeepScanBoolean = env->GetBooleanField(gameDir, deepScanBooleanField);
+ std::string uriString = GetJString(env, juriString);
+ AndroidSettings::values.game_dirs.push_back(
+ AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
+}
+
} // extern "C"
diff --git a/src/android/app/src/main/res/layout/card_folder.xml b/src/android/app/src/main/res/layout/card_folder.xml
new file mode 100644
index 000000000..4e0c04b6b
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_folder.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ style="?attr/materialCardViewOutlinedStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="16dp"
+ android:layout_marginVertical="12dp"
+ android:focusable="true">
+
+ <androidx.constraintlayout.widget.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:padding="16dp"
+ android:layout_gravity="center_vertical"
+ android:animateLayoutChanges="true">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/path"
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:ellipsize="none"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@+id/button_layout"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="@string/select_gpu_driver_default" />
+
+ <LinearLayout
+ android:id="@+id/button_layout"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <Button
+ android:id="@+id/button_edit"
+ style="@style/Widget.Material3.Button.IconButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/delete"
+ android:tooltipText="@string/edit"
+ app:icon="@drawable/ic_edit"
+ app:iconTint="?attr/colorControlNormal" />
+
+ <Button
+ android:id="@+id/button_delete"
+ style="@style/Widget.Material3.Button.IconButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/delete"
+ android:tooltipText="@string/delete"
+ app:icon="@drawable/ic_delete"
+ app:iconTint="?attr/colorControlNormal" />
+
+ </LinearLayout>
+
+ </androidx.constraintlayout.widget.ConstraintLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/dialog_add_folder.xml b/src/android/app/src/main/res/layout/dialog_add_folder.xml
new file mode 100644
index 000000000..01f95e868
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_add_folder.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="24dp"
+ android:orientation="vertical">
+
+ <com.google.android.material.textview.MaterialTextView
+ android:id="@+id/path"
+ style="@style/TextAppearance.Material3.BodyLarge"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_gravity="center_vertical|start"
+ android:layout_weight="1"
+ android:ellipsize="marquee"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:requiresFadingEdge="horizontal"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ tools:text="folder/folder/folder/folder" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingTop="8dp">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:layout_weight="1"
+ android:text="@string/deep_scan"
+ android:textAlignment="viewStart" />
+
+ <com.google.android.material.checkbox.MaterialCheckBox
+ android:id="@+id/deep_scan_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_folder_properties.xml b/src/android/app/src/main/res/layout/dialog_folder_properties.xml
new file mode 100644
index 000000000..248d048cb
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_folder_properties.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="24dp"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/deep_scan_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <com.google.android.material.textview.MaterialTextView
+ style="@style/TextAppearance.Material3.BodyMedium"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|start"
+ android:layout_weight="1"
+ android:text="@string/deep_scan"
+ android:textAlignment="viewStart" />
+
+ <com.google.android.material.checkbox.MaterialCheckBox
+ android:id="@+id/deep_scan_switch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_folders.xml b/src/android/app/src/main/res/layout/fragment_folders.xml
new file mode 100644
index 000000000..74f2f3754
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_folders.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/coordinator_folders"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/colorSurface">
+
+ <androidx.coordinatorlayout.widget.CoordinatorLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <com.google.android.material.appbar.AppBarLayout
+ android:id="@+id/appbar_folders"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:fitsSystemWindows="true"
+ app:liftOnScrollTargetViewId="@id/list_folders">
+
+ <com.google.android.material.appbar.MaterialToolbar
+ android:id="@+id/toolbar_folders"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/actionBarSize"
+ app:navigationIcon="@drawable/ic_back"
+ app:title="@string/game_folders" />
+
+ </com.google.android.material.appbar.AppBarLayout>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/list_folders"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+ <com.google.android.material.floatingactionbutton.FloatingActionButton
+ android:id="@+id/button_add"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|end"
+ android:contentDescription="@string/add_games"
+ app:srcCompat="@drawable/ic_add"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
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 6d4c1f86d..cf70b4bc4 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -28,6 +28,9 @@
<action
android:id="@+id/action_homeSettingsFragment_to_appletLauncherFragment"
app:destination="@id/appletLauncherFragment" />
+ <action
+ android:id="@+id/action_homeSettingsFragment_to_gameFoldersFragment"
+ app:destination="@id/gameFoldersFragment" />
</fragment>
<fragment
@@ -117,5 +120,9 @@
android:id="@+id/cabinetLauncherDialogFragment"
android:name="org.yuzu.yuzu_emu.fragments.CabinetLauncherDialogFragment"
android:label="CabinetLauncherDialogFragment" />
+ <fragment
+ android:id="@+id/gameFoldersFragment"
+ android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
+ android:label="GameFoldersFragment" />
</navigation>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index ef855ea6f..380d14213 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,7 +13,7 @@
<dimen name="menu_width">256dp</dimen>
<dimen name="card_width">165dp</dimen>
<dimen name="icon_inset">24dp</dimen>
- <dimen name="spacing_bottom_list_fab">72dp</dimen>
+ <dimen name="spacing_bottom_list_fab">76dp</dimen>
<dimen name="spacing_fab">24dp</dimen>
<dimen name="dialog_margin">20dp</dimen>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 95b90fd6d..a6ccef8a1 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -38,6 +38,7 @@
<string name="empty_gamelist">No files were found or no game directory has been selected yet.</string>
<string name="search_and_filter_games">Search and filter games</string>
<string name="select_games_folder">Select games folder</string>
+ <string name="manage_game_folders">Manage game folders</string>
<string name="select_games_folder_description">Allows yuzu to populate the games list</string>
<string name="add_games_warning">Skip selecting games folder?</string>
<string name="add_games_warning_description">Games won\'t be displayed in the Games list if a folder isn\'t selected.</string>
@@ -124,6 +125,11 @@
<string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
<string name="share_save_file">Share save file</string>
<string name="export_save_failed">Failed to export save</string>
+ <string name="game_folders">Game folders</string>
+ <string name="deep_scan">Deep scan</string>
+ <string name="add_game_folder">Add game folder</string>
+ <string name="folder_already_added">This folder was already added!</string>
+ <string name="game_folder_properties">Game folder properties</string>
<!-- Applet launcher strings -->
<string name="applets">Applet launcher</string>
@@ -258,6 +264,7 @@
<string name="cancelling">Cancelling</string>
<string name="install">Install</string>
<string name="delete">Delete</string>
+ <string name="edit">Edit</string>
<string name="export_success">Exported successfully</string>
<!-- GPU driver installation -->
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index d38d5c6d1..bbc55eb56 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -182,6 +182,15 @@ if(ANDROID)
)
endif()
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ target_sources(common PRIVATE
+ linux/gamemode.cpp
+ linux/gamemode.h
+ )
+
+ target_link_libraries(common PRIVATE gamemode)
+endif()
+
if(ARCHITECTURE_x86_64)
target_sources(common
PRIVATE
diff --git a/src/common/linux/gamemode.cpp b/src/common/linux/gamemode.cpp
new file mode 100644
index 000000000..8d3e2934a
--- /dev/null
+++ b/src/common/linux/gamemode.cpp
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <gamemode_client.h>
+
+#include "common/linux/gamemode.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+
+namespace Common::Linux {
+
+void StartGamemode() {
+ if (Settings::values.enable_gamemode) {
+ if (gamemode_request_start() < 0) {
+ LOG_WARNING(Frontend, "Failed to start gamemode: {}", gamemode_error_string());
+ } else {
+ LOG_INFO(Frontend, "Started gamemode");
+ }
+ }
+}
+
+void StopGamemode() {
+ if (Settings::values.enable_gamemode) {
+ if (gamemode_request_end() < 0) {
+ LOG_WARNING(Frontend, "Failed to stop gamemode: {}", gamemode_error_string());
+ } else {
+ LOG_INFO(Frontend, "Stopped gamemode");
+ }
+ }
+}
+
+void SetGamemodeState(bool state) {
+ if (state) {
+ StartGamemode();
+ } else {
+ StopGamemode();
+ }
+}
+
+} // namespace Common::Linux
diff --git a/src/common/linux/gamemode.h b/src/common/linux/gamemode.h
new file mode 100644
index 000000000..b80646ae2
--- /dev/null
+++ b/src/common/linux/gamemode.h
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+namespace Common::Linux {
+
+/**
+ * Start the (Feral Interactive) Linux gamemode if it is installed and it is activated
+ */
+void StartGamemode();
+
+/**
+ * Stop the (Feral Interactive) Linux gamemode if it is installed and it is activated
+ */
+void StopGamemode();
+
+/**
+ * Start or stop the (Feral Interactive) Linux gamemode if it is installed and it is activated
+ * @param state The new state the gamemode should have
+ */
+void SetGamemodeState(bool state);
+
+} // namespace Common::Linux
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 16c2feeab..4666bd0a0 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -236,6 +236,8 @@ const char* TranslateCategory(Category category) {
return "Services";
case Category::Paths:
return "Paths";
+ case Category::Linux:
+ return "Linux";
case Category::MaxEnum:
break;
}
diff --git a/src/common/settings.h b/src/common/settings.h
index 508615011..98341ad96 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -393,6 +393,8 @@ struct Values {
Category::RendererDebug};
// TODO: remove this once AMDVLK supports VK_EXT_depth_bias_control
bool renderer_amdvlk_depth_bias_workaround{};
+ Setting<bool> disable_buffer_reorder{linkage, false, "disable_buffer_reorder",
+ Category::RendererDebug};
// System
SwitchableSetting<Language, true> language_index{linkage,
@@ -436,6 +438,9 @@ struct Values {
true,
true};
+ // Linux
+ SwitchableSetting<bool> enable_gamemode{linkage, true, "enable_gamemode", Category::Linux};
+
// Controls
InputSetting<std::array<PlayerInput, 10>> players;
diff --git a/src/common/settings_common.h b/src/common/settings_common.h
index 7943223eb..344c04439 100644
--- a/src/common/settings_common.h
+++ b/src/common/settings_common.h
@@ -41,6 +41,7 @@ enum class Category : u32 {
Multiplayer,
Services,
Paths,
+ Linux,
MaxEnum,
};
diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h
index a72df034e..c7b48a58d 100644
--- a/src/core/frontend/emu_window.h
+++ b/src/core/frontend/emu_window.h
@@ -167,6 +167,11 @@ protected:
*/
std::pair<f32, f32> MapToTouchScreen(u32 framebuffer_x, u32 framebuffer_y) const;
+ /**
+ * Clip the provided coordinates to be inside the touchscreen area.
+ */
+ std::pair<u32, u32> ClipToTouchScreen(u32 new_x, u32 new_y) const;
+
WindowSystemInfo window_info;
bool strict_context_required = false;
@@ -181,11 +186,6 @@ private:
// By default, ignore this request and do nothing.
}
- /**
- * Clip the provided coordinates to be inside the touchscreen area.
- */
- std::pair<u32, u32> ClipToTouchScreen(u32 new_x, u32 new_y) const;
-
Layout::FramebufferLayout framebuffer_layout; ///< Current framebuffer layout
u32 client_area_width; ///< Current client width, should be set by window impl.
diff --git a/src/core/hid/emulated_controller.cpp b/src/core/hid/emulated_controller.cpp
index a22015ada..a6e681e15 100644
--- a/src/core/hid/emulated_controller.cpp
+++ b/src/core/hid/emulated_controller.cpp
@@ -509,9 +509,11 @@ void EmulatedController::ReloadInput() {
});
}
turbo_button_state = 0;
+ is_initalized = true;
}
void EmulatedController::UnloadInput() {
+ is_initalized = false;
for (auto& button : button_devices) {
button.reset();
}
@@ -1207,6 +1209,9 @@ void EmulatedController::SetNfc(const Common::Input::CallbackStatus& callback) {
}
bool EmulatedController::SetVibration(std::size_t device_index, VibrationValue vibration) {
+ if (!is_initalized) {
+ return false;
+ }
if (device_index >= output_devices.size()) {
return false;
}
@@ -1242,6 +1247,10 @@ bool EmulatedController::IsVibrationEnabled(std::size_t device_index) {
const auto player_index = Service::HID::NpadIdTypeToIndex(npad_id_type);
const auto& player = Settings::values.players.GetValue()[player_index];
+ if (!is_initalized) {
+ return false;
+ }
+
if (!player.vibration_enabled) {
return false;
}
@@ -1261,6 +1270,10 @@ Common::Input::DriverResult EmulatedController::SetPollingMode(
EmulatedDeviceIndex device_index, Common::Input::PollingMode polling_mode) {
LOG_INFO(Service_HID, "Set polling mode {}, device_index={}", polling_mode, device_index);
+ if (!is_initalized) {
+ return Common::Input::DriverResult::InvalidHandle;
+ }
+
auto& left_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Left)];
auto& right_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_output_device = output_devices[3];
@@ -1306,6 +1319,10 @@ bool EmulatedController::SetCameraFormat(
Core::IrSensor::ImageTransferProcessorFormat camera_format) {
LOG_INFO(Service_HID, "Set camera format {}", camera_format);
+ if (!is_initalized) {
+ return false;
+ }
+
auto& right_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& camera_output_device = output_devices[2];
@@ -1329,6 +1346,11 @@ void EmulatedController::SetRingParam(Common::ParamPackage param) {
}
bool EmulatedController::HasNfc() const {
+
+ if (!is_initalized) {
+ return false;
+ }
+
const auto& nfc_output_device = output_devices[3];
switch (npad_type) {
@@ -1366,6 +1388,10 @@ bool EmulatedController::RemoveNfcHandle() {
}
bool EmulatedController::StartNfcPolling() {
+ if (!is_initalized) {
+ return false;
+ }
+
auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_virtual_output_device = output_devices[3];
@@ -1377,6 +1403,10 @@ bool EmulatedController::StartNfcPolling() {
}
bool EmulatedController::StopNfcPolling() {
+ if (!is_initalized) {
+ return false;
+ }
+
auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_virtual_output_device = output_devices[3];
@@ -1388,6 +1418,10 @@ bool EmulatedController::StopNfcPolling() {
}
bool EmulatedController::ReadAmiiboData(std::vector<u8>& data) {
+ if (!is_initalized) {
+ return false;
+ }
+
auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_virtual_output_device = output_devices[3];
@@ -1400,6 +1434,10 @@ bool EmulatedController::ReadAmiiboData(std::vector<u8>& data) {
bool EmulatedController::ReadMifareData(const Common::Input::MifareRequest& request,
Common::Input::MifareRequest& out_data) {
+ if (!is_initalized) {
+ return false;
+ }
+
auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_virtual_output_device = output_devices[3];
@@ -1412,6 +1450,10 @@ bool EmulatedController::ReadMifareData(const Common::Input::MifareRequest& requ
}
bool EmulatedController::WriteMifareData(const Common::Input::MifareRequest& request) {
+ if (!is_initalized) {
+ return false;
+ }
+
auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_virtual_output_device = output_devices[3];
@@ -1423,6 +1465,10 @@ bool EmulatedController::WriteMifareData(const Common::Input::MifareRequest& req
}
bool EmulatedController::WriteNfc(const std::vector<u8>& data) {
+ if (!is_initalized) {
+ return false;
+ }
+
auto& nfc_output_device = output_devices[static_cast<std::size_t>(DeviceIndex::Right)];
auto& nfc_virtual_output_device = output_devices[3];
@@ -1434,6 +1480,10 @@ bool EmulatedController::WriteNfc(const std::vector<u8>& data) {
}
void EmulatedController::SetLedPattern() {
+ if (!is_initalized) {
+ return;
+ }
+
for (auto& device : output_devices) {
if (!device) {
continue;
diff --git a/src/core/hid/emulated_controller.h b/src/core/hid/emulated_controller.h
index ea18c2343..d6e20ab66 100644
--- a/src/core/hid/emulated_controller.h
+++ b/src/core/hid/emulated_controller.h
@@ -559,6 +559,7 @@ private:
NpadStyleTag supported_style_tag{NpadStyleSet::All};
bool is_connected{false};
bool is_configuring{false};
+ bool is_initalized{false};
bool system_buttons_enabled{true};
f32 motion_sensitivity{Core::HID::MotionInput::IsAtRestStandard};
u32 turbo_button_state{0};
diff --git a/src/core/hle/service/am/applets/applet_cabinet.cpp b/src/core/hle/service/am/applets/applet_cabinet.cpp
index b379dadeb..3906c0fa4 100644
--- a/src/core/hle/service/am/applets/applet_cabinet.cpp
+++ b/src/core/hle/service/am/applets/applet_cabinet.cpp
@@ -122,7 +122,8 @@ void Cabinet::DisplayCompleted(bool apply_changes, std::string_view amiibo_name)
Service::NFP::RegisterInfoPrivate register_info{};
std::memcpy(register_info.amiibo_name.data(), amiibo_name.data(),
std::min(amiibo_name.size(), register_info.amiibo_name.size() - 1));
-
+ register_info.mii_store_data.BuildRandom(Mii::Age::All, Mii::Gender::All, Mii::Race::All);
+ register_info.mii_store_data.SetNickname({u'y', u'u', u'z', u'u'});
nfp_device->SetRegisterInfoPrivate(register_info);
break;
}
@@ -130,7 +131,7 @@ void Cabinet::DisplayCompleted(bool apply_changes, std::string_view amiibo_name)
nfp_device->DeleteApplicationArea();
break;
case Service::NFP::CabinetMode::StartRestorer:
- nfp_device->RestoreAmiibo();
+ nfp_device->Restore();
break;
case Service::NFP::CabinetMode::StartFormatter:
nfp_device->Format();
diff --git a/src/core/hle/service/hid/controllers/touchscreen.cpp b/src/core/hle/service/hid/controllers/touchscreen.cpp
index 3bcf0ee9f..fcd973414 100644
--- a/src/core/hle/service/hid/controllers/touchscreen.cpp
+++ b/src/core/hle/service/hid/controllers/touchscreen.cpp
@@ -16,7 +16,8 @@ namespace Service::HID {
constexpr std::size_t SHARED_MEMORY_OFFSET = 0x400;
TouchScreen::TouchScreen(Core::HID::HIDCore& hid_core_, u8* raw_shared_memory_)
- : ControllerBase{hid_core_} {
+ : ControllerBase{hid_core_}, touchscreen_width(Layout::ScreenUndocked::Width),
+ touchscreen_height(Layout::ScreenUndocked::Height) {
static_assert(SHARED_MEMORY_OFFSET + sizeof(TouchSharedMemory) < shared_memory_size,
"TouchSharedMemory is bigger than the shared memory");
shared_memory = std::construct_at(
@@ -95,8 +96,8 @@ void TouchScreen::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
if (id < active_fingers_count) {
const auto& [active_x, active_y] = active_fingers[id].position;
touch_entry.position = {
- .x = static_cast<u16>(active_x * Layout::ScreenUndocked::Width),
- .y = static_cast<u16>(active_y * Layout::ScreenUndocked::Height),
+ .x = static_cast<u16>(active_x * static_cast<float>(touchscreen_width)),
+ .y = static_cast<u16>(active_y * static_cast<float>(touchscreen_height)),
};
touch_entry.diameter_x = Settings::values.touchscreen.diameter_x;
touch_entry.diameter_y = Settings::values.touchscreen.diameter_y;
@@ -120,4 +121,9 @@ void TouchScreen::OnUpdate(const Core::Timing::CoreTiming& core_timing) {
shared_memory->touch_screen_lifo.WriteNextEntry(next_state);
}
+void TouchScreen::SetTouchscreenDimensions(u32 width, u32 height) {
+ touchscreen_width = width;
+ touchscreen_height = height;
+}
+
} // namespace Service::HID
diff --git a/src/core/hle/service/hid/controllers/touchscreen.h b/src/core/hle/service/hid/controllers/touchscreen.h
index cd342ce91..79f026a81 100644
--- a/src/core/hle/service/hid/controllers/touchscreen.h
+++ b/src/core/hle/service/hid/controllers/touchscreen.h
@@ -28,6 +28,8 @@ public:
// When the controller is requesting an update for the shared memory
void OnUpdate(const Core::Timing::CoreTiming& core_timing) override;
+ void SetTouchscreenDimensions(u32 width, u32 height);
+
private:
static constexpr std::size_t MAX_FINGERS = 16;
@@ -53,5 +55,7 @@ private:
Core::HID::EmulatedConsole* console = nullptr;
std::array<Core::HID::TouchFinger, MAX_FINGERS> fingers{};
+ u32 touchscreen_width;
+ u32 touchscreen_height;
};
} // namespace Service::HID
diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp
index 583142e35..a7d1578d9 100644
--- a/src/core/hle/service/hid/hid_server.cpp
+++ b/src/core/hle/service/hid/hid_server.cpp
@@ -208,6 +208,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{1001, &IHidServer::GetNpadCommunicationMode, "GetNpadCommunicationMode"},
{1002, &IHidServer::SetTouchScreenConfiguration, "SetTouchScreenConfiguration"},
{1003, &IHidServer::IsFirmwareUpdateNeededForNotification, "IsFirmwareUpdateNeededForNotification"},
+ {1004, &IHidServer::SetTouchScreenResolution, "SetTouchScreenResolution"},
{2000, nullptr, "ActivateDigitizer"},
};
// clang-format on
@@ -2363,6 +2364,21 @@ void IHidServer::IsFirmwareUpdateNeededForNotification(HLERequestContext& ctx) {
rb.Push(false);
}
+void IHidServer::SetTouchScreenResolution(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto width{rp.Pop<u32>()};
+ const auto height{rp.Pop<u32>()};
+ const auto applet_resource_user_id{rp.Pop<u64>()};
+
+ GetResourceManager()->GetTouchScreen()->SetTouchscreenDimensions(width, height);
+
+ LOG_INFO(Service_HID, "called, width={}, height={}, applet_resource_user_id={}", width, height,
+ applet_resource_user_id);
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+}
+
std::shared_ptr<ResourceManager> IHidServer::GetResourceManager() {
resource_manager->Initialize();
return resource_manager;
diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h
index eb2e8e7f4..cc7c4ebdd 100644
--- a/src/core/hle/service/hid/hid_server.h
+++ b/src/core/hle/service/hid/hid_server.h
@@ -141,6 +141,7 @@ private:
void GetNpadCommunicationMode(HLERequestContext& ctx);
void SetTouchScreenConfiguration(HLERequestContext& ctx);
void IsFirmwareUpdateNeededForNotification(HLERequestContext& ctx);
+ void SetTouchScreenResolution(HLERequestContext& ctx);
std::shared_ptr<ResourceManager> resource_manager;
std::shared_ptr<HidFirmwareSettings> firmware_settings;
diff --git a/src/core/hle/service/mii/types/ver3_store_data.cpp b/src/core/hle/service/mii/types/ver3_store_data.cpp
index a019cc9f7..c27646fcf 100644
--- a/src/core/hle/service/mii/types/ver3_store_data.cpp
+++ b/src/core/hle/service/mii/types/ver3_store_data.cpp
@@ -98,7 +98,7 @@ void Ver3StoreData::BuildToStoreData(StoreData& out_store_data) const {
}
void Ver3StoreData::BuildFromStoreData(const StoreData& store_data) {
- version = 1;
+ version = 3;
mii_information.gender.Assign(static_cast<u8>(store_data.GetGender()));
mii_information.favorite_color.Assign(static_cast<u8>(store_data.GetFavoriteColor()));
height = store_data.GetHeight();
diff --git a/src/core/hle/service/nfc/common/device.cpp b/src/core/hle/service/nfc/common/device.cpp
index e7a00deb3..47516f883 100644
--- a/src/core/hle/service/nfc/common/device.cpp
+++ b/src/core/hle/service/nfc/common/device.cpp
@@ -401,6 +401,12 @@ Result NfcDevice::SendCommandByPassThrough(const Time::Clock::TimeSpanType& time
}
Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target_) {
+ bool is_corrupted = false;
+
+ if (model_type != NFP::ModelType::Amiibo) {
+ return ResultInvalidArgument;
+ }
+
if (device_state != DeviceState::TagFound) {
LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
return ResultWrongDeviceState;
@@ -420,26 +426,32 @@ Result NfcDevice::Mount(NFP::ModelType model_type, NFP::MountTarget mount_target
if (is_plain_amiibo) {
std::vector<u8> data(sizeof(NFP::NTAG215File));
memcpy(data.data(), &tag_data, sizeof(tag_data));
- WriteBackupData(tag_data.uid, data);
+ }
- device_state = DeviceState::TagMounted;
- mount_target = mount_target_;
- return ResultSuccess;
+ if (!is_plain_amiibo && !NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) {
+ LOG_ERROR(Service_NFP, "Can't decode amiibo");
+ is_corrupted = true;
}
- if (!NFP::AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data)) {
- bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess();
- LOG_ERROR(Service_NFP, "Can't decode amiibo, has_backup= {}", has_backup);
- return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData;
+ if (tag_data.settings.settings.amiibo_initialized && !tag_data.owner_mii.IsValid()) {
+ LOG_ERROR(Service_NFP, "Invalid mii data");
+ is_corrupted = true;
}
- std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File));
- memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data));
- WriteBackupData(encrypted_tag_data.uuid, data);
+ if (!is_corrupted) {
+ std::vector<u8> data(sizeof(NFP::EncryptedNTAG215File));
+ memcpy(data.data(), &encrypted_tag_data, sizeof(encrypted_tag_data));
+ WriteBackupData(encrypted_tag_data.uuid, data);
+ }
device_state = DeviceState::TagMounted;
mount_target = mount_target_;
+ if (is_corrupted) {
+ bool has_backup = HasBackup(encrypted_tag_data.uuid).IsSuccess();
+ return has_backup ? ResultCorruptedDataWithBackup : ResultCorruptedData;
+ }
+
return ResultSuccess;
}
@@ -606,6 +618,17 @@ Result NfcDevice::Restore() {
}
}
+ // Restore mii data in case is corrupted by previous instances of yuzu
+ if (tag_data.settings.settings.amiibo_initialized && !tag_data.owner_mii.IsValid()) {
+ LOG_ERROR(Service_NFP, "Regenerating mii data");
+ Mii::StoreData new_mii{};
+ new_mii.BuildRandom(Mii::Age::All, Mii::Gender::All, Mii::Race::All);
+ new_mii.SetNickname({u'y', u'u', u'z', u'u', u'\0'});
+
+ tag_data.owner_mii.BuildFromStoreData(new_mii);
+ tag_data.mii_extension.SetFromStoreData(new_mii);
+ }
+
// Overwrite tag contents with backup and mount the tag
tag_data = temporary_tag_data;
encrypted_tag_data = temporary_encrypted_tag_data;
@@ -851,25 +874,6 @@ Result NfcDevice::SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& registe
return Flush();
}
-Result NfcDevice::RestoreAmiibo() {
- if (device_state != DeviceState::TagMounted) {
- LOG_ERROR(Service_NFP, "Wrong device state {}", device_state);
- if (device_state == DeviceState::TagRemoved) {
- return ResultTagRemoved;
- }
- return ResultWrongDeviceState;
- }
-
- if (mount_target == NFP::MountTarget::None || mount_target == NFP::MountTarget::Rom) {
- LOG_ERROR(Service_NFP, "Amiibo is read only", device_state);
- return ResultWrongDeviceState;
- }
-
- // TODO: Load amiibo from backup on system
- LOG_ERROR(Service_NFP, "Not Implemented");
- return ResultSuccess;
-}
-
Result NfcDevice::Format() {
Result result = ResultSuccess;
@@ -877,7 +881,9 @@ Result NfcDevice::Format() {
result = Mount(NFP::ModelType::Amiibo, NFP::MountTarget::All);
}
- if (result.IsError()) {
+ // We are formatting all data. Corruption is not an issue.
+ if (result.IsError() &&
+ (result != ResultCorruptedData && result != ResultCorruptedDataWithBackup)) {
return result;
}
diff --git a/src/core/hle/service/nfc/common/device.h b/src/core/hle/service/nfc/common/device.h
index 0ed1ff34c..d8efe25ec 100644
--- a/src/core/hle/service/nfc/common/device.h
+++ b/src/core/hle/service/nfc/common/device.h
@@ -68,7 +68,6 @@ public:
Result DeleteRegisterInfo();
Result SetRegisterInfoPrivate(const NFP::RegisterInfoPrivate& register_info);
- Result RestoreAmiibo();
Result Format();
Result OpenApplicationArea(u32 access_id);
diff --git a/src/core/hle/service/set/set_sys.cpp b/src/core/hle/service/set/set_sys.cpp
index 19c667b42..48304e6d1 100644
--- a/src/core/hle/service/set/set_sys.cpp
+++ b/src/core/hle/service/set/set_sys.cpp
@@ -19,19 +19,8 @@
namespace Service::Set {
-namespace {
-constexpr u64 SYSTEM_VERSION_FILE_MINOR_REVISION_OFFSET = 0x05;
-
-enum class GetFirmwareVersionType {
- Version1,
- Version2,
-};
-
-void GetFirmwareVersionImpl(Core::System& system, HLERequestContext& ctx,
- GetFirmwareVersionType type) {
- ASSERT_MSG(ctx.GetWriteBufferSize() == 0x100,
- "FirmwareVersion output buffer must be 0x100 bytes in size!");
-
+Result GetFirmwareVersionImpl(FirmwareVersionFormat& out_firmware, Core::System& system,
+ GetFirmwareVersionType type) {
constexpr u64 FirmwareVersionSystemDataId = 0x0100000000000809;
auto& fsc = system.GetFileSystemController();
@@ -45,46 +34,43 @@ void GetFirmwareVersionImpl(Core::System& system, HLERequestContext& ctx,
nca = bis_system->GetEntry(FirmwareVersionSystemDataId, FileSys::ContentRecordType::Data);
}
if (nca) {
- romfs = FileSys::ExtractRomFS(nca->GetRomFS());
+ if (auto nca_romfs = nca->GetRomFS(); nca_romfs) {
+ romfs = FileSys::ExtractRomFS(nca_romfs);
+ }
}
if (!romfs) {
romfs = FileSys::ExtractRomFS(
FileSys::SystemArchive::SynthesizeSystemArchive(FirmwareVersionSystemDataId));
}
- const auto early_exit_failure = [&ctx](std::string_view desc, Result code) {
+ const auto early_exit_failure = [](std::string_view desc, Result code) {
LOG_ERROR(Service_SET, "General failure while attempting to resolve firmware version ({}).",
desc);
- IPC::ResponseBuilder rb{ctx, 2};
- rb.Push(code);
+ return code;
};
const auto ver_file = romfs->GetFile("file");
if (ver_file == nullptr) {
- early_exit_failure("The system version archive didn't contain the file 'file'.",
- FileSys::ERROR_INVALID_ARGUMENT);
- return;
+ return early_exit_failure("The system version archive didn't contain the file 'file'.",
+ FileSys::ERROR_INVALID_ARGUMENT);
}
auto data = ver_file->ReadAllBytes();
- if (data.size() != 0x100) {
- early_exit_failure("The system version file 'file' was not the correct size.",
- FileSys::ERROR_OUT_OF_BOUNDS);
- return;
+ if (data.size() != sizeof(FirmwareVersionFormat)) {
+ return early_exit_failure("The system version file 'file' was not the correct size.",
+ FileSys::ERROR_OUT_OF_BOUNDS);
}
+ std::memcpy(&out_firmware, data.data(), sizeof(FirmwareVersionFormat));
+
// If the command is GetFirmwareVersion (as opposed to GetFirmwareVersion2), hardware will
// zero out the REVISION_MINOR field.
if (type == GetFirmwareVersionType::Version1) {
- data[SYSTEM_VERSION_FILE_MINOR_REVISION_OFFSET] = 0;
+ out_firmware.revision_minor = 0;
}
- ctx.WriteBuffer(data);
-
- IPC::ResponseBuilder rb{ctx, 2};
- rb.Push(ResultSuccess);
+ return ResultSuccess;
}
-} // Anonymous namespace
void SET_SYS::SetLanguageCode(HLERequestContext& ctx) {
IPC::RequestParser rp{ctx};
@@ -98,12 +84,32 @@ void SET_SYS::SetLanguageCode(HLERequestContext& ctx) {
void SET_SYS::GetFirmwareVersion(HLERequestContext& ctx) {
LOG_DEBUG(Service_SET, "called");
- GetFirmwareVersionImpl(system, ctx, GetFirmwareVersionType::Version1);
+
+ FirmwareVersionFormat firmware_data{};
+ const auto result =
+ GetFirmwareVersionImpl(firmware_data, system, GetFirmwareVersionType::Version1);
+
+ if (result.IsSuccess()) {
+ ctx.WriteBuffer(firmware_data);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
}
void SET_SYS::GetFirmwareVersion2(HLERequestContext& ctx) {
LOG_DEBUG(Service_SET, "called");
- GetFirmwareVersionImpl(system, ctx, GetFirmwareVersionType::Version2);
+
+ FirmwareVersionFormat firmware_data{};
+ const auto result =
+ GetFirmwareVersionImpl(firmware_data, system, GetFirmwareVersionType::Version2);
+
+ if (result.IsSuccess()) {
+ ctx.WriteBuffer(firmware_data);
+ }
+
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
}
void SET_SYS::GetAccountSettings(HLERequestContext& ctx) {
diff --git a/src/core/hle/service/set/set_sys.h b/src/core/hle/service/set/set_sys.h
index 93023c6dd..5f770fd32 100644
--- a/src/core/hle/service/set/set_sys.h
+++ b/src/core/hle/service/set/set_sys.h
@@ -4,6 +4,7 @@
#pragma once
#include "common/uuid.h"
+#include "core/hle/result.h"
#include "core/hle/service/service.h"
#include "core/hle/service/time/clock_types.h"
@@ -12,6 +13,29 @@ class System;
}
namespace Service::Set {
+enum class LanguageCode : u64;
+enum class GetFirmwareVersionType {
+ Version1,
+ Version2,
+};
+
+struct FirmwareVersionFormat {
+ u8 major;
+ u8 minor;
+ u8 micro;
+ INSERT_PADDING_BYTES(1);
+ u8 revision_major;
+ u8 revision_minor;
+ INSERT_PADDING_BYTES(2);
+ std::array<char, 0x20> platform;
+ std::array<u8, 0x40> version_hash;
+ std::array<char, 0x18> display_version;
+ std::array<char, 0x80> display_title;
+};
+static_assert(sizeof(FirmwareVersionFormat) == 0x100, "FirmwareVersionFormat is an invalid size");
+
+Result GetFirmwareVersionImpl(FirmwareVersionFormat& out_firmware, Core::System& system,
+ GetFirmwareVersionType type);
class SET_SYS final : public ServiceFramework<SET_SYS> {
public:
diff --git a/src/core/hle/service/time/clock_types.h b/src/core/hle/service/time/clock_types.h
index 9fc01ea90..7149fffeb 100644
--- a/src/core/hle/service/time/clock_types.h
+++ b/src/core/hle/service/time/clock_types.h
@@ -11,6 +11,11 @@
#include "core/hle/service/time/errors.h"
#include "core/hle/service/time/time_zone_types.h"
+// Defined by WinBase.h on Windows
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
namespace Service::Time::Clock {
enum class TimeType : u8 {
diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt
index 1537271fc..22e9337c4 100644
--- a/src/frontend_common/CMakeLists.txt
+++ b/src/frontend_common/CMakeLists.txt
@@ -7,4 +7,4 @@ add_library(frontend_common STATIC
)
create_target_directory_groups(frontend_common)
-target_link_libraries(frontend_common PUBLIC core SimpleIni PRIVATE common Boost::headers)
+target_link_libraries(frontend_common PUBLIC core SimpleIni::SimpleIni PRIVATE common Boost::headers)
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp
index 7474cb0f9..1a0491c2c 100644
--- a/src/frontend_common/config.cpp
+++ b/src/frontend_common/config.cpp
@@ -924,12 +924,14 @@ std::string Config::AdjustOutputString(const std::string& string) {
// Windows requires that two forward slashes are used at the start of a path for unmapped
// network drives so we have to watch for that here
+#ifndef ANDROID
if (string.substr(0, 2) == "//") {
boost::replace_all(adjusted_string, "//", "/");
adjusted_string.insert(0, "/");
} else {
boost::replace_all(adjusted_string, "//", "/");
}
+#endif
// Needed for backwards compatibility with QSettings deserialization
for (const auto& special_character : special_characters) {
diff --git a/src/frontend_common/config.h b/src/frontend_common/config.h
index 20a1a8056..b3812af17 100644
--- a/src/frontend_common/config.h
+++ b/src/frontend_common/config.h
@@ -7,6 +7,7 @@
#include <string>
#include "common/settings.h"
+#define SI_NO_CONVERSION
#include <SimpleIni.h>
#include <boost/algorithm/string/replace.hpp>
diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_memory.cpp b/src/shader_recompiler/backend/glasm/emit_glasm_memory.cpp
index 2705ab140..9319ea007 100644
--- a/src/shader_recompiler/backend/glasm/emit_glasm_memory.cpp
+++ b/src/shader_recompiler/backend/glasm/emit_glasm_memory.cpp
@@ -5,6 +5,7 @@
#include "shader_recompiler/backend/glasm/glasm_emit_context.h"
#include "shader_recompiler/frontend/ir/program.h"
#include "shader_recompiler/frontend/ir/value.h"
+#include "shader_recompiler/profile.h"
#include "shader_recompiler/runtime_info.h"
namespace Shader::Backend::GLASM {
@@ -35,7 +36,9 @@ void GlobalStorageOp(EmitContext& ctx, Register address, bool pointer_based, std
continue;
}
const auto& ssbo{ctx.info.storage_buffers_descriptors[index]};
- ctx.Add("LDC.U64 DC.x,c{}[{}];" // ssbo_addr
+ const u64 ssbo_align_mask{~(ctx.profile.min_ssbo_alignment - 1U)};
+ ctx.Add("LDC.U64 DC.x,c{}[{}];" // unaligned_ssbo_addr
+ "AND.U64 DC.x,DC.x,{};" // ssbo_addr = unaligned_ssbo_addr & ssbo_align_mask
"LDC.U32 RC.x,c{}[{}];" // ssbo_size_u32
"CVT.U64.U32 DC.y,RC.x;" // ssbo_size = ssbo_size_u32
"ADD.U64 DC.y,DC.y,DC.x;" // ssbo_end = ssbo_addr + ssbo_size
@@ -44,8 +47,8 @@ void GlobalStorageOp(EmitContext& ctx, Register address, bool pointer_based, std
"AND.U.CC RC.x,RC.x,RC.y;" // cond = a && b
"IF NE.x;" // if cond
"SUB.U64 DC.x,{}.x,DC.x;", // offset = input_addr - ssbo_addr
- ssbo.cbuf_index, ssbo.cbuf_offset, ssbo.cbuf_index, ssbo.cbuf_offset + 8, address,
- address, address);
+ ssbo.cbuf_index, ssbo.cbuf_offset, ssbo_align_mask, ssbo.cbuf_index,
+ ssbo.cbuf_offset + 8, address, address, address);
if (pointer_based) {
ctx.Add("PK64.U DC.y,c[{}];" // host_ssbo = cbuf
"ADD.U64 DC.x,DC.x,DC.y;" // host_addr = host_ssbo + offset
diff --git a/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp b/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp
index 9ff4028c2..fd9a99449 100644
--- a/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp
+++ b/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp
@@ -601,7 +601,10 @@ std::string EmitContext::DefineGlobalMemoryFunctions() {
addr_xy[i] = fmt::format("ftou({}[{}].{})", cbuf, addr_loc / 16, Swizzle(addr_loc));
size_xy[i] = fmt::format("ftou({}[{}].{})", cbuf, size_loc / 16, Swizzle(size_loc));
}
- const auto addr_pack{fmt::format("packUint2x32(uvec2({},{}))", addr_xy[0], addr_xy[1])};
+ const u32 ssbo_align_mask{~(static_cast<u32>(profile.min_ssbo_alignment) - 1U)};
+ const auto aligned_low_addr{fmt::format("{}&{}", addr_xy[0], ssbo_align_mask)};
+ const auto aligned_addr{fmt::format("uvec2({},{})", aligned_low_addr, addr_xy[1])};
+ const auto addr_pack{fmt::format("packUint2x32({})", aligned_addr)};
const auto addr_statment{fmt::format("uint64_t {}={};", ssbo_addr, addr_pack)};
func += addr_statment;
diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
index 57df6fc34..3350f1f85 100644
--- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
+++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
@@ -891,7 +891,9 @@ void EmitContext::DefineGlobalMemoryFunctions(const Info& info) {
const Id ssbo_size_pointer{OpAccessChain(uniform_types.U32, cbufs[ssbo.cbuf_index].U32,
zero, ssbo_size_cbuf_offset)};
- const Id ssbo_addr{OpBitcast(U64, OpLoad(U32[2], ssbo_addr_pointer))};
+ const u64 ssbo_align_mask{~(profile.min_ssbo_alignment - 1U)};
+ const Id unaligned_addr{OpBitcast(U64, OpLoad(U32[2], ssbo_addr_pointer))};
+ const Id ssbo_addr{OpBitwiseAnd(U64, unaligned_addr, Constant(U64, ssbo_align_mask))};
const Id ssbo_size{OpUConvert(U64, OpLoad(U32[1], ssbo_size_pointer))};
const Id ssbo_end{OpIAdd(U64, ssbo_addr, ssbo_size)};
const Id cond{OpLogicalAnd(U1, OpUGreaterThanEqual(U1, addr, ssbo_addr),
diff --git a/src/shader_recompiler/frontend/maxwell/translate_program.cpp b/src/shader_recompiler/frontend/maxwell/translate_program.cpp
index 8fac6bad3..321ea625b 100644
--- a/src/shader_recompiler/frontend/maxwell/translate_program.cpp
+++ b/src/shader_recompiler/frontend/maxwell/translate_program.cpp
@@ -298,7 +298,7 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo
Optimization::PositionPass(env, program);
- Optimization::GlobalMemoryToStorageBufferPass(program);
+ Optimization::GlobalMemoryToStorageBufferPass(program, host_info);
Optimization::TexturePass(env, program, host_info);
if (Settings::values.resolution_info.active) {
diff --git a/src/shader_recompiler/host_translate_info.h b/src/shader_recompiler/host_translate_info.h
index 7d2ded907..1b53404fc 100644
--- a/src/shader_recompiler/host_translate_info.h
+++ b/src/shader_recompiler/host_translate_info.h
@@ -16,6 +16,7 @@ struct HostTranslateInfo {
bool needs_demote_reorder{}; ///< True when the device needs DemoteToHelperInvocation reordered
bool support_snorm_render_buffer{}; ///< True when the device supports SNORM render buffers
bool support_viewport_index_layer{}; ///< True when the device supports gl_Layer in VS
+ u32 min_ssbo_alignment{}; ///< Minimum alignment supported by the device for SSBOs
bool support_geometry_shader_passthrough{}; ///< True when the device supports geometry
///< passthrough shaders
bool support_conditional_barrier{}; ///< True when the device supports barriers in conditional
diff --git a/src/shader_recompiler/ir_opt/global_memory_to_storage_buffer_pass.cpp b/src/shader_recompiler/ir_opt/global_memory_to_storage_buffer_pass.cpp
index d1e59f22e..0cea79945 100644
--- a/src/shader_recompiler/ir_opt/global_memory_to_storage_buffer_pass.cpp
+++ b/src/shader_recompiler/ir_opt/global_memory_to_storage_buffer_pass.cpp
@@ -11,6 +11,7 @@
#include "shader_recompiler/frontend/ir/breadth_first_search.h"
#include "shader_recompiler/frontend/ir/ir_emitter.h"
#include "shader_recompiler/frontend/ir/value.h"
+#include "shader_recompiler/host_translate_info.h"
#include "shader_recompiler/ir_opt/passes.h"
namespace Shader::Optimization {
@@ -408,7 +409,7 @@ void CollectStorageBuffers(IR::Block& block, IR::Inst& inst, StorageInfo& info)
}
/// Returns the offset in indices (not bytes) for an equivalent storage instruction
-IR::U32 StorageOffset(IR::Block& block, IR::Inst& inst, StorageBufferAddr buffer) {
+IR::U32 StorageOffset(IR::Block& block, IR::Inst& inst, StorageBufferAddr buffer, u32 alignment) {
IR::IREmitter ir{block, IR::Block::InstructionList::s_iterator_to(inst)};
IR::U32 offset;
if (const std::optional<LowAddrInfo> low_addr{TrackLowAddress(&inst)}) {
@@ -421,7 +422,10 @@ IR::U32 StorageOffset(IR::Block& block, IR::Inst& inst, StorageBufferAddr buffer
}
// Subtract the least significant 32 bits from the guest offset. The result is the storage
// buffer offset in bytes.
- const IR::U32 low_cbuf{ir.GetCbuf(ir.Imm32(buffer.index), ir.Imm32(buffer.offset))};
+ IR::U32 low_cbuf{ir.GetCbuf(ir.Imm32(buffer.index), ir.Imm32(buffer.offset))};
+
+ // Align the offset base to match the host alignment requirements
+ low_cbuf = ir.BitwiseAnd(low_cbuf, ir.Imm32(~(alignment - 1U)));
return ir.ISub(offset, low_cbuf);
}
@@ -516,7 +520,7 @@ void Replace(IR::Block& block, IR::Inst& inst, const IR::U32& storage_index,
}
} // Anonymous namespace
-void GlobalMemoryToStorageBufferPass(IR::Program& program) {
+void GlobalMemoryToStorageBufferPass(IR::Program& program, const HostTranslateInfo& host_info) {
StorageInfo info;
for (IR::Block* const block : program.post_order_blocks) {
for (IR::Inst& inst : block->Instructions()) {
@@ -540,7 +544,8 @@ void GlobalMemoryToStorageBufferPass(IR::Program& program) {
const IR::U32 index{IR::Value{static_cast<u32>(info.set.index_of(it))}};
IR::Block* const block{storage_inst.block};
IR::Inst* const inst{storage_inst.inst};
- const IR::U32 offset{StorageOffset(*block, *inst, storage_buffer)};
+ const IR::U32 offset{
+ StorageOffset(*block, *inst, storage_buffer, host_info.min_ssbo_alignment)};
Replace(*block, *inst, index, offset);
}
}
diff --git a/src/shader_recompiler/ir_opt/passes.h b/src/shader_recompiler/ir_opt/passes.h
index d4d5285e5..1e637cb23 100644
--- a/src/shader_recompiler/ir_opt/passes.h
+++ b/src/shader_recompiler/ir_opt/passes.h
@@ -16,7 +16,7 @@ void CollectShaderInfoPass(Environment& env, IR::Program& program);
void ConditionalBarrierPass(IR::Program& program);
void ConstantPropagationPass(Environment& env, IR::Program& program);
void DeadCodeEliminationPass(IR::Program& program);
-void GlobalMemoryToStorageBufferPass(IR::Program& program);
+void GlobalMemoryToStorageBufferPass(IR::Program& program, const HostTranslateInfo& host_info);
void IdentityRemovalPass(IR::Program& program);
void LowerFp64ToFp32(IR::Program& program);
void LowerFp16ToFp32(IR::Program& program);
diff --git a/src/shader_recompiler/profile.h b/src/shader_recompiler/profile.h
index a9de9f4a9..66901a965 100644
--- a/src/shader_recompiler/profile.h
+++ b/src/shader_recompiler/profile.h
@@ -85,6 +85,8 @@ struct Profile {
/// Maxwell and earlier nVidia architectures have broken robust support
bool has_broken_robust{};
+
+ u64 min_ssbo_alignment{};
};
} // namespace Shader
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index b65b9f2a2..c22c7631c 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -15,6 +15,7 @@ add_library(video_core STATIC
buffer_cache/buffer_cache.cpp
buffer_cache/buffer_cache.h
buffer_cache/memory_tracker_base.h
+ buffer_cache/usage_tracker.h
buffer_cache/word_manager.h
cache_types.h
cdma_pusher.cpp
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index f5b10411b..6d1fc3887 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -67,6 +67,7 @@ void BufferCache<P>::TickFrame() {
if (!channel_state) {
return;
}
+ runtime.TickFrame(slot_buffers);
// Calculate hits and shots and move hit bits to the right
const u32 hits = std::reduce(channel_state->uniform_cache_hits.begin(),
@@ -230,7 +231,10 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am
for (const IntervalType& add_interval : tmp_intervals) {
common_ranges.add(add_interval);
}
- runtime.CopyBuffer(dest_buffer, src_buffer, copies);
+ const auto& copy = copies[0];
+ src_buffer.MarkUsage(copy.src_offset, copy.size);
+ dest_buffer.MarkUsage(copy.dst_offset, copy.size);
+ runtime.CopyBuffer(dest_buffer, src_buffer, copies, true);
if (has_new_downloads) {
memory_tracker.MarkRegionAsGpuModified(*cpu_dest_address, amount);
}
@@ -258,9 +262,10 @@ bool BufferCache<P>::DMAClear(GPUVAddr dst_address, u64 amount, u32 value) {
common_ranges.subtract(subtract_interval);
const BufferId buffer = FindBuffer(*cpu_dst_address, static_cast<u32>(size));
- auto& dest_buffer = slot_buffers[buffer];
+ Buffer& dest_buffer = slot_buffers[buffer];
const u32 offset = dest_buffer.Offset(*cpu_dst_address);
runtime.ClearBuffer(dest_buffer, offset, size, value);
+ dest_buffer.MarkUsage(offset, size);
return true;
}
@@ -603,6 +608,7 @@ void BufferCache<P>::CommitAsyncFlushesHigh() {
VAddr orig_cpu_addr = static_cast<VAddr>(second_copy.src_offset);
const IntervalType base_interval{orig_cpu_addr, orig_cpu_addr + copy.size};
async_downloads += std::make_pair(base_interval, 1);
+ buffer.MarkUsage(copy.src_offset, copy.size);
runtime.CopyBuffer(download_staging.buffer, buffer, copies, false);
normalized_copies.push_back(second_copy);
}
@@ -621,8 +627,9 @@ void BufferCache<P>::CommitAsyncFlushesHigh() {
// Have in mind the staging buffer offset for the copy
copy.dst_offset += download_staging.offset;
const std::array copies{copy};
- runtime.CopyBuffer(download_staging.buffer, slot_buffers[buffer_id], copies,
- false);
+ Buffer& buffer = slot_buffers[buffer_id];
+ buffer.MarkUsage(copy.src_offset, copy.size);
+ runtime.CopyBuffer(download_staging.buffer, buffer, copies, false);
}
runtime.PostCopyBarrier();
runtime.Finish();
@@ -742,7 +749,7 @@ void BufferCache<P>::BindHostIndexBuffer() {
{BufferCopy{.src_offset = upload_staging.offset, .dst_offset = 0, .size = size}}};
std::memcpy(upload_staging.mapped_span.data(),
draw_state.inline_index_draw_indexes.data(), size);
- runtime.CopyBuffer(buffer, upload_staging.buffer, copies);
+ runtime.CopyBuffer(buffer, upload_staging.buffer, copies, true);
} else {
buffer.ImmediateUpload(0, draw_state.inline_index_draw_indexes);
}
@@ -754,6 +761,7 @@ void BufferCache<P>::BindHostIndexBuffer() {
offset + draw_state.index_buffer.first * draw_state.index_buffer.FormatSizeInBytes();
runtime.BindIndexBuffer(buffer, new_offset, size);
} else {
+ buffer.MarkUsage(offset, size);
runtime.BindIndexBuffer(draw_state.topology, draw_state.index_buffer.format,
draw_state.index_buffer.first, draw_state.index_buffer.count,
buffer, offset, size);
@@ -790,6 +798,7 @@ void BufferCache<P>::BindHostVertexBuffers() {
const u32 stride = maxwell3d->regs.vertex_streams[index].stride;
const u32 offset = buffer.Offset(binding.cpu_addr);
+ buffer.MarkUsage(offset, binding.size);
host_bindings.buffers.push_back(&buffer);
host_bindings.offsets.push_back(offset);
@@ -895,6 +904,7 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
if constexpr (HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS) {
channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
}
+ buffer.MarkUsage(offset, size);
if constexpr (NEEDS_BIND_UNIFORM_INDEX) {
runtime.BindUniformBuffer(stage, binding_index, buffer, offset, size);
} else {
@@ -913,6 +923,7 @@ void BufferCache<P>::BindHostGraphicsStorageBuffers(size_t stage) {
SynchronizeBuffer(buffer, binding.cpu_addr, size);
const u32 offset = buffer.Offset(binding.cpu_addr);
+ buffer.MarkUsage(offset, size);
const bool is_written = ((channel_state->written_storage_buffers[stage] >> index) & 1) != 0;
if (is_written) {
@@ -943,6 +954,7 @@ void BufferCache<P>::BindHostGraphicsTextureBuffers(size_t stage) {
const u32 offset = buffer.Offset(binding.cpu_addr);
const PixelFormat format = binding.format;
+ buffer.MarkUsage(offset, size);
if constexpr (SEPARATE_IMAGE_BUFFERS_BINDINGS) {
if (((channel_state->image_texture_buffers[stage] >> index) & 1) != 0) {
runtime.BindImageBuffer(buffer, offset, size, format);
@@ -975,9 +987,10 @@ void BufferCache<P>::BindHostTransformFeedbackBuffers() {
MarkWrittenBuffer(binding.buffer_id, binding.cpu_addr, size);
const u32 offset = buffer.Offset(binding.cpu_addr);
+ buffer.MarkUsage(offset, size);
host_bindings.buffers.push_back(&buffer);
host_bindings.offsets.push_back(offset);
- host_bindings.sizes.push_back(binding.size);
+ host_bindings.sizes.push_back(size);
}
if (host_bindings.buffers.size() > 0) {
runtime.BindTransformFeedbackBuffers(host_bindings);
@@ -1001,6 +1014,7 @@ void BufferCache<P>::BindHostComputeUniformBuffers() {
SynchronizeBuffer(buffer, binding.cpu_addr, size);
const u32 offset = buffer.Offset(binding.cpu_addr);
+ buffer.MarkUsage(offset, size);
if constexpr (NEEDS_BIND_UNIFORM_INDEX) {
runtime.BindComputeUniformBuffer(binding_index, buffer, offset, size);
++binding_index;
@@ -1021,6 +1035,7 @@ void BufferCache<P>::BindHostComputeStorageBuffers() {
SynchronizeBuffer(buffer, binding.cpu_addr, size);
const u32 offset = buffer.Offset(binding.cpu_addr);
+ buffer.MarkUsage(offset, size);
const bool is_written =
((channel_state->written_compute_storage_buffers >> index) & 1) != 0;
@@ -1053,6 +1068,7 @@ void BufferCache<P>::BindHostComputeTextureBuffers() {
const u32 offset = buffer.Offset(binding.cpu_addr);
const PixelFormat format = binding.format;
+ buffer.MarkUsage(offset, size);
if constexpr (SEPARATE_IMAGE_BUFFERS_BINDINGS) {
if (((channel_state->image_compute_texture_buffers >> index) & 1) != 0) {
runtime.BindImageBuffer(buffer, offset, size, format);
@@ -1172,10 +1188,11 @@ void BufferCache<P>::UpdateVertexBuffer(u32 index) {
if (!gpu_memory->IsWithinGPUAddressRange(gpu_addr_end)) {
size = static_cast<u32>(gpu_memory->MaxContinuousRange(gpu_addr_begin, size));
}
+ const BufferId buffer_id = FindBuffer(*cpu_addr, size);
channel_state->vertex_buffers[index] = Binding{
.cpu_addr = *cpu_addr,
.size = size,
- .buffer_id = FindBuffer(*cpu_addr, size),
+ .buffer_id = buffer_id,
};
}
@@ -1401,7 +1418,8 @@ void BufferCache<P>::JoinOverlap(BufferId new_buffer_id, BufferId overlap_id,
.dst_offset = dst_base_offset,
.size = overlap.SizeBytes(),
});
- runtime.CopyBuffer(new_buffer, overlap, copies);
+ new_buffer.MarkUsage(copies[0].dst_offset, copies[0].size);
+ runtime.CopyBuffer(new_buffer, overlap, copies, true);
DeleteBuffer(overlap_id, true);
}
@@ -1414,7 +1432,9 @@ BufferId BufferCache<P>::CreateBuffer(VAddr cpu_addr, u32 wanted_size) {
const u32 size = static_cast<u32>(overlap.end - overlap.begin);
const BufferId new_buffer_id = slot_buffers.insert(runtime, rasterizer, overlap.begin, size);
auto& new_buffer = slot_buffers[new_buffer_id];
- runtime.ClearBuffer(new_buffer, 0, new_buffer.SizeBytes(), 0);
+ const size_t size_bytes = new_buffer.SizeBytes();
+ runtime.ClearBuffer(new_buffer, 0, size_bytes, 0);
+ new_buffer.MarkUsage(0, size_bytes);
for (const BufferId overlap_id : overlap.ids) {
JoinOverlap(new_buffer_id, overlap_id, !overlap.has_stream_leap);
}
@@ -1467,11 +1487,6 @@ void BufferCache<P>::TouchBuffer(Buffer& buffer, BufferId buffer_id) noexcept {
template <class P>
bool BufferCache<P>::SynchronizeBuffer(Buffer& buffer, VAddr cpu_addr, u32 size) {
- return SynchronizeBufferImpl(buffer, cpu_addr, size);
-}
-
-template <class P>
-bool BufferCache<P>::SynchronizeBufferImpl(Buffer& buffer, VAddr cpu_addr, u32 size) {
boost::container::small_vector<BufferCopy, 4> copies;
u64 total_size_bytes = 0;
u64 largest_copy = 0;
@@ -1494,51 +1509,6 @@ bool BufferCache<P>::SynchronizeBufferImpl(Buffer& buffer, VAddr cpu_addr, u32 s
}
template <class P>
-bool BufferCache<P>::SynchronizeBufferNoModified(Buffer& buffer, VAddr cpu_addr, u32 size) {
- boost::container::small_vector<BufferCopy, 4> copies;
- u64 total_size_bytes = 0;
- u64 largest_copy = 0;
- IntervalSet found_sets{};
- auto make_copies = [&] {
- for (auto& interval : found_sets) {
- const std::size_t sub_size = interval.upper() - interval.lower();
- const VAddr cpu_addr_ = interval.lower();
- copies.push_back(BufferCopy{
- .src_offset = total_size_bytes,
- .dst_offset = cpu_addr_ - buffer.CpuAddr(),
- .size = sub_size,
- });
- total_size_bytes += sub_size;
- largest_copy = std::max<u64>(largest_copy, sub_size);
- }
- const std::span<BufferCopy> copies_span(copies.data(), copies.size());
- UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
- };
- memory_tracker.ForEachUploadRange(cpu_addr, size, [&](u64 cpu_addr_out, u64 range_size) {
- const VAddr base_adr = cpu_addr_out;
- const VAddr end_adr = base_adr + range_size;
- const IntervalType add_interval{base_adr, end_adr};
- found_sets.add(add_interval);
- });
- if (found_sets.empty()) {
- return true;
- }
- const IntervalType search_interval{cpu_addr, cpu_addr + size};
- auto it = common_ranges.lower_bound(search_interval);
- auto it_end = common_ranges.upper_bound(search_interval);
- if (it == common_ranges.end()) {
- make_copies();
- return false;
- }
- while (it != it_end) {
- found_sets.subtract(*it);
- it++;
- }
- make_copies();
- return false;
-}
-
-template <class P>
void BufferCache<P>::UploadMemory(Buffer& buffer, u64 total_size_bytes, u64 largest_copy,
std::span<BufferCopy> copies) {
if constexpr (USE_MEMORY_MAPS_FOR_UPLOADS) {
@@ -1586,7 +1556,8 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
// Apply the staging offset
copy.src_offset += upload_staging.offset;
}
- runtime.CopyBuffer(buffer, upload_staging.buffer, copies);
+ const bool can_reorder = runtime.CanReorderUpload(buffer, copies);
+ runtime.CopyBuffer(buffer, upload_staging.buffer, copies, true, can_reorder);
}
}
@@ -1628,7 +1599,8 @@ void BufferCache<P>::InlineMemoryImplementation(VAddr dest_address, size_t copy_
}};
u8* const src_pointer = upload_staging.mapped_span.data();
std::memcpy(src_pointer, inlined_buffer.data(), copy_size);
- runtime.CopyBuffer(buffer, upload_staging.buffer, copies);
+ const bool can_reorder = runtime.CanReorderUpload(buffer, copies);
+ runtime.CopyBuffer(buffer, upload_staging.buffer, copies, true, can_reorder);
} else {
buffer.ImmediateUpload(buffer.Offset(dest_address), inlined_buffer.first(copy_size));
}
@@ -1681,8 +1653,9 @@ void BufferCache<P>::DownloadBufferMemory(Buffer& buffer, VAddr cpu_addr, u64 si
for (BufferCopy& copy : copies) {
// Modify copies to have the staging offset in mind
copy.dst_offset += download_staging.offset;
+ buffer.MarkUsage(copy.src_offset, copy.size);
}
- runtime.CopyBuffer(download_staging.buffer, buffer, copies_span);
+ runtime.CopyBuffer(download_staging.buffer, buffer, copies_span, true);
runtime.Finish();
for (const BufferCopy& copy : copies) {
const VAddr copy_cpu_addr = buffer.CpuAddr() + copy.src_offset;
@@ -1780,15 +1753,25 @@ Binding BufferCache<P>::StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index,
const u32 memory_layout_size = static_cast<u32>(gpu_memory->GetMemoryLayoutSize(gpu_addr));
return std::min(memory_layout_size, static_cast<u32>(8_MiB));
}();
- const std::optional<VAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
- if (!cpu_addr || size == 0) {
+ // Alignment only applies to the offset of the buffer
+ const u32 alignment = runtime.GetStorageBufferAlignment();
+ const GPUVAddr aligned_gpu_addr = Common::AlignDown(gpu_addr, alignment);
+ const u32 aligned_size = static_cast<u32>(gpu_addr - aligned_gpu_addr) + size;
+
+ const std::optional<VAddr> aligned_cpu_addr = gpu_memory->GpuToCpuAddress(aligned_gpu_addr);
+ if (!aligned_cpu_addr || size == 0) {
LOG_WARNING(HW_GPU, "Failed to find storage buffer for cbuf index {}", cbuf_index);
return NULL_BINDING;
}
- const VAddr cpu_end = Common::AlignUp(*cpu_addr + size, YUZU_PAGESIZE);
+ const std::optional<VAddr> cpu_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
+ ASSERT_MSG(cpu_addr, "Unaligned storage buffer address not found for cbuf index {}",
+ cbuf_index);
+ // The end address used for size calculation does not need to be aligned
+ const VAddr cpu_end = Common::AlignUp(*cpu_addr + size, Core::Memory::YUZU_PAGESIZE);
+
const Binding binding{
- .cpu_addr = *cpu_addr,
- .size = is_written ? size : static_cast<u32>(cpu_end - *cpu_addr),
+ .cpu_addr = *aligned_cpu_addr,
+ .size = is_written ? aligned_size : static_cast<u32>(cpu_end - *aligned_cpu_addr),
.buffer_id = BufferId{},
};
return binding;
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index eed267361..d6d696d8c 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -529,10 +529,6 @@ private:
bool SynchronizeBuffer(Buffer& buffer, VAddr cpu_addr, u32 size);
- bool SynchronizeBufferImpl(Buffer& buffer, VAddr cpu_addr, u32 size);
-
- bool SynchronizeBufferNoModified(Buffer& buffer, VAddr cpu_addr, u32 size);
-
void UploadMemory(Buffer& buffer, u64 total_size_bytes, u64 largest_copy,
std::span<BufferCopy> copies);
diff --git a/src/video_core/buffer_cache/usage_tracker.h b/src/video_core/buffer_cache/usage_tracker.h
new file mode 100644
index 000000000..5f8688d31
--- /dev/null
+++ b/src/video_core/buffer_cache/usage_tracker.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "common/alignment.h"
+#include "common/common_types.h"
+
+namespace VideoCommon {
+
+class UsageTracker {
+ static constexpr size_t BYTES_PER_BIT_SHIFT = 6;
+ static constexpr size_t PAGE_SHIFT = 6 + BYTES_PER_BIT_SHIFT;
+ static constexpr size_t PAGE_BYTES = 1 << PAGE_SHIFT;
+
+public:
+ explicit UsageTracker(size_t size) {
+ const size_t num_pages = (size >> PAGE_SHIFT) + 1;
+ pages.resize(num_pages, 0ULL);
+ }
+
+ void Reset() noexcept {
+ std::ranges::fill(pages, 0ULL);
+ }
+
+ void Track(u64 offset, u64 size) noexcept {
+ const size_t page = offset >> PAGE_SHIFT;
+ const size_t page_end = (offset + size) >> PAGE_SHIFT;
+ TrackPage(page, offset, size);
+ if (page == page_end) {
+ return;
+ }
+ for (size_t i = page + 1; i < page_end; i++) {
+ pages[i] = ~u64{0};
+ }
+ const size_t offset_end = offset + size;
+ const size_t offset_end_page_aligned = Common::AlignDown(offset_end, PAGE_BYTES);
+ TrackPage(page_end, offset_end_page_aligned, offset_end - offset_end_page_aligned);
+ }
+
+ [[nodiscard]] bool IsUsed(u64 offset, u64 size) const noexcept {
+ const size_t page = offset >> PAGE_SHIFT;
+ const size_t page_end = (offset + size) >> PAGE_SHIFT;
+ if (IsPageUsed(page, offset, size)) {
+ return true;
+ }
+ for (size_t i = page + 1; i < page_end; i++) {
+ if (pages[i] != 0) {
+ return true;
+ }
+ }
+ const size_t offset_end = offset + size;
+ const size_t offset_end_page_aligned = Common::AlignDown(offset_end, PAGE_BYTES);
+ return IsPageUsed(page_end, offset_end_page_aligned, offset_end - offset_end_page_aligned);
+ }
+
+private:
+ void TrackPage(u64 page, u64 offset, u64 size) noexcept {
+ const size_t offset_in_page = offset % PAGE_BYTES;
+ const size_t first_bit = offset_in_page >> BYTES_PER_BIT_SHIFT;
+ const size_t num_bits = std::min<size_t>(size, PAGE_BYTES) >> BYTES_PER_BIT_SHIFT;
+ const size_t mask = ~u64{0} >> (64 - num_bits);
+ pages[page] |= (~u64{0} & mask) << first_bit;
+ }
+
+ bool IsPageUsed(u64 page, u64 offset, u64 size) const noexcept {
+ const size_t offset_in_page = offset % PAGE_BYTES;
+ const size_t first_bit = offset_in_page >> BYTES_PER_BIT_SHIFT;
+ const size_t num_bits = std::min<size_t>(size, PAGE_BYTES) >> BYTES_PER_BIT_SHIFT;
+ const size_t mask = ~u64{0} >> (64 - num_bits);
+ const size_t mask2 = (~u64{0} & mask) << first_bit;
+ return (pages[page] & mask2) != 0;
+ }
+
+private:
+ std::vector<u64> pages;
+};
+
+} // namespace VideoCommon
diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.cpp b/src/video_core/renderer_opengl/gl_buffer_cache.cpp
index 38d553d3c..dfd696de6 100644
--- a/src/video_core/renderer_opengl/gl_buffer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_buffer_cache.cpp
@@ -178,13 +178,14 @@ void BufferCacheRuntime::CopyBuffer(GLuint dst_buffer, Buffer& src_buffer,
}
void BufferCacheRuntime::CopyBuffer(Buffer& dst_buffer, GLuint src_buffer,
- std::span<const VideoCommon::BufferCopy> copies, bool barrier) {
+ std::span<const VideoCommon::BufferCopy> copies, bool barrier,
+ bool) {
CopyBuffer(dst_buffer.Handle(), src_buffer, copies, barrier);
}
void BufferCacheRuntime::CopyBuffer(Buffer& dst_buffer, Buffer& src_buffer,
- std::span<const VideoCommon::BufferCopy> copies) {
- CopyBuffer(dst_buffer.Handle(), src_buffer.Handle(), copies);
+ std::span<const VideoCommon::BufferCopy> copies, bool) {
+ CopyBuffer(dst_buffer.Handle(), src_buffer.Handle(), copies, true);
}
void BufferCacheRuntime::PreCopyBarrier() {
diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.h b/src/video_core/renderer_opengl/gl_buffer_cache.h
index 41b746f3b..000f29a82 100644
--- a/src/video_core/renderer_opengl/gl_buffer_cache.h
+++ b/src/video_core/renderer_opengl/gl_buffer_cache.h
@@ -30,6 +30,8 @@ public:
void MakeResident(GLenum access) noexcept;
+ void MarkUsage(u64 offset, u64 size) {}
+
[[nodiscard]] GLuint View(u32 offset, u32 size, VideoCore::Surface::PixelFormat format);
[[nodiscard]] GLuint64EXT HostGpuAddr() const noexcept {
@@ -66,22 +68,29 @@ public:
[[nodiscard]] StagingBufferMap DownloadStagingBuffer(size_t size);
+ bool CanReorderUpload(const Buffer&, std::span<const VideoCommon::BufferCopy>) {
+ return false;
+ }
+
void CopyBuffer(GLuint dst_buffer, GLuint src_buffer,
- std::span<const VideoCommon::BufferCopy> copies, bool barrier = true);
+ std::span<const VideoCommon::BufferCopy> copies, bool barrier);
void CopyBuffer(GLuint dst_buffer, Buffer& src_buffer,
- std::span<const VideoCommon::BufferCopy> copies, bool barrier = true);
+ std::span<const VideoCommon::BufferCopy> copies, bool barrier);
void CopyBuffer(Buffer& dst_buffer, GLuint src_buffer,
- std::span<const VideoCommon::BufferCopy> copies, bool barrier = true);
+ std::span<const VideoCommon::BufferCopy> copies, bool barrier,
+ bool can_reorder_upload = false);
void CopyBuffer(Buffer& dst_buffer, Buffer& src_buffer,
- std::span<const VideoCommon::BufferCopy> copies);
+ std::span<const VideoCommon::BufferCopy> copies, bool);
void PreCopyBarrier();
void PostCopyBarrier();
void Finish();
+ void TickFrame(VideoCommon::SlotVector<Buffer>&) noexcept {}
+
void ClearBuffer(Buffer& dest_buffer, u32 offset, size_t size, u32 value);
void BindIndexBuffer(Buffer& buffer, u32 offset, u32 size);
@@ -182,6 +191,10 @@ public:
return device.CanReportMemoryUsage();
}
+ u32 GetStorageBufferAlignment() const {
+ return static_cast<u32>(device.GetShaderStorageBufferAlignment());
+ }
+
private:
static constexpr std::array PABO_LUT{
GL_VERTEX_PROGRAM_PARAMETER_BUFFER_NV, GL_TESS_CONTROL_PROGRAM_PARAMETER_BUFFER_NV,
diff --git a/src/video_core/renderer_opengl/gl_device.cpp b/src/video_core/renderer_opengl/gl_device.cpp
index 94258ccd0..993438a27 100644
--- a/src/video_core/renderer_opengl/gl_device.cpp
+++ b/src/video_core/renderer_opengl/gl_device.cpp
@@ -265,33 +265,33 @@ std::string Device::GetVendorName() const {
if (vendor_name == "Intel") {
// For Mesa, `Intel` is an overloaded vendor string that could mean crocus or iris.
// Simply return `INTEL` for those as well as the Windows driver.
- return "INTEL";
+ return "Intel";
}
if (vendor_name == "Intel Open Source Technology Center") {
- return "I965";
+ return "i965";
}
if (vendor_name == "Mesa Project") {
- return "I915";
+ return "i915";
}
if (vendor_name == "Mesa/X.org") {
// This vendor string is overloaded between llvmpipe, softpipe, and virgl, so just return
// MESA instead of one of those driver names.
- return "MESA";
+ return "Mesa";
}
if (vendor_name == "AMD") {
- return "RADEONSI";
+ return "RadeonSI";
}
if (vendor_name == "nouveau") {
- return "NOUVEAU";
+ return "Nouveau";
}
if (vendor_name == "X.Org") {
return "R600";
}
if (vendor_name == "Collabora Ltd") {
- return "ZINK";
+ return "Zink";
}
if (vendor_name == "Intel Corporation") {
- return "OPENSWR";
+ return "OpenSWR";
}
if (vendor_name == "Microsoft Corporation") {
return "D3D12";
@@ -300,7 +300,7 @@ std::string Device::GetVendorName() const {
// Mesa's tegra driver reports `NVIDIA`. Only present in this list because the default
// strategy would have returned `NVIDIA` here for this driver, the same result as the
// proprietary driver.
- return "TEGRA";
+ return "Tegra";
}
return vendor_name;
}
diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp
index 2888e0238..26f2d0ea7 100644
--- a/src/video_core/renderer_opengl/gl_shader_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp
@@ -232,6 +232,7 @@ ShaderCache::ShaderCache(RasterizerOpenGL& rasterizer_, Core::Frontend::EmuWindo
.has_gl_bool_ref_bug = device.HasBoolRefBug(),
.ignore_nan_fp_comparisons = true,
.gl_max_compute_smem_size = device.GetMaxComputeSharedMemorySize(),
+ .min_ssbo_alignment = device.GetShaderStorageBufferAlignment(),
},
host_info{
.support_float64 = true,
@@ -240,6 +241,7 @@ ShaderCache::ShaderCache(RasterizerOpenGL& rasterizer_, Core::Frontend::EmuWindo
.needs_demote_reorder = device.IsAmd(),
.support_snorm_render_buffer = false,
.support_viewport_index_layer = device.HasVertexViewportLayer(),
+ .min_ssbo_alignment = static_cast<u32>(device.GetShaderStorageBufferAlignment()),
.support_geometry_shader_passthrough = device.HasGeometryShaderPassthrough(),
.support_conditional_barrier = device.SupportsConditionalBarriers(),
} {
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index d8148e89a..5958f52f7 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -79,13 +79,13 @@ vk::Buffer CreateBuffer(const Device& device, const MemoryAllocator& memory_allo
} // Anonymous namespace
Buffer::Buffer(BufferCacheRuntime&, VideoCommon::NullBufferParams null_params)
- : VideoCommon::BufferBase<VideoCore::RasterizerInterface>(null_params) {}
+ : VideoCommon::BufferBase<VideoCore::RasterizerInterface>(null_params), tracker{4096} {}
Buffer::Buffer(BufferCacheRuntime& runtime, VideoCore::RasterizerInterface& rasterizer_,
VAddr cpu_addr_, u64 size_bytes_)
: VideoCommon::BufferBase<VideoCore::RasterizerInterface>(rasterizer_, cpu_addr_, size_bytes_),
- device{&runtime.device}, buffer{
- CreateBuffer(*device, runtime.memory_allocator, SizeBytes())} {
+ device{&runtime.device}, buffer{CreateBuffer(*device, runtime.memory_allocator, SizeBytes())},
+ tracker{SizeBytes()} {
if (runtime.device.HasDebuggingToolAttached()) {
buffer.SetObjectNameEXT(fmt::format("Buffer 0x{:x}", CpuAddr()).c_str());
}
@@ -355,12 +355,35 @@ bool BufferCacheRuntime::CanReportMemoryUsage() const {
return device.CanReportMemoryUsage();
}
+u32 BufferCacheRuntime::GetStorageBufferAlignment() const {
+ return static_cast<u32>(device.GetStorageBufferAlignment());
+}
+
+void BufferCacheRuntime::TickFrame(VideoCommon::SlotVector<Buffer>& slot_buffers) noexcept {
+ for (auto it = slot_buffers.begin(); it != slot_buffers.end(); it++) {
+ it->ResetUsageTracking();
+ }
+}
+
void BufferCacheRuntime::Finish() {
scheduler.Finish();
}
+bool BufferCacheRuntime::CanReorderUpload(const Buffer& buffer,
+ std::span<const VideoCommon::BufferCopy> copies) {
+ if (Settings::values.disable_buffer_reorder) {
+ return false;
+ }
+ const bool can_use_upload_cmdbuf =
+ std::ranges::all_of(copies, [&](const VideoCommon::BufferCopy& copy) {
+ return !buffer.IsRegionUsed(copy.dst_offset, copy.size);
+ });
+ return can_use_upload_cmdbuf;
+}
+
void BufferCacheRuntime::CopyBuffer(VkBuffer dst_buffer, VkBuffer src_buffer,
- std::span<const VideoCommon::BufferCopy> copies, bool barrier) {
+ std::span<const VideoCommon::BufferCopy> copies, bool barrier,
+ bool can_reorder_upload) {
if (dst_buffer == VK_NULL_HANDLE || src_buffer == VK_NULL_HANDLE) {
return;
}
@@ -376,9 +399,18 @@ void BufferCacheRuntime::CopyBuffer(VkBuffer dst_buffer, VkBuffer src_buffer,
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
};
+
// Measuring a popular game, this number never exceeds the specified size once data is warmed up
boost::container::small_vector<VkBufferCopy, 8> vk_copies(copies.size());
std::ranges::transform(copies, vk_copies.begin(), MakeBufferCopy);
+ if (src_buffer == staging_pool.StreamBuf() && can_reorder_upload) {
+ scheduler.RecordWithUploadBuffer([src_buffer, dst_buffer, vk_copies](
+ vk::CommandBuffer, vk::CommandBuffer upload_cmdbuf) {
+ upload_cmdbuf.CopyBuffer(src_buffer, dst_buffer, vk_copies);
+ });
+ return;
+ }
+
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([src_buffer, dst_buffer, vk_copies, barrier](vk::CommandBuffer cmdbuf) {
if (barrier) {
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index 95446c732..0b3fbd6d0 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -5,6 +5,7 @@
#include "video_core/buffer_cache/buffer_cache_base.h"
#include "video_core/buffer_cache/memory_tracker_base.h"
+#include "video_core/buffer_cache/usage_tracker.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/renderer_vulkan/vk_compute_pass.h"
#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
@@ -34,6 +35,18 @@ public:
return *buffer;
}
+ [[nodiscard]] bool IsRegionUsed(u64 offset, u64 size) const noexcept {
+ return tracker.IsUsed(offset, size);
+ }
+
+ void MarkUsage(u64 offset, u64 size) noexcept {
+ tracker.Track(offset, size);
+ }
+
+ void ResetUsageTracking() noexcept {
+ tracker.Reset();
+ }
+
operator VkBuffer() const noexcept {
return *buffer;
}
@@ -49,6 +62,7 @@ private:
const Device* device{};
vk::Buffer buffer;
std::vector<BufferView> views;
+ VideoCommon::UsageTracker tracker;
};
class QuadArrayIndexBuffer;
@@ -67,6 +81,8 @@ public:
ComputePassDescriptorQueue& compute_pass_descriptor_queue,
DescriptorPool& descriptor_pool);
+ void TickFrame(VideoCommon::SlotVector<Buffer>& slot_buffers) noexcept;
+
void Finish();
u64 GetDeviceLocalMemory() const;
@@ -75,16 +91,21 @@ public:
bool CanReportMemoryUsage() const;
+ u32 GetStorageBufferAlignment() const;
+
[[nodiscard]] StagingBufferRef UploadStagingBuffer(size_t size);
[[nodiscard]] StagingBufferRef DownloadStagingBuffer(size_t size, bool deferred = false);
+ bool CanReorderUpload(const Buffer& buffer, std::span<const VideoCommon::BufferCopy> copies);
+
void FreeDeferredStagingBuffer(StagingBufferRef& ref);
void PreCopyBarrier();
void CopyBuffer(VkBuffer src_buffer, VkBuffer dst_buffer,
- std::span<const VideoCommon::BufferCopy> copies, bool barrier = true);
+ std::span<const VideoCommon::BufferCopy> copies, bool barrier,
+ bool can_reorder_upload = false);
void PostCopyBarrier();
diff --git a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp
index 6b288b994..ac8b6e838 100644
--- a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp
+++ b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp
@@ -100,12 +100,14 @@ void MasterSemaphore::Wait(u64 tick) {
Refresh();
}
-VkResult MasterSemaphore::SubmitQueue(vk::CommandBuffer& cmdbuf, VkSemaphore signal_semaphore,
- VkSemaphore wait_semaphore, u64 host_tick) {
+VkResult MasterSemaphore::SubmitQueue(vk::CommandBuffer& cmdbuf, vk::CommandBuffer& upload_cmdbuf,
+ VkSemaphore signal_semaphore, VkSemaphore wait_semaphore,
+ u64 host_tick) {
if (semaphore) {
- return SubmitQueueTimeline(cmdbuf, signal_semaphore, wait_semaphore, host_tick);
+ return SubmitQueueTimeline(cmdbuf, upload_cmdbuf, signal_semaphore, wait_semaphore,
+ host_tick);
} else {
- return SubmitQueueFence(cmdbuf, signal_semaphore, wait_semaphore, host_tick);
+ return SubmitQueueFence(cmdbuf, upload_cmdbuf, signal_semaphore, wait_semaphore, host_tick);
}
}
@@ -115,6 +117,7 @@ static constexpr std::array<VkPipelineStageFlags, 2> wait_stage_masks{
};
VkResult MasterSemaphore::SubmitQueueTimeline(vk::CommandBuffer& cmdbuf,
+ vk::CommandBuffer& upload_cmdbuf,
VkSemaphore signal_semaphore,
VkSemaphore wait_semaphore, u64 host_tick) {
const VkSemaphore timeline_semaphore = *semaphore;
@@ -123,6 +126,8 @@ VkResult MasterSemaphore::SubmitQueueTimeline(vk::CommandBuffer& cmdbuf,
const std::array signal_values{host_tick, u64(0)};
const std::array signal_semaphores{timeline_semaphore, signal_semaphore};
+ const std::array cmdbuffers{*upload_cmdbuf, *cmdbuf};
+
const u32 num_wait_semaphores = wait_semaphore ? 1 : 0;
const VkTimelineSemaphoreSubmitInfo timeline_si{
.sType = VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO,
@@ -138,8 +143,8 @@ VkResult MasterSemaphore::SubmitQueueTimeline(vk::CommandBuffer& cmdbuf,
.waitSemaphoreCount = num_wait_semaphores,
.pWaitSemaphores = &wait_semaphore,
.pWaitDstStageMask = wait_stage_masks.data(),
- .commandBufferCount = 1,
- .pCommandBuffers = cmdbuf.address(),
+ .commandBufferCount = static_cast<u32>(cmdbuffers.size()),
+ .pCommandBuffers = cmdbuffers.data(),
.signalSemaphoreCount = num_signal_semaphores,
.pSignalSemaphores = signal_semaphores.data(),
};
@@ -147,19 +152,23 @@ VkResult MasterSemaphore::SubmitQueueTimeline(vk::CommandBuffer& cmdbuf,
return device.GetGraphicsQueue().Submit(submit_info);
}
-VkResult MasterSemaphore::SubmitQueueFence(vk::CommandBuffer& cmdbuf, VkSemaphore signal_semaphore,
- VkSemaphore wait_semaphore, u64 host_tick) {
+VkResult MasterSemaphore::SubmitQueueFence(vk::CommandBuffer& cmdbuf,
+ vk::CommandBuffer& upload_cmdbuf,
+ VkSemaphore signal_semaphore, VkSemaphore wait_semaphore,
+ u64 host_tick) {
const u32 num_signal_semaphores = signal_semaphore ? 1 : 0;
const u32 num_wait_semaphores = wait_semaphore ? 1 : 0;
+ const std::array cmdbuffers{*upload_cmdbuf, *cmdbuf};
+
const VkSubmitInfo submit_info{
.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
.pNext = nullptr,
.waitSemaphoreCount = num_wait_semaphores,
.pWaitSemaphores = &wait_semaphore,
.pWaitDstStageMask = wait_stage_masks.data(),
- .commandBufferCount = 1,
- .pCommandBuffers = cmdbuf.address(),
+ .commandBufferCount = static_cast<u32>(cmdbuffers.size()),
+ .pCommandBuffers = cmdbuffers.data(),
.signalSemaphoreCount = num_signal_semaphores,
.pSignalSemaphores = &signal_semaphore,
};
diff --git a/src/video_core/renderer_vulkan/vk_master_semaphore.h b/src/video_core/renderer_vulkan/vk_master_semaphore.h
index 3f599d7bd..7dfb93ffb 100644
--- a/src/video_core/renderer_vulkan/vk_master_semaphore.h
+++ b/src/video_core/renderer_vulkan/vk_master_semaphore.h
@@ -52,14 +52,16 @@ public:
void Wait(u64 tick);
/// Submits the device graphics queue, updating the tick as necessary
- VkResult SubmitQueue(vk::CommandBuffer& cmdbuf, VkSemaphore signal_semaphore,
- VkSemaphore wait_semaphore, u64 host_tick);
+ VkResult SubmitQueue(vk::CommandBuffer& cmdbuf, vk::CommandBuffer& upload_cmdbuf,
+ VkSemaphore signal_semaphore, VkSemaphore wait_semaphore, u64 host_tick);
private:
- VkResult SubmitQueueTimeline(vk::CommandBuffer& cmdbuf, VkSemaphore signal_semaphore,
- VkSemaphore wait_semaphore, u64 host_tick);
- VkResult SubmitQueueFence(vk::CommandBuffer& cmdbuf, VkSemaphore signal_semaphore,
- VkSemaphore wait_semaphore, u64 host_tick);
+ VkResult SubmitQueueTimeline(vk::CommandBuffer& cmdbuf, vk::CommandBuffer& upload_cmdbuf,
+ VkSemaphore signal_semaphore, VkSemaphore wait_semaphore,
+ u64 host_tick);
+ VkResult SubmitQueueFence(vk::CommandBuffer& cmdbuf, vk::CommandBuffer& upload_cmdbuf,
+ VkSemaphore signal_semaphore, VkSemaphore wait_semaphore,
+ u64 host_tick);
void WaitThread(std::stop_token token);
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 89b455bff..2a13b2a72 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -373,6 +373,7 @@ PipelineCache::PipelineCache(RasterizerVulkan& rasterizer_, const Device& device
driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY,
.has_broken_robust =
device.IsNvidia() && device.GetNvidiaArch() <= NvidiaArchitecture::Arch_Pascal,
+ .min_ssbo_alignment = device.GetStorageBufferAlignment(),
};
host_info = Shader::HostTranslateInfo{
@@ -383,6 +384,7 @@ PipelineCache::PipelineCache(RasterizerVulkan& rasterizer_, const Device& device
driver_id == VK_DRIVER_ID_AMD_PROPRIETARY || driver_id == VK_DRIVER_ID_AMD_OPEN_SOURCE,
.support_snorm_render_buffer = true,
.support_viewport_index_layer = device.IsExtShaderViewportIndexLayerSupported(),
+ .min_ssbo_alignment = static_cast<u32>(device.GetStorageBufferAlignment()),
.support_geometry_shader_passthrough = device.IsNvGeometryShaderPassthroughSupported(),
.support_conditional_barrier = device.SupportsConditionalBarriers(),
};
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp
index 3be7837f4..146923db4 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.cpp
+++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp
@@ -22,11 +22,12 @@ namespace Vulkan {
MICROPROFILE_DECLARE(Vulkan_WaitForWorker);
-void Scheduler::CommandChunk::ExecuteAll(vk::CommandBuffer cmdbuf) {
+void Scheduler::CommandChunk::ExecuteAll(vk::CommandBuffer cmdbuf,
+ vk::CommandBuffer upload_cmdbuf) {
auto command = first;
while (command != nullptr) {
auto next = command->GetNext();
- command->Execute(cmdbuf);
+ command->Execute(cmdbuf, upload_cmdbuf);
command->~Command();
command = next;
}
@@ -180,7 +181,7 @@ void Scheduler::WorkerThread(std::stop_token stop_token) {
// Perform the work, tracking whether the chunk was a submission
// before executing.
const bool has_submit = work->HasSubmit();
- work->ExecuteAll(current_cmdbuf);
+ work->ExecuteAll(current_cmdbuf, current_upload_cmdbuf);
// If the chunk was a submission, reallocate the command buffer.
if (has_submit) {
@@ -205,6 +206,13 @@ void Scheduler::AllocateWorkerCommandBuffer() {
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
.pInheritanceInfo = nullptr,
});
+ current_upload_cmdbuf = vk::CommandBuffer(command_pool->Commit(), device.GetDispatchLoader());
+ current_upload_cmdbuf.Begin({
+ .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
+ .pNext = nullptr,
+ .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
+ .pInheritanceInfo = nullptr,
+ });
}
u64 Scheduler::SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) {
@@ -212,7 +220,17 @@ u64 Scheduler::SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_se
InvalidateState();
const u64 signal_value = master_semaphore->NextTick();
- Record([signal_semaphore, wait_semaphore, signal_value, this](vk::CommandBuffer cmdbuf) {
+ RecordWithUploadBuffer([signal_semaphore, wait_semaphore, signal_value,
+ this](vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf) {
+ static constexpr VkMemoryBarrier WRITE_BARRIER{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT,
+ };
+ upload_cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT,
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, WRITE_BARRIER);
+ upload_cmdbuf.End();
cmdbuf.End();
if (on_submit) {
@@ -221,7 +239,7 @@ u64 Scheduler::SubmitExecution(VkSemaphore signal_semaphore, VkSemaphore wait_se
std::scoped_lock lock{submit_mutex};
switch (const VkResult result = master_semaphore->SubmitQueue(
- cmdbuf, signal_semaphore, wait_semaphore, signal_value)) {
+ cmdbuf, upload_cmdbuf, signal_semaphore, wait_semaphore, signal_value)) {
case VK_SUCCESS:
break;
case VK_ERROR_DEVICE_LOST:
diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h
index da03803aa..f8d8ca80a 100644
--- a/src/video_core/renderer_vulkan/vk_scheduler.h
+++ b/src/video_core/renderer_vulkan/vk_scheduler.h
@@ -80,7 +80,8 @@ public:
/// Send work to a separate thread.
template <typename T>
- void Record(T&& command) {
+ requires std::is_invocable_v<T, vk::CommandBuffer, vk::CommandBuffer>
+ void RecordWithUploadBuffer(T&& command) {
if (chunk->Record(command)) {
return;
}
@@ -88,6 +89,15 @@ public:
(void)chunk->Record(command);
}
+ template <typename T>
+ requires std::is_invocable_v<T, vk::CommandBuffer>
+ void Record(T&& c) {
+ this->RecordWithUploadBuffer(
+ [command = std::move(c)](vk::CommandBuffer cmdbuf, vk::CommandBuffer) {
+ command(cmdbuf);
+ });
+ }
+
/// Returns the current command buffer tick.
[[nodiscard]] u64 CurrentTick() const noexcept {
return master_semaphore->CurrentTick();
@@ -119,7 +129,7 @@ private:
public:
virtual ~Command() = default;
- virtual void Execute(vk::CommandBuffer cmdbuf) const = 0;
+ virtual void Execute(vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf) const = 0;
Command* GetNext() const {
return next;
@@ -142,8 +152,8 @@ private:
TypedCommand(TypedCommand&&) = delete;
TypedCommand& operator=(TypedCommand&&) = delete;
- void Execute(vk::CommandBuffer cmdbuf) const override {
- command(cmdbuf);
+ void Execute(vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf) const override {
+ command(cmdbuf, upload_cmdbuf);
}
private:
@@ -152,7 +162,7 @@ private:
class CommandChunk final {
public:
- void ExecuteAll(vk::CommandBuffer cmdbuf);
+ void ExecuteAll(vk::CommandBuffer cmdbuf, vk::CommandBuffer upload_cmdbuf);
template <typename T>
bool Record(T& command) {
@@ -228,6 +238,7 @@ private:
VideoCommon::QueryCacheBase<QueryCacheParams>* query_cache = nullptr;
vk::CommandBuffer current_cmdbuf;
+ vk::CommandBuffer current_upload_cmdbuf;
std::unique_ptr<CommandChunk> chunk;
std::function<void()> on_submit;
diff --git a/src/video_core/renderer_vulkan/vk_smaa.cpp b/src/video_core/renderer_vulkan/vk_smaa.cpp
index 5efd7d66e..70644ea82 100644
--- a/src/video_core/renderer_vulkan/vk_smaa.cpp
+++ b/src/video_core/renderer_vulkan/vk_smaa.cpp
@@ -672,7 +672,7 @@ void SMAA::UploadImages(Scheduler& scheduler) {
UploadImage(m_device, m_allocator, scheduler, m_static_images[Search], search_extent,
VK_FORMAT_R8_UNORM, ARRAY_TO_SPAN(searchTexBytes));
- scheduler.Record([&](vk::CommandBuffer& cmdbuf) {
+ scheduler.Record([&](vk::CommandBuffer cmdbuf) {
for (auto& images : m_dynamic_images) {
for (size_t i = 0; i < MaxDynamicImage; i++) {
ClearColorImage(cmdbuf, *images.images[i]);
@@ -707,7 +707,7 @@ VkImageView SMAA::Draw(Scheduler& scheduler, size_t image_index, VkImage source_
UpdateDescriptorSets(source_image_view, image_index);
scheduler.RequestOutsideRenderPassOperationContext();
- scheduler.Record([=, this](vk::CommandBuffer& cmdbuf) {
+ scheduler.Record([=, this](vk::CommandBuffer cmdbuf) {
TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL);
TransitionImageLayout(cmdbuf, edges_image, VK_IMAGE_LAYOUT_GENERAL);
BeginRenderPass(cmdbuf, m_renderpasses[EdgeDetection], edge_detection_framebuffer,
diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
index d3deb9072..f63a20327 100644
--- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
+++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.h
@@ -36,6 +36,10 @@ public:
StagingBufferRef Request(size_t size, MemoryUsage usage, bool deferred = false);
void FreeDeferred(StagingBufferRef& ref);
+ [[nodiscard]] VkBuffer StreamBuf() const noexcept {
+ return *stream_buffer;
+ }
+
void TickFrame();
private:
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index de34f6d49..5dbec2e62 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -1785,8 +1785,22 @@ ImageView::ImageView(TextureCacheRuntime&, const VideoCommon::ImageInfo& info,
: VideoCommon::ImageViewBase{info, view_info, gpu_addr_},
buffer_size{VideoCommon::CalculateGuestSizeInBytes(info)} {}
-ImageView::ImageView(TextureCacheRuntime&, const VideoCommon::NullImageViewParams& params)
- : VideoCommon::ImageViewBase{params} {}
+ImageView::ImageView(TextureCacheRuntime& runtime, const VideoCommon::NullImageViewParams& params)
+ : VideoCommon::ImageViewBase{params}, device{&runtime.device} {
+ if (device->HasNullDescriptor()) {
+ return;
+ }
+
+ // Handle fallback for devices without nullDescriptor
+ ImageInfo info{};
+ info.format = PixelFormat::A8B8G8R8_UNORM;
+
+ null_image = MakeImage(*device, runtime.memory_allocator, info, {});
+ image_handle = *null_image;
+ for (u32 i = 0; i < Shader::NUM_TEXTURE_TYPES; i++) {
+ image_views[i] = MakeView(VK_FORMAT_A8B8G8R8_UNORM_PACK32, VK_IMAGE_ASPECT_COLOR_BIT);
+ }
+}
ImageView::~ImageView() = default;
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index 7a0807709..edf5d7635 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -267,6 +267,7 @@ private:
vk::ImageView depth_view;
vk::ImageView stencil_view;
vk::ImageView color_view;
+ vk::Image null_image;
VkImage image_handle = VK_NULL_HANDLE;
VkImageView render_target = VK_NULL_HANDLE;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
diff --git a/src/video_core/texture_cache/slot_vector.h b/src/video_core/texture_cache/slot_vector.h
index 9df6a2903..3ffa2a661 100644
--- a/src/video_core/texture_cache/slot_vector.h
+++ b/src/video_core/texture_cache/slot_vector.h
@@ -138,6 +138,10 @@ public:
return Iterator(this, SlotId{SlotId::INVALID_INDEX});
}
+ [[nodiscard]] size_t size() const noexcept {
+ return values_capacity - free_list.size();
+ }
+
private:
struct NonTrivialDummy {
NonTrivialDummy() noexcept {}
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 6900b8ffa..188ceeed7 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -847,11 +847,41 @@ std::string Device::GetDriverName() const {
case VK_DRIVER_ID_NVIDIA_PROPRIETARY:
return "NVIDIA";
case VK_DRIVER_ID_INTEL_PROPRIETARY_WINDOWS:
- return "INTEL";
+ return "Intel";
case VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA:
return "ANV";
+ case VK_DRIVER_ID_IMAGINATION_PROPRIETARY:
+ return "PowerVR";
+ case VK_DRIVER_ID_QUALCOMM_PROPRIETARY:
+ return "Qualcomm";
+ case VK_DRIVER_ID_ARM_PROPRIETARY:
+ return "Mali";
+ case VK_DRIVER_ID_GOOGLE_SWIFTSHADER:
+ return "SwiftShader";
+ case VK_DRIVER_ID_BROADCOM_PROPRIETARY:
+ return "Broadcom";
case VK_DRIVER_ID_MESA_LLVMPIPE:
- return "LAVAPIPE";
+ return "Lavapipe";
+ case VK_DRIVER_ID_MOLTENVK:
+ return "MoltenVK";
+ case VK_DRIVER_ID_VERISILICON_PROPRIETARY:
+ return "Vivante";
+ case VK_DRIVER_ID_MESA_TURNIP:
+ return "Turnip";
+ case VK_DRIVER_ID_MESA_V3DV:
+ return "V3DV";
+ case VK_DRIVER_ID_MESA_PANVK:
+ return "PanVK";
+ case VK_DRIVER_ID_MESA_VENUS:
+ return "Venus";
+ case VK_DRIVER_ID_MESA_DOZEN:
+ return "Dozen";
+ case VK_DRIVER_ID_MESA_NVK:
+ return "NVK";
+ case VK_DRIVER_ID_IMAGINATION_OPEN_SOURCE_MESA:
+ return "PVR";
+ // case VK_DRIVER_ID_MESA_AGXV:
+ // return "Asahi";
default:
return properties.driver.driverName;
}
@@ -869,7 +899,8 @@ bool Device::ShouldBoostClocks() const {
driver_id == VK_DRIVER_ID_INTEL_OPEN_SOURCE_MESA ||
driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY || driver_id == VK_DRIVER_ID_MESA_TURNIP;
- const bool is_steam_deck = vendor_id == 0x1002 && device_id == 0x163F;
+ const bool is_steam_deck = (vendor_id == 0x1002 && device_id == 0x163F) ||
+ (vendor_id == 0x1002 && device_id == 0x1435);
const bool is_debugging = this->HasDebuggingToolAttached();
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h
index 0487cd3b6..a0c70797f 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.h
+++ b/src/video_core/vulkan_common/vulkan_wrapper.h
@@ -1101,6 +1101,10 @@ public:
return &handle;
}
+ VkCommandBuffer operator*() const noexcept {
+ return handle;
+ }
+
void Begin(const VkCommandBufferBeginInfo& begin_info) const {
Check(dld->vkBeginCommandBuffer(handle, &begin_info));
}
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 90278052a..f3ad2214b 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -386,7 +386,7 @@ if (NOT WIN32)
target_include_directories(yuzu PRIVATE ${Qt${QT_MAJOR_VERSION}Gui_PRIVATE_INCLUDE_DIRS})
endif()
if (UNIX AND NOT APPLE)
- target_link_libraries(yuzu PRIVATE Qt${QT_MAJOR_VERSION}::DBus)
+ target_link_libraries(yuzu PRIVATE Qt${QT_MAJOR_VERSION}::DBus gamemode)
endif()
target_compile_definitions(yuzu PRIVATE
diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp
index 2afa72140..ed5750155 100644
--- a/src/yuzu/bootmanager.cpp
+++ b/src/yuzu/bootmanager.cpp
@@ -30,7 +30,6 @@
#include <QSize>
#include <QStringLiteral>
#include <QSurfaceFormat>
-#include <QTimer>
#include <QWindow>
#include <QtCore/qobjectdefs.h>
@@ -66,6 +65,8 @@ class QObject;
class QPaintEngine;
class QSurface;
+constexpr int default_mouse_constrain_timeout = 10;
+
EmuThread::EmuThread(Core::System& system) : m_system{system} {}
EmuThread::~EmuThread() = default;
@@ -304,6 +305,9 @@ GRenderWindow::GRenderWindow(GMainWindow* parent, EmuThread* emu_thread_,
Qt::QueuedConnection);
connect(this, &GRenderWindow::ExitSignal, parent, &GMainWindow::OnExit, Qt::QueuedConnection);
connect(this, &GRenderWindow::TasPlaybackStateChanged, parent, &GMainWindow::OnTasStateChanged);
+
+ mouse_constrain_timer.setInterval(default_mouse_constrain_timeout);
+ connect(&mouse_constrain_timer, &QTimer::timeout, this, &GRenderWindow::ConstrainMouse);
}
void GRenderWindow::ExecuteProgram(std::size_t program_index) {
@@ -393,6 +397,22 @@ void GRenderWindow::closeEvent(QCloseEvent* event) {
QWidget::closeEvent(event);
}
+void GRenderWindow::leaveEvent(QEvent* event) {
+ if (Settings::values.mouse_panning) {
+ const QRect& rect = QWidget::geometry();
+ QPoint position = QCursor::pos();
+
+ qint32 x = qBound(rect.left(), position.x(), rect.right());
+ qint32 y = qBound(rect.top(), position.y(), rect.bottom());
+ // Only start the timer if the mouse has left the window bound.
+ // The leave event is also triggered when the window looses focus.
+ if (x != position.x() || y != position.y()) {
+ mouse_constrain_timer.start();
+ }
+ event->accept();
+ }
+}
+
int GRenderWindow::QtKeyToSwitchKey(Qt::Key qt_key) {
static constexpr std::array<std::pair<Qt::Key, Settings::NativeKeyboard::Keys>, 106> key_map = {
std::pair<Qt::Key, Settings::NativeKeyboard::Keys>{Qt::Key_A, Settings::NativeKeyboard::A},
@@ -658,10 +678,19 @@ void GRenderWindow::mouseMoveEvent(QMouseEvent* event) {
input_subsystem->GetMouse()->TouchMove(touch_x, touch_y);
input_subsystem->GetMouse()->Move(pos.x(), pos.y(), center_x, center_y);
+ // Center mouse for mouse panning
if (Settings::values.mouse_panning && !Settings::values.mouse_enabled) {
QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
}
+ // Constrain mouse for mouse emulation with mouse panning
+ if (Settings::values.mouse_panning && Settings::values.mouse_enabled) {
+ const auto [clamped_mouse_x, clamped_mouse_y] = ClipToTouchScreen(x, y);
+ QCursor::setPos(mapToGlobal(
+ QPoint{static_cast<int>(clamped_mouse_x), static_cast<int>(clamped_mouse_y)}));
+ }
+
+ mouse_constrain_timer.stop();
emit MouseActivity();
}
@@ -675,6 +704,31 @@ void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) {
input_subsystem->GetMouse()->ReleaseButton(button);
}
+void GRenderWindow::ConstrainMouse() {
+ if (emu_thread == nullptr || !Settings::values.mouse_panning) {
+ mouse_constrain_timer.stop();
+ return;
+ }
+ if (!this->isActiveWindow()) {
+ mouse_constrain_timer.stop();
+ return;
+ }
+
+ if (Settings::values.mouse_enabled) {
+ const auto pos = mapFromGlobal(QCursor::pos());
+ const int new_pos_x = std::clamp(pos.x(), 0, width());
+ const int new_pos_y = std::clamp(pos.y(), 0, height());
+
+ QCursor::setPos(mapToGlobal(QPoint{new_pos_x, new_pos_y}));
+ return;
+ }
+
+ const int center_x = width() / 2;
+ const int center_y = height() / 2;
+
+ QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
+}
+
void GRenderWindow::wheelEvent(QWheelEvent* event) {
const int x = event->angleDelta().x();
const int y = event->angleDelta().y();
diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h
index 87b23df12..60edd464c 100644
--- a/src/yuzu/bootmanager.h
+++ b/src/yuzu/bootmanager.h
@@ -17,6 +17,7 @@
#include <QString>
#include <QStringList>
#include <QThread>
+#include <QTimer>
#include <QWidget>
#include <qglobal.h>
#include <qnamespace.h>
@@ -38,7 +39,6 @@ class QMouseEvent;
class QObject;
class QResizeEvent;
class QShowEvent;
-class QTimer;
class QTouchEvent;
class QWheelEvent;
@@ -166,6 +166,7 @@ public:
std::pair<u32, u32> ScaleTouch(const QPointF& pos) const;
void closeEvent(QCloseEvent* event) override;
+ void leaveEvent(QEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
@@ -229,6 +230,7 @@ private:
void TouchBeginEvent(const QTouchEvent* event);
void TouchUpdateEvent(const QTouchEvent* event);
void TouchEndEvent();
+ void ConstrainMouse();
void RequestCameraCapture();
void OnCameraCapture(int requestId, const QImage& img);
@@ -268,6 +270,8 @@ private:
std::unique_ptr<QTimer> camera_timer;
#endif
+ QTimer mouse_constrain_timer;
+
Core::System& system;
protected:
diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp
index ef421c754..1010038b7 100644
--- a/src/yuzu/configuration/configure_debug.cpp
+++ b/src/yuzu/configuration/configure_debug.cpp
@@ -51,6 +51,8 @@ void ConfigureDebug::SetConfiguration() {
ui->enable_all_controllers->setChecked(Settings::values.enable_all_controllers.GetValue());
ui->enable_renderdoc_hotkey->setEnabled(runtime_lock);
ui->enable_renderdoc_hotkey->setChecked(Settings::values.enable_renderdoc_hotkey.GetValue());
+ ui->disable_buffer_reorder->setEnabled(runtime_lock);
+ ui->disable_buffer_reorder->setChecked(Settings::values.disable_buffer_reorder.GetValue());
ui->enable_graphics_debugging->setEnabled(runtime_lock);
ui->enable_graphics_debugging->setChecked(Settings::values.renderer_debug.GetValue());
ui->enable_shader_feedback->setEnabled(runtime_lock);
@@ -96,6 +98,7 @@ void ConfigureDebug::ApplyConfiguration() {
Settings::values.enable_all_controllers = ui->enable_all_controllers->isChecked();
Settings::values.renderer_debug = ui->enable_graphics_debugging->isChecked();
Settings::values.enable_renderdoc_hotkey = ui->enable_renderdoc_hotkey->isChecked();
+ Settings::values.disable_buffer_reorder = ui->disable_buffer_reorder->isChecked();
Settings::values.renderer_shader_feedback = ui->enable_shader_feedback->isChecked();
Settings::values.cpu_debug_mode = ui->enable_cpu_debugging->isChecked();
Settings::values.enable_nsight_aftermath = ui->enable_nsight_aftermath->isChecked();
diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui
index 76fe98924..22b51f39c 100644
--- a/src/yuzu/configuration/configure_debug.ui
+++ b/src/yuzu/configuration/configure_debug.ui
@@ -271,19 +271,6 @@
</widget>
</item>
<item row="8" column="0">
- <widget class="QCheckBox" name="disable_macro_hle">
- <property name="enabled">
- <bool>true</bool>
- </property>
- <property name="toolTip">
- <string>When checked, it disables the macro HLE functions. Enabling this makes games run slower</string>
- </property>
- <property name="text">
- <string>Disable Macro HLE</string>
- </property>
- </widget>
- </item>
- <item row="7" column="0">
<widget class="QCheckBox" name="dump_macros">
<property name="enabled">
<bool>true</bool>
@@ -306,17 +293,27 @@
</property>
</widget>
</item>
- <item row="2" column="0">
- <widget class="QCheckBox" name="enable_shader_feedback">
+ <item row="6" column="0">
+ <widget class="QCheckBox" name="dump_shaders">
+ <property name="enabled">
+ <bool>true</bool>
+ </property>
<property name="toolTip">
- <string>When checked, yuzu will log statistics about the compiled pipeline cache</string>
+ <string>When checked, it will dump all the original assembler shaders from the disk shader cache or game as found</string>
</property>
<property name="text">
- <string>Enable Shader Feedback</string>
+ <string>Dump Game Shaders</string>
</property>
</widget>
</item>
- <item row="6" column="0">
+ <item row="1" column="0">
+ <widget class="QCheckBox" name="enable_renderdoc_hotkey">
+ <property name="text">
+ <string>Enable Renderdoc Hotkey</string>
+ </property>
+ </widget>
+ </item>
+ <item row="7" column="0">
<widget class="QCheckBox" name="disable_macro_jit">
<property name="enabled">
<bool>true</bool>
@@ -330,20 +327,17 @@
</widget>
</item>
<item row="9" column="0">
- <spacer name="verticalSpacer_5">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
+ <widget class="QCheckBox" name="disable_macro_hle">
+ <property name="enabled">
+ <bool>true</bool>
</property>
- <property name="sizeType">
- <enum>QSizePolicy::Preferred</enum>
+ <property name="toolTip">
+ <string>When checked, it disables the macro HLE functions. Enabling this makes games run slower</string>
</property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>0</height>
- </size>
+ <property name="text">
+ <string>Disable Macro HLE</string>
</property>
- </spacer>
+ </widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="enable_graphics_debugging">
@@ -358,23 +352,39 @@
</property>
</widget>
</item>
- <item row="5" column="0">
- <widget class="QCheckBox" name="dump_shaders">
- <property name="enabled">
- <bool>true</bool>
+ <item row="10" column="0">
+ <spacer name="verticalSpacer_5">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Preferred</enum>
</property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item row="2" column="0">
+ <widget class="QCheckBox" name="enable_shader_feedback">
<property name="toolTip">
- <string>When checked, it will dump all the original assembler shaders from the disk shader cache or game as found</string>
+ <string>When checked, yuzu will log statistics about the compiled pipeline cache</string>
</property>
<property name="text">
- <string>Dump Game Shaders</string>
+ <string>Enable Shader Feedback</string>
</property>
</widget>
</item>
- <item row="1" column="0">
- <widget class="QCheckBox" name="enable_renderdoc_hotkey">
+ <item row="5" column="0">
+ <widget class="QCheckBox" name="disable_buffer_reorder">
+ <property name="toolTip">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When checked, disables reording of mapped memory uploads which allows to associate uploads with specific draws. May reduce performance in some cases.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
<property name="text">
- <string>Enable Renderdoc Hotkey</string>
+ <string>Disable Buffer Reorder</string>
</property>
</widget>
</item>
diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp
index c727fadd1..701b895e7 100644
--- a/src/yuzu/configuration/configure_general.cpp
+++ b/src/yuzu/configuration/configure_general.cpp
@@ -36,12 +36,29 @@ ConfigureGeneral::~ConfigureGeneral() = default;
void ConfigureGeneral::SetConfiguration() {}
void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
- QLayout& layout = *ui->general_widget->layout();
+ QLayout& general_layout = *ui->general_widget->layout();
+ QLayout& linux_layout = *ui->linux_widget->layout();
- std::map<u32, QWidget*> hold{};
+ std::map<u32, QWidget*> general_hold{};
+ std::map<u32, QWidget*> linux_hold{};
- for (const auto setting :
- UISettings::values.linkage.by_category[Settings::Category::UiGeneral]) {
+ std::vector<Settings::BasicSetting*> settings;
+
+ auto push = [&settings](auto& list) {
+ for (auto setting : list) {
+ settings.push_back(setting);
+ }
+ };
+
+ push(UISettings::values.linkage.by_category[Settings::Category::UiGeneral]);
+ push(Settings::values.linkage.by_category[Settings::Category::Linux]);
+
+ // Only show Linux group on Unix
+#ifndef __unix__
+ ui->LinuxGroupBox->setVisible(false);
+#endif
+
+ for (const auto setting : settings) {
auto* widget = builder.BuildWidget(setting, apply_funcs);
if (widget == nullptr) {
@@ -52,11 +69,23 @@ void ConfigureGeneral::Setup(const ConfigurationShared::Builder& builder) {
continue;
}
- hold.emplace(setting->Id(), widget);
+ switch (setting->GetCategory()) {
+ case Settings::Category::UiGeneral:
+ general_hold.emplace(setting->Id(), widget);
+ break;
+ case Settings::Category::Linux:
+ linux_hold.emplace(setting->Id(), widget);
+ break;
+ default:
+ widget->deleteLater();
+ }
}
- for (const auto& [id, widget] : hold) {
- layout.addWidget(widget);
+ for (const auto& [id, widget] : general_hold) {
+ general_layout.addWidget(widget);
+ }
+ for (const auto& [id, widget] : linux_hold) {
+ linux_layout.addWidget(widget);
}
}
diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui
index a10e7d3a5..ef20891a3 100644
--- a/src/yuzu/configuration/configure_general.ui
+++ b/src/yuzu/configuration/configure_general.ui
@@ -47,6 +47,33 @@
</widget>
</item>
<item>
+ <widget class="QGroupBox" name="LinuxGroupBox">
+ <property name="title">
+ <string>Linux</string>
+ </property>
+ <layout class="QVBoxLayout" name="LinuxVerticalLayout_1">
+ <item>
+ <widget class="QWidget" name="linux_widget" native="true">
+ <layout class="QVBoxLayout" name="LinuxVerticalLayout_2">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
diff --git a/src/yuzu/configuration/configure_system.ui b/src/yuzu/configuration/configure_system.ui
index 2a735836e..04b771129 100644
--- a/src/yuzu/configuration/configure_system.ui
+++ b/src/yuzu/configuration/configure_system.ui
@@ -57,7 +57,7 @@
</widget>
</item>
<item>
- <widget class="QGroupBox" name="groupBox">
+ <widget class="QGroupBox" name="coreGroup">
<property name="title">
<string>Core</string>
</property>
diff --git a/src/yuzu/configuration/shared_translation.cpp b/src/yuzu/configuration/shared_translation.cpp
index daf0e1663..7e908924c 100644
--- a/src/yuzu/configuration/shared_translation.cpp
+++ b/src/yuzu/configuration/shared_translation.cpp
@@ -177,6 +177,9 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
INSERT(UISettings, controller_applet_disabled, tr("Disable controller applet"),
QStringLiteral());
+ // Linux
+ INSERT(Settings, enable_gamemode, tr("Enable Gamemode"), QStringLiteral());
+
// Ui Debugging
// Ui Multiplayer
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index defe45198..10c788290 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -17,6 +17,7 @@
#ifdef __unix__
#include <csignal>
#include <sys/socket.h>
+#include "common/linux/gamemode.h"
#endif
#include <boost/container/flat_set.hpp>
@@ -47,6 +48,7 @@
#include "core/hle/service/am/applet_ae.h"
#include "core/hle/service/am/applet_oe.h"
#include "core/hle/service/am/applets/applets.h"
+#include "core/hle/service/set/set_sys.h"
#include "yuzu/multiplayer/state.h"
#include "yuzu/util/controller_navigation.h"
@@ -186,7 +188,6 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
#endif
constexpr int default_mouse_hide_timeout = 2500;
-constexpr int default_mouse_center_timeout = 10;
constexpr int default_input_update_timeout = 1;
constexpr size_t CopyBufferSize = 1_MiB;
@@ -319,6 +320,7 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
provider{std::make_unique<FileSys::ManualContentProvider>()} {
#ifdef __unix__
SetupSigInterrupts();
+ SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
#endif
system->Initialize();
@@ -436,9 +438,6 @@ GMainWindow::GMainWindow(std::unique_ptr<QtConfig> config_, bool has_broken_vulk
connect(&mouse_hide_timer, &QTimer::timeout, this, &GMainWindow::HideMouseCursor);
connect(ui->menubar, &QMenuBar::hovered, this, &GMainWindow::ShowMouseCursor);
- mouse_center_timer.setInterval(default_mouse_center_timeout);
- connect(&mouse_center_timer, &QTimer::timeout, this, &GMainWindow::CenterMouseCursor);
-
update_input_timer.setInterval(default_input_update_timeout);
connect(&update_input_timer, &QTimer::timeout, this, &GMainWindow::UpdateInputDrivers);
update_input_timer.start();
@@ -1048,7 +1047,12 @@ void GMainWindow::InitializeWidgets() {
statusBar()->addPermanentWidget(label);
}
- // TODO (flTobi): Add the widget when multiplayer is fully implemented
+ firmware_label = new QLabel();
+ firmware_label->setObjectName(QStringLiteral("FirmwareLabel"));
+ firmware_label->setVisible(false);
+ firmware_label->setFocusPolicy(Qt::NoFocus);
+ statusBar()->addPermanentWidget(firmware_label);
+
statusBar()->addPermanentWidget(multiplayer_state->GetStatusText(), 0);
statusBar()->addPermanentWidget(multiplayer_state->GetStatusIcon(), 0);
@@ -1370,14 +1374,6 @@ void GMainWindow::InitializeHotkeys() {
}
});
connect_shortcut(QStringLiteral("Toggle Mouse Panning"), [&] {
- if (Settings::values.mouse_enabled) {
- Settings::values.mouse_panning = false;
- QMessageBox::warning(
- this, tr("Emulated mouse is enabled"),
- tr("Real mouse input and mouse panning are incompatible. Please disable the "
- "emulated mouse in input advanced settings to allow mouse panning."));
- return;
- }
Settings::values.mouse_panning = !Settings::values.mouse_panning;
if (Settings::values.mouse_panning) {
render_window->installEventFilter(render_window);
@@ -2126,6 +2122,10 @@ void GMainWindow::OnEmulationStopped() {
discord_rpc->Update();
+#ifdef __unix__
+ Common::Linux::StopGamemode();
+#endif
+
// The emulation is stopped, so closing the window or not does not matter anymore
disconnect(render_window, &GRenderWindow::Closed, this, &GMainWindow::OnStopGame);
@@ -2165,6 +2165,10 @@ void GMainWindow::OnEmulationStopped() {
emu_frametime_label->setVisible(false);
renderer_status_button->setEnabled(!UISettings::values.has_broken_vulkan);
+ if (!firmware_label->text().isEmpty()) {
+ firmware_label->setVisible(true);
+ }
+
current_game_path.clear();
// When closing the game, destroy the GLWindow to clear the context after the game is closed
@@ -3504,6 +3508,10 @@ void GMainWindow::OnStartGame() {
play_time_manager->Start();
discord_rpc->Update();
+
+#ifdef __unix__
+ Common::Linux::StartGamemode();
+#endif
}
void GMainWindow::OnRestartGame() {
@@ -3524,6 +3532,10 @@ void GMainWindow::OnPauseGame() {
play_time_manager->Stop();
UpdateMenuState();
AllowOSSleep();
+
+#ifdef __unix__
+ Common::Linux::StopGamemode();
+#endif
}
void GMainWindow::OnPauseContinueGame() {
@@ -3805,6 +3817,9 @@ void GMainWindow::OnConfigure() {
const auto old_theme = UISettings::values.theme;
const bool old_discord_presence = UISettings::values.enable_discord_presence.GetValue();
const auto old_language_index = Settings::values.language_index.GetValue();
+#ifdef __unix__
+ const bool old_gamemode = Settings::values.enable_gamemode.GetValue();
+#endif
Settings::SetConfiguringGlobal(true);
ConfigureDialog configure_dialog(this, hotkey_registry, input_subsystem.get(),
@@ -3866,6 +3881,11 @@ void GMainWindow::OnConfigure() {
if (UISettings::values.enable_discord_presence.GetValue() != old_discord_presence) {
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
}
+#ifdef __unix__
+ if (Settings::values.enable_gamemode.GetValue() != old_gamemode) {
+ SetGamemodeEnabled(Settings::values.enable_gamemode.GetValue());
+ }
+#endif
if (!multiplayer_state->IsHostingPublicRoom()) {
multiplayer_state->UpdateCredentials();
@@ -4591,6 +4611,7 @@ void GMainWindow::UpdateStatusBar() {
emu_speed_label->setVisible(!Settings::values.use_multi_core.GetValue());
game_fps_label->setVisible(true);
emu_frametime_label->setVisible(true);
+ firmware_label->setVisible(false);
}
void GMainWindow::UpdateGPUAccuracyButton() {
@@ -4700,26 +4721,10 @@ void GMainWindow::ShowMouseCursor() {
}
}
-void GMainWindow::CenterMouseCursor() {
- if (emu_thread == nullptr || !Settings::values.mouse_panning) {
- mouse_center_timer.stop();
- return;
- }
- if (!this->isActiveWindow()) {
- mouse_center_timer.stop();
- return;
- }
- const int center_x = render_window->width() / 2;
- const int center_y = render_window->height() / 2;
-
- QCursor::setPos(mapToGlobal(QPoint{center_x, center_y}));
-}
-
void GMainWindow::OnMouseActivity() {
if (!Settings::values.mouse_panning) {
ShowMouseCursor();
}
- mouse_center_timer.stop();
}
void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
@@ -4810,6 +4815,8 @@ void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) {
"games."));
}
+ SetFirmwareVersion();
+
if (behavior == ReinitializeKeyBehavior::Warning) {
game_list->PopulateAsync(UISettings::values.game_dirs);
}
@@ -4837,7 +4844,7 @@ bool GMainWindow::CheckSystemArchiveDecryption() {
}
bool GMainWindow::CheckFirmwarePresence() {
- constexpr u64 MiiEditId = 0x0100000000001009ull;
+ constexpr u64 MiiEditId = static_cast<u64>(Service::AM::Applets::AppletProgramId::MiiEdit);
auto bis_system = system->GetFileSystemController().GetSystemNANDContents();
if (!bis_system) {
@@ -4852,6 +4859,28 @@ bool GMainWindow::CheckFirmwarePresence() {
return true;
}
+void GMainWindow::SetFirmwareVersion() {
+ Service::Set::FirmwareVersionFormat firmware_data{};
+ const auto result = Service::Set::GetFirmwareVersionImpl(
+ firmware_data, *system, Service::Set::GetFirmwareVersionType::Version2);
+
+ if (result.IsError() || !CheckFirmwarePresence()) {
+ LOG_INFO(Frontend, "Installed firmware: No firmware available");
+ firmware_label->setVisible(false);
+ return;
+ }
+
+ firmware_label->setVisible(true);
+
+ const std::string display_version(firmware_data.display_version.data());
+ const std::string display_title(firmware_data.display_title.data());
+
+ LOG_INFO(Frontend, "Installed firmware: {}", display_title);
+
+ firmware_label->setText(QString::fromStdString(display_version));
+ firmware_label->setToolTip(QString::fromStdString(display_title));
+}
+
bool GMainWindow::SelectRomFSDumpTarget(const FileSys::ContentProvider& installed, u64 program_id,
u64* selected_title_id, u8* selected_content_record_type) {
using ContentInfo = std::tuple<u64, FileSys::TitleType, FileSys::ContentRecordType>;
@@ -4996,22 +5025,6 @@ void GMainWindow::dragMoveEvent(QDragMoveEvent* event) {
AcceptDropEvent(event);
}
-void GMainWindow::leaveEvent(QEvent* event) {
- if (Settings::values.mouse_panning) {
- const QRect& rect = geometry();
- QPoint position = QCursor::pos();
-
- qint32 x = qBound(rect.left(), position.x(), rect.right());
- qint32 y = qBound(rect.top(), position.y(), rect.bottom());
- // Only start the timer if the mouse has left the window bound.
- // The leave event is also triggered when the window looses focus.
- if (x != position.x() || y != position.y()) {
- mouse_center_timer.start();
- }
- event->accept();
- }
-}
-
bool GMainWindow::ConfirmChangeGame() {
if (emu_thread == nullptr)
return true;
@@ -5181,6 +5194,14 @@ void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) {
discord_rpc->Update();
}
+#ifdef __unix__
+void GMainWindow::SetGamemodeEnabled(bool state) {
+ if (emulation_running) {
+ Common::Linux::SetGamemodeState(state);
+ }
+}
+#endif
+
void GMainWindow::changeEvent(QEvent* event) {
#ifdef __unix__
// PaletteChange event appears to only reach so far into the GUI, explicitly asking to
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index c989c079d..530e445f9 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -340,6 +340,7 @@ private:
void SetupSigInterrupts();
static void HandleSigInterrupt(int);
void OnSigInterruptNotifierActivated();
+ void SetGamemodeEnabled(bool state);
#endif
private slots:
@@ -451,13 +452,13 @@ private:
void UpdateInputDrivers();
void HideMouseCursor();
void ShowMouseCursor();
- void CenterMouseCursor();
void OpenURL(const QUrl& url);
void LoadTranslation();
void OpenPerGameConfiguration(u64 title_id, const std::string& file_name);
bool CheckDarkMode();
bool CheckSystemArchiveDecryption();
bool CheckFirmwarePresence();
+ void SetFirmwareVersion();
void ConfigureFilesystemProvider(const std::string& filepath);
/**
* Open (or not) the right confirm dialog based on current setting and game exit lock
@@ -512,6 +513,7 @@ private:
QLabel* game_fps_label = nullptr;
QLabel* emu_frametime_label = nullptr;
QLabel* tas_label = nullptr;
+ QLabel* firmware_label = nullptr;
QPushButton* gpu_accuracy_button = nullptr;
QPushButton* renderer_status_button = nullptr;
QPushButton* dock_status_button = nullptr;
@@ -533,7 +535,6 @@ private:
bool auto_paused = false;
bool auto_muted = false;
QTimer mouse_hide_timer;
- QTimer mouse_center_timer;
QTimer update_input_timer;
QString startup_icon_theme;
@@ -590,5 +591,4 @@ protected:
void dropEvent(QDropEvent* event) override;
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
- void leaveEvent(QEvent* event) override;
};
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index 0416d5951..a81635fa4 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -63,6 +63,10 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
}
#endif
+#ifdef __unix__
+#include "common/linux/gamemode.h"
+#endif
+
static void PrintHelp(const char* argv0) {
std::cout << "Usage: " << argv0
<< " [options] <filename>\n"
@@ -425,6 +429,10 @@ int main(int argc, char** argv) {
exit(0);
});
+#ifdef __unix__
+ Common::Linux::StartGamemode();
+#endif
+
void(system.Run());
if (system.DebuggerEnabled()) {
system.InitializeDebugger();
@@ -436,6 +444,10 @@ int main(int argc, char** argv) {
void(system.Pause());
system.ShutdownMainProcess();
+#ifdef __unix__
+ Common::Linux::StopGamemode();
+#endif
+
detached_tasks.WaitForAllTasks();
return 0;
}