From 91abe7f7d07d840bcc8c45e3b4ed9e0ded110f56 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Fri, 13 Feb 2026 14:30:04 +0100 Subject: [PATCH 01/94] common: Add NATVIS to BitField class for better VS debugging (#1731) --- src/common/CMakeLists.txt | 1 + src/common/bit_field.natvis | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/common/bit_field.natvis diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index b7062e991..ac6728111 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -68,6 +68,7 @@ add_library(citra_common STATIC detached_tasks.cpp detached_tasks.h bit_field.h + bit_field.natvis bit_set.h bounded_threadsafe_queue.h cityhash.cpp diff --git a/src/common/bit_field.natvis b/src/common/bit_field.natvis new file mode 100644 index 000000000..2bdc0ce11 --- /dev/null +++ b/src/common/bit_field.natvis @@ -0,0 +1,28 @@ + + + + + + + + {((unsigned long long)storage >> $T1) & (((unsigned long long)1 << $T2) - 1)} + + + + ((unsigned long long)storage >> $T1) & (((unsigned long long)1 << $T2) - 1) + + + (long long)((((unsigned long long)storage >> $T1) & ((1ULL << $T2)-1)) & (1ULL << ($T2-1)) + ? -((1ULL << $T2) - (((unsigned long long)storage >> $T1) & ((1ULL << $T2)-1))) + : (((unsigned long long)storage >> $T1) & ((1ULL << $T2)-1))) + + + $T1 + + + $T2 + + storage + + + From 304db9173b9144fa900c2fe487545fae3425000d Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 16 Feb 2026 15:59:22 +0100 Subject: [PATCH 02/94] video_core: vulkan: Add disk shader cache (#1725) --- CMakeModules/GenerateSCMRev.cmake | 5 + .../utils/DiskShaderCacheProgress.kt | 6 +- src/android/app/src/main/jni/id_cache.cpp | 7 +- src/android/app/src/main/jni/native.cpp | 11 +- .../app/src/main/res/values/strings.xml | 2 +- src/citra_qt/bootmanager.cpp | 22 +- src/citra_qt/bootmanager.h | 5 +- src/citra_qt/citra_qt.cpp | 5 +- src/citra_qt/citra_qt.h | 2 +- src/citra_qt/game_list.cpp | 51 + src/citra_qt/loading_screen.cpp | 18 +- src/citra_qt/loading_screen.h | 8 +- src/common/CMakeLists.txt | 5 + src/common/hash.h | 14 +- src/common/zstd_compression.cpp | 7 +- src/common/zstd_compression.h | 9 + src/video_core/CMakeLists.txt | 2 + .../custom_textures/custom_tex_manager.cpp | 5 +- src/video_core/pica/shader_setup.h | 16 +- src/video_core/rasterizer_interface.h | 3 +- .../renderer_opengl/gl_rasterizer.cpp | 4 +- .../renderer_opengl/gl_shader_manager.cpp | 142 +- .../renderer_vulkan/vk_graphics_pipeline.cpp | 97 +- .../renderer_vulkan/vk_graphics_pipeline.h | 245 ++- src/video_core/renderer_vulkan/vk_instance.h | 8 +- .../renderer_vulkan/vk_pipeline_cache.cpp | 273 +-- .../renderer_vulkan/vk_pipeline_cache.h | 33 +- .../renderer_vulkan/vk_rasterizer.cpp | 111 +- .../renderer_vulkan/vk_shader_disk_cache.cpp | 1487 +++++++++++++++++ .../renderer_vulkan/vk_shader_disk_cache.h | 346 ++++ .../renderer_vulkan/vk_shader_util.cpp | 13 +- .../renderer_vulkan/vk_shader_util.h | 10 +- .../shader/generator/glsl_fs_shader_gen.cpp | 31 +- .../shader/generator/glsl_fs_shader_gen.h | 10 +- .../shader/generator/glsl_shader_gen.cpp | 64 +- .../shader/generator/glsl_shader_gen.h | 9 +- .../shader/generator/pica_fs_config.cpp | 98 +- .../shader/generator/pica_fs_config.h | 257 ++- src/video_core/shader/generator/profile.h | 58 +- .../shader/generator/shader_gen.cpp | 49 +- src/video_core/shader/generator/shader_gen.h | 114 +- .../shader/generator/spv_fs_shader_gen.cpp | 15 +- .../shader/generator/spv_fs_shader_gen.h | 4 +- 43 files changed, 3134 insertions(+), 547 deletions(-) create mode 100644 src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp create mode 100644 src/video_core/renderer_vulkan/vk_shader_disk_cache.h diff --git a/CMakeModules/GenerateSCMRev.cmake b/CMakeModules/GenerateSCMRev.cmake index 8d0f63cdc..377ca9693 100644 --- a/CMakeModules/GenerateSCMRev.cmake +++ b/CMakeModules/GenerateSCMRev.cmake @@ -11,6 +11,10 @@ set(HASH_FILES "${VIDEO_CORE}/renderer_opengl/gl_shader_util.h" "${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp" "${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h" + "${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.cpp" + "${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.h" + "${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.cpp" + "${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.h" "${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.cpp" "${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.h" "${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.cpp" @@ -19,6 +23,7 @@ set(HASH_FILES "${VIDEO_CORE}/shader/generator/glsl_shader_gen.h" "${VIDEO_CORE}/shader/generator/pica_fs_config.cpp" "${VIDEO_CORE}/shader/generator/pica_fs_config.h" + "${VIDEO_CORE}/shader/generator/profile.h" "${VIDEO_CORE}/shader/generator/shader_gen.cpp" "${VIDEO_CORE}/shader/generator/shader_gen.h" "${VIDEO_CORE}/shader/generator/shader_uniforms.cpp" diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt index a34924a9a..15ea56d3c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/DiskShaderCacheProgress.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -23,7 +23,7 @@ object DiskShaderCacheProgress { } @JvmStatic - fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int) { + fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int, obj: String) { val emulationActivity = NativeLibrary.sEmulationActivity.get() if (emulationActivity == null) { Log.error("[DiskShaderCacheProgress] EmulationActivity not present") @@ -40,7 +40,7 @@ object DiskShaderCacheProgress { ) LoadCallbackStage.Build -> emulationViewModel.updateProgress( - emulationActivity.getString(R.string.building_shaders), + emulationActivity.getString(R.string.building_shaders, obj ), progress, max ) diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index d7d4a109a..197e81aaa 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -207,9 +207,10 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { env->NewGlobalRef(env->FindClass("org/citra/citra_emu/utils/DiskShaderCacheProgress"))); jclass load_callback_stage_class = env->FindClass("org/citra/citra_emu/utils/DiskShaderCacheProgress$LoadCallbackStage"); - s_disk_cache_load_progress = env->GetStaticMethodID( - s_disk_cache_progress_class, "loadProgress", - "(Lorg/citra/citra_emu/utils/DiskShaderCacheProgress$LoadCallbackStage;II)V"); + s_disk_cache_load_progress = + env->GetStaticMethodID(s_disk_cache_progress_class, "loadProgress", + "(Lorg/citra/citra_emu/utils/" + "DiskShaderCacheProgress$LoadCallbackStage;IILjava/lang/String;)V"); s_compress_progress_method = env->GetStaticMethodID(s_native_library_class, "onCompressProgress", "(JJ)V"); // Initialize LoadCallbackStage map diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 508ad6de3..84a4fd3ce 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -130,12 +130,13 @@ static bool HandleCoreError(Core::System::ResultStatus result, const std::string env->NewStringUTF(details.c_str())) != JNI_FALSE; } -static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max) { +static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max, + const std::string& object) { JNIEnv* env = IDCache::GetEnvForThread(); env->CallStaticVoidMethod(IDCache::GetDiskCacheProgressClass(), IDCache::GetDiskCacheLoadProgress(), IDCache::GetJavaLoadCallbackStage(stage), static_cast(progress), - static_cast(max)); + static_cast(max), env->NewStringUTF(object.c_str())); } static Camera::NDK::Factory* g_ndk_factory{}; @@ -217,7 +218,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { true, shared_context); #elif ENABLE_VULKAN - window = std::make_unique(s_surface, vulkan_library); + window = std::make_unique(s_surface, vulkan_library, false); secondary_window = std::make_unique(s_secondary_surface, vulkan_library, true); #else @@ -267,7 +268,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { stop_run = false; pause_emulation = false; - LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); + LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); system.GPU().ApplyPerProgramSettings(program_id); @@ -275,7 +276,7 @@ static Core::System::ResultStatus RunCitra(const std::string& filepath) { system.GPU().Renderer().Rasterizer()->LoadDefaultDiskResources(stop_run, &LoadDiskCacheProgress); - LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); + LoadDiskCacheProgress(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); SCOPE_EXIT({ TryShutdown(); }); diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index c2c71a447..18959b3a8 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -571,7 +571,7 @@ Preparing Shaders - Building Shaders + Building %s Play diff --git a/src/citra_qt/bootmanager.cpp b/src/citra_qt/bootmanager.cpp index e13fc48b7..39649b8bf 100644 --- a/src/citra_qt/bootmanager.cpp +++ b/src/citra_qt/bootmanager.cpp @@ -66,29 +66,31 @@ void EmuThread::run() { const auto scope = core_context.Acquire(); if (Settings::values.custom_textures && Settings::values.preload_textures) { - emit LoadProgress(VideoCore::LoadCallbackStage::Preload, 0, 0); + emit LoadProgress(VideoCore::LoadCallbackStage::Preload, 0, 0, ""); system.CustomTexManager().PreloadTextures( - stop_run, [this](VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total) { emit LoadProgress(stage, value, total); }); + stop_run, + [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total, + const std::string& object) { emit LoadProgress(stage, value, total, object); }); } system.GPU().Renderer().Rasterizer()->SetSwitchDiskResourcesCallback( - [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { - emit SwitchDiskResources(stage, value, total); + [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total, + const std::string& object) { + emit SwitchDiskResources(stage, value, total, object); }); - emit LoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); + emit LoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); u64 program_id{}; system.GetAppLoader().ReadProgramId(program_id); system.GPU().ApplyPerProgramSettings(program_id); system.GPU().Renderer().Rasterizer()->LoadDefaultDiskResources( - stop_run, [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { - emit LoadProgress(stage, value, total); - }); + stop_run, + [this](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total, + const std::string& object) { emit LoadProgress(stage, value, total, object); }); - emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0); + emit LoadProgress(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); core_context.MakeCurrent(); diff --git a/src/citra_qt/bootmanager.h b/src/citra_qt/bootmanager.h index 8082b5590..118959c60 100644 --- a/src/citra_qt/bootmanager.h +++ b/src/citra_qt/bootmanager.h @@ -108,10 +108,11 @@ signals: void ErrorThrown(Core::System::ResultStatus, std::string); - void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); + void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total, + const std::string& object); void SwitchDiskResources(VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total); + std::size_t total, const std::string& object); void HideLoadingScreen(); }; diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index 061f25acf..aab00987b 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -4093,14 +4093,15 @@ void GMainWindow::OnEmulatorUpdateAvailable() { #endif void GMainWindow::OnSwitchDiskResources(VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total) { + std::size_t total, const std::string& object) { if (stage == VideoCore::LoadCallbackStage::Prepare) { loading_shaders_label->setText(QString()); loading_shaders_label->setVisible(true); } else if (stage == VideoCore::LoadCallbackStage::Complete) { loading_shaders_label->setVisible(false); } else { - loading_shaders_label->setText(loading_screen->GetStageTranslation(stage, value, total)); + loading_shaders_label->setText( + loading_screen->GetStageTranslation(stage, value, total, object)); } } diff --git a/src/citra_qt/citra_qt.h b/src/citra_qt/citra_qt.h index a4fa3c9aa..6657bdd22 100644 --- a/src/citra_qt/citra_qt.h +++ b/src/citra_qt/citra_qt.h @@ -311,7 +311,7 @@ private slots: void OnEmulatorUpdateAvailable(); #endif void OnSwitchDiskResources(VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total); + std::size_t total, const std::string& object); #ifdef ENABLE_DEVELOPER_OPTIONS void StartLaunchStressTest(const QString& game_path); #endif diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 3ab174440..f34753ca7 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -33,6 +33,8 @@ #include "citra_qt/game_list_p.h" #include "citra_qt/game_list_worker.h" #include "citra_qt/uisettings.h" +#include "common/common_paths.h" +#include "common/file_util.h" #include "common/logging/log.h" #include "common/settings.h" #include "core/core.h" @@ -606,6 +608,36 @@ void ForEachOpenGLCacheFile(u64 program_id, auto func) { QFile file{QString::fromStdString(path)}; func(file); } + const std::string path = + fmt::format("{}opengl/transferable/{:016X}.bin", + FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir), program_id); + QFile file{QString::fromStdString(path)}; + func(file); +} +#endif + +#ifdef ENABLE_VULKAN +void ForEachVulkanCacheFile(u64 program_id, auto func) { + for (const std::string_view cache_type : {"vs", "fs", "gs", "pl"}) { + const std::string path = fmt::format("{}vulkan/transferable/{:016X}_{}.vkch", + FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir), + program_id, cache_type); + QFile file{QString::fromStdString(path)}; + func(file); + } + + FileUtil::ForeachDirectoryEntry( + nullptr, + fmt::format("{}vulkan/pipeline", FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir)), + [program_id, &func]([[maybe_unused]] u64* num_entries_out, const std::string& directory, + const std::string& virtual_name) { + if (virtual_name.starts_with(fmt::format("{:016X}", program_id))) { + QFile file{QString::fromStdString(directory + DIR_SEP + virtual_name)}; + func(file); + } + + return true; + }); } #endif @@ -642,6 +674,10 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr QAction* delete_opengl_disk_shader_cache = shader_menu->addAction(tr("Delete OpenGL Shader Cache")); #endif +#ifdef ENABLE_VULKAN + QAction* delete_vulkan_disk_shader_cache = + shader_menu->addAction(tr("Delete Vulkan Shader Cache")); +#endif QMenu* uninstall_menu = context_menu.addMenu(tr("Uninstall")); QAction* uninstall_all = uninstall_menu->addAction(tr("Everything")); @@ -678,6 +714,12 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr program_id, [&opengl_cache_exists](QFile& file) { opengl_cache_exists |= file.exists(); }); #endif +#ifdef ENABLE_VULKAN + bool vulkan_cache_exists = false; + ForEachVulkanCacheFile( + program_id, [&vulkan_cache_exists](QFile& file) { vulkan_cache_exists |= file.exists(); }); +#endif + favorite->setVisible(program_id != 0); favorite->setCheckable(true); favorite->setChecked(UISettings::values.favorited_ids.contains(program_id)); @@ -721,6 +763,10 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr delete_opengl_disk_shader_cache->setEnabled(opengl_cache_exists); #endif +#ifdef ENABLE_VULKAN + delete_vulkan_disk_shader_cache->setEnabled(vulkan_cache_exists); +#endif + uninstall_all->setEnabled(is_installed || has_update || has_dlc); uninstall_game->setEnabled(is_installed); uninstall_update->setEnabled(has_update); @@ -800,6 +846,11 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr connect(delete_opengl_disk_shader_cache, &QAction::triggered, this, [program_id] { ForEachOpenGLCacheFile(program_id, [](QFile& file) { file.remove(); }); }); +#endif +#ifdef ENABLE_VULKAN + connect(delete_vulkan_disk_shader_cache, &QAction::triggered, this, [program_id] { + ForEachVulkanCacheFile(program_id, [](QFile& file) { file.remove(); }); + }); #endif connect(uninstall_all, &QAction::triggered, this, [=, this] { QMessageBox::StandardButton answer = QMessageBox::question( diff --git a/src/citra_qt/loading_screen.cpp b/src/citra_qt/loading_screen.cpp index ba2fdcaa6..84db1bb74 100644 --- a/src/citra_qt/loading_screen.cpp +++ b/src/citra_qt/loading_screen.cpp @@ -68,8 +68,7 @@ const static std::unordered_map stage QT_TRANSLATE_NOOP("LoadingScreen", "Preloading Textures %1 / %2")}, {VideoCore::LoadCallbackStage::Decompile, QT_TRANSLATE_NOOP("LoadingScreen", "Preparing Shaders %1 / %2")}, - {VideoCore::LoadCallbackStage::Build, - QT_TRANSLATE_NOOP("LoadingScreen", "Loading Shaders %1 / %2")}, + {VideoCore::LoadCallbackStage::Build, QT_TRANSLATE_NOOP("LoadingScreen", "Loading %3 %1 / %2")}, {VideoCore::LoadCallbackStage::Complete, QT_TRANSLATE_NOOP("LoadingScreen", "Launching...")}, }; const static std::unordered_map progressbar_style{ @@ -131,7 +130,7 @@ void LoadingScreen::Prepare(Loader::AppLoader& loader) { } ui->title->setText(tr("Now Loading\n%1").arg(QString::fromStdString(title))); eta_shown = false; - OnLoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0); + OnLoadProgress(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); } void LoadingScreen::OnLoadComplete() { @@ -139,7 +138,7 @@ void LoadingScreen::OnLoadComplete() { } void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total) { + std::size_t total, const std::string& object) { using namespace std::chrono; const auto now = high_resolution_clock::now(); // reset the timer if the stage changes @@ -184,7 +183,7 @@ void LoadingScreen::OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size } // update labels and progress bar - ui->stage->setText(GetStageTranslation(stage, value, total)); + ui->stage->setText(GetStageTranslation(stage, value, total, object)); ui->value->setText(estimate); ui->progress_bar->setValue(static_cast(value)); previous_time = now; @@ -199,11 +198,12 @@ void LoadingScreen::paintEvent(QPaintEvent* event) { } QString LoadingScreen::GetStageTranslation(VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total) { + std::size_t total, const std::string& object) { const auto& stg = tr(stage_translations.at(stage)); - if (stage == VideoCore::LoadCallbackStage::Decompile || - stage == VideoCore::LoadCallbackStage::Build || - stage == VideoCore::LoadCallbackStage::Preload) { + if (stage == VideoCore::LoadCallbackStage::Build) { + return stg.arg(value).arg(total).arg(QString::fromStdString(object)); + } else if (stage == VideoCore::LoadCallbackStage::Decompile || + stage == VideoCore::LoadCallbackStage::Preload) { return stg.arg(value).arg(total); } else { return stg; diff --git a/src/citra_qt/loading_screen.h b/src/citra_qt/loading_screen.h index a6bfc7d09..ad4fc5298 100644 --- a/src/citra_qt/loading_screen.h +++ b/src/citra_qt/loading_screen.h @@ -40,7 +40,8 @@ public: void Clear(); /// Slot used to update the status of the progress bar - void OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); + void OnLoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total, + const std::string& object); /// Hides the LoadingScreen with a fade out effect void OnLoadComplete(); @@ -50,10 +51,11 @@ public: void paintEvent(QPaintEvent* event) override; QString GetStageTranslation(VideoCore::LoadCallbackStage stage, std::size_t value, - std::size_t total); + std::size_t total, const std::string& object = ""); signals: - void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total); + void LoadProgress(VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total, + const std::string& object); /// Signals that this widget is completely hidden now and should be replaced with the other /// widget void Hidden(); diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index ac6728111..6bb14fc9e 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -19,6 +19,10 @@ add_custom_command(OUTPUT scm_rev.cpp "${VIDEO_CORE}/renderer_opengl/gl_shader_disk_cache.h" "${VIDEO_CORE}/renderer_opengl/gl_shader_util.cpp" "${VIDEO_CORE}/renderer_opengl/gl_shader_util.h" + "${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.cpp" + "${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.h" + "${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.cpp" + "${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.h" "${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp" "${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h" "${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.cpp" @@ -29,6 +33,7 @@ add_custom_command(OUTPUT scm_rev.cpp "${VIDEO_CORE}/shader/generator/glsl_shader_gen.h" "${VIDEO_CORE}/shader/generator/pica_fs_config.cpp" "${VIDEO_CORE}/shader/generator/pica_fs_config.h" + "${VIDEO_CORE}/shader/generator/profile.h" "${VIDEO_CORE}/shader/generator/shader_gen.cpp" "${VIDEO_CORE}/shader/generator/shader_gen.h" "${VIDEO_CORE}/shader/generator/shader_uniforms.cpp" diff --git a/src/common/hash.h b/src/common/hash.h index 1df29638a..8462c41b5 100644 --- a/src/common/hash.h +++ b/src/common/hash.h @@ -54,8 +54,14 @@ static inline u64 ComputeStructHash64(const T& data) noexcept { * Combines the seed parameter with the provided hash, producing a new unique hash * Implementation from: http://boost.sourceforge.net/doc/html/boost/hash_combine.html */ -[[nodiscard]] inline u64 HashCombine(const u64 seed, const u64 hash) { - return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2)); +[[nodiscard]] constexpr u64 HashCombine(u64 seed) { + return seed; +} + +template +[[nodiscard]] constexpr u64 HashCombine(u64 seed, u64 hash, Ts... rest) { + seed ^= hash + 0x9e3779b9 + (seed << 6) + (seed >> 2); + return HashCombine(seed, rest...); } template @@ -95,7 +101,7 @@ struct HashableStruct { return !(*this == o); }; - std::size_t Hash() const noexcept { + u64 Hash() const noexcept { return Common::ComputeStructHash64(state); } }; @@ -109,7 +115,7 @@ struct HashableString { HashableString(const std::string& s) : value(s) {} HashableString(std::string&& s) noexcept : value(std::move(s)) {} - std::size_t Hash() const noexcept { + u64 Hash() const noexcept { return ComputeHash64(value.data(), value.size()); } diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index ac85ea197..e177f24d9 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -53,9 +53,12 @@ std::vector CompressDataZSTDDefault(std::span source) { return CompressDataZSTD(source, ZSTD_CLEVEL_DEFAULT); } +std::size_t GetDecompressedSize(std::span compressed) { + return ZSTD_getFrameContentSize(compressed.data(), compressed.size()); +} + std::vector DecompressDataZSTD(std::span compressed) { - const std::size_t decompressed_size = - ZSTD_getFrameContentSize(compressed.data(), compressed.size()); + const std::size_t decompressed_size = GetDecompressedSize(compressed); if (decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN) { LOG_ERROR(Common, "ZSTD decompressed size could not be determined."); diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h index 75aceab2e..bd1d990dd 100644 --- a/src/common/zstd_compression.h +++ b/src/common/zstd_compression.h @@ -40,6 +40,15 @@ namespace Common::Compression { */ [[nodiscard]] std::vector CompressDataZSTDDefault(std::span source); +/** + * Gets the decompressed size of the specified Zstandard compressed memory region. + * + * @param compressed the compressed source memory region. + * + * @return the size of the decompressed data. + */ +[[nodiscard]] std::size_t GetDecompressedSize(std::span compressed); + /** * Decompresses a source memory region with Zstandard and returns the uncompressed data in a vector. * diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 27076cbe4..3da82159e 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -189,6 +189,8 @@ if (ENABLE_VULKAN) renderer_vulkan/vk_present_window.h renderer_vulkan/vk_render_manager.cpp renderer_vulkan/vk_render_manager.h + renderer_vulkan/vk_shader_disk_cache.cpp + renderer_vulkan/vk_shader_disk_cache.h renderer_vulkan/vk_shader_util.cpp renderer_vulkan/vk_shader_util.h renderer_vulkan/vk_stream_buffer.cpp diff --git a/src/video_core/custom_textures/custom_tex_manager.cpp b/src/video_core/custom_textures/custom_tex_manager.cpp index 2ec3c946b..58e5fe918 100644 --- a/src/video_core/custom_textures/custom_tex_manager.cpp +++ b/src/video_core/custom_textures/custom_tex_manager.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -228,7 +228,8 @@ void CustomTexManager::PreloadTextures(const std::atomic_bool& stop_run, material->LoadFromDisk(flip_png_files); size_sum += material->size; if (callback) { - callback(VideoCore::LoadCallbackStage::Preload, preloaded, custom_textures.size()); + callback(VideoCore::LoadCallbackStage::Preload, preloaded, custom_textures.size(), + ""); } preloaded++; } diff --git a/src/video_core/pica/shader_setup.h b/src/video_core/pica/shader_setup.h index 3ddd4f366..afad847e8 100644 --- a/src/video_core/pica/shader_setup.h +++ b/src/video_core/pica/shader_setup.h @@ -94,9 +94,9 @@ public: } } - void UpdateProgramCode(const ProgramCode& other) { + void UpdateProgramCode(const ProgramCode& other, u32 other_size = MAX_PROGRAM_CODE_LENGTH) { program_code = other; - biggest_program_size = program_code.size(); + biggest_program_size = std::max(biggest_program_size, other_size); MakeProgramCodeDirty(); } @@ -116,9 +116,9 @@ public: } } - void UpdateSwizzleData(const SwizzleData& other) { + void UpdateSwizzleData(const SwizzleData& other, u32 other_size = MAX_SWIZZLE_DATA_LENGTH) { swizzle_data = other; - biggest_swizzle_size = swizzle_data.size(); + biggest_swizzle_size = std::max(biggest_swizzle_size, other_size); MakeSwizzleDataDirty(); } @@ -130,6 +130,14 @@ public: return swizzle_data; } + u32 GetBiggestProgramSize() const { + return biggest_program_size; + } + + u32 GetBiggestSwizzleSize() const { + return biggest_swizzle_size; + } + public: Uniforms uniforms; PackedAttribute uniform_queue; diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h index 01b82f2f0..88bdd35c0 100644 --- a/src/video_core/rasterizer_interface.h +++ b/src/video_core/rasterizer_interface.h @@ -26,7 +26,8 @@ enum class LoadCallbackStage { Build, Complete, }; -using DiskResourceLoadCallback = std::function; +using DiskResourceLoadCallback = + std::function; class RasterizerInterface { public: diff --git a/src/video_core/renderer_opengl/gl_rasterizer.cpp b/src/video_core/renderer_opengl/gl_rasterizer.cpp index c8b6f725b..2c721bcf7 100644 --- a/src/video_core/renderer_opengl/gl_rasterizer.cpp +++ b/src/video_core/renderer_opengl/gl_rasterizer.cpp @@ -211,14 +211,14 @@ void RasterizerOpenGL::SwitchDiskResources(u64 title_id) { render_window, driver, title_id, !driver.IsOpenGLES())); if (switch_disk_resources_callback) { - switch_disk_resources_callback(VideoCore::LoadCallbackStage::Prepare, 0, 0); + switch_disk_resources_callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); } std::atomic_bool stop_loading; new_manager->LoadDiskCache(stop_loading, switch_disk_resources_callback, accurate_mul); if (switch_disk_resources_callback) { - switch_disk_resources_callback(VideoCore::LoadCallbackStage::Complete, 0, 0); + switch_disk_resources_callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); } } diff --git a/src/video_core/renderer_opengl/gl_shader_manager.cpp b/src/video_core/renderer_opengl/gl_shader_manager.cpp index 7b91a1639..786fb3fc5 100644 --- a/src/video_core/renderer_opengl/gl_shader_manager.cpp +++ b/src/video_core/renderer_opengl/gl_shader_manager.cpp @@ -9,6 +9,7 @@ #include #include #include +#include "common/hash.h" #include "common/settings.h" #include "core/frontend/emu_window.h" #include "video_core/pica/shader_setup.h" @@ -92,12 +93,7 @@ static std::tuple BuildVSConfigFromRaw( setup.UpdateProgramCode(program_code); setup.UpdateSwizzleData(swizzle_data); - // Enable the geometry-shader only if we are actually doing per-fragment lighting - // and care about proper quaternions. Otherwise just use standard vertex+fragment shaders - const bool use_geometry_shader = !raw.GetRawShaderConfig().lighting.disable; - return {PicaVSConfig{raw.GetRawShaderConfig(), setup, driver.HasClipCullDistance(), - use_geometry_shader, accurate_mul}, - setup}; + return {PicaVSConfig{raw.GetRawShaderConfig(), setup}, setup}; } /** @@ -163,31 +159,31 @@ public: ~ShaderCache() = default; template - std::tuple> Get(const KeyConfigType& config, - Args&&... args) { - auto [iter, new_shader] = shaders.emplace(config, OGLShaderStage{separable}); + std::tuple> Get(const KeyConfigType& config, + Args&&... args) { + auto [iter, new_shader] = shaders.emplace(config.Hash(), OGLShaderStage{separable}); OGLShaderStage& cached_shader = iter->second; std::optional result{}; if (new_shader) { result = CodeGenerator(config, args...); cached_shader.Create(result->c_str(), ShaderType); } - return {cached_shader.GetHandle(), std::move(result)}; + return {iter->first, cached_shader.GetHandle(), std::move(result)}; } void Inject(const KeyConfigType& key, OGLProgram&& program) { OGLShaderStage stage{separable}; stage.Inject(std::move(program)); - shaders.emplace(key, std::move(stage)); + shaders.emplace(key.Hash(), std::move(stage)); } void Inject(const KeyConfigType& key, OGLShaderStage&& stage) { - shaders.emplace(key, std::move(stage)); + shaders.emplace(key.Hash(), std::move(stage)); } private: bool separable; - std::unordered_map shaders; + std::unordered_map shaders; }; // This is a cache designed for shaders translated from PICA shaders. The first cache matches the @@ -195,62 +191,68 @@ private: // GLSL code. The configuration is like this because there might be leftover code in the PICA shader // program buffer from the previous shader, which is hashed into the config, resulting several // different config values from the same shader program. -template class ShaderDoubleCache { public: - explicit ShaderDoubleCache(bool separable) : separable(separable) {} - std::tuple> Get(const KeyConfigType& key, - const Pica::ShaderSetup& setup) { + explicit ShaderDoubleCache(bool _separable) : separable{_separable} {} + std::tuple> Get(const KeyConfigType& key, + const ExtraConfigType& extra, + const Pica::ShaderSetup& setup) { std::optional result{}; - auto map_it = shader_map.find(key); + const size_t key_hash = key.Hash(); + auto map_it = shader_map.find(key_hash); if (map_it == shader_map.end()) { - auto program = CodeGenerator(setup, key, separable); + auto program = Common::HashableString(CodeGenerator(setup, key, extra)); if (program.empty()) { - shader_map[key] = nullptr; - return {0, std::nullopt}; + shader_map[key_hash] = nullptr; + return {0, 0, std::nullopt}; } - auto [iter, new_shader] = shader_cache.emplace(program, OGLShaderStage{separable}); + auto [iter, new_shader] = + shader_cache.emplace(program.Hash(), OGLShaderStage{separable}); OGLShaderStage& cached_shader = iter->second; if (new_shader) { - result = program; - cached_shader.Create(program.c_str(), ShaderType); + result = std::move(program); + cached_shader.Create((*result).c_str(), ShaderType); } - shader_map[key] = &cached_shader; - return {cached_shader.GetHandle(), std::move(result)}; + shader_map[key_hash] = &cached_shader; + return {key_hash, cached_shader.GetHandle(), std::move(result)}; } if (map_it->second == nullptr) { - return {0, std::nullopt}; + return {0, 0, std::nullopt}; } - return {map_it->second->GetHandle(), std::nullopt}; + return {key_hash, map_it->second->GetHandle(), std::nullopt}; } void Inject(const KeyConfigType& key, std::string decomp, OGLProgram&& program) { OGLShaderStage stage{separable}; stage.Inject(std::move(program)); - const auto iter = shader_cache.emplace(std::move(decomp), std::move(stage)).first; + auto decomp_hash = Common::HashableString(std::move(decomp)); + const auto iter = shader_cache.emplace(decomp_hash.Hash(), std::move(stage)).first; OGLShaderStage& cached_shader = iter->second; - shader_map.insert_or_assign(key, &cached_shader); + shader_map.insert_or_assign(key.Hash(), &cached_shader); } void Inject(const KeyConfigType& key, std::string decomp, OGLShaderStage&& stage) { - const auto iter = shader_cache.emplace(std::move(decomp), std::move(stage)).first; + auto decomp_hash = Common::HashableString(std::move(decomp)); + const auto iter = shader_cache.emplace(decomp_hash.Hash(), std::move(stage)).first; OGLShaderStage& cached_shader = iter->second; - shader_map.insert_or_assign(key, &cached_shader); + shader_map.insert_or_assign(key.Hash(), &cached_shader); } private: bool separable; - std::unordered_map shader_map; - std::unordered_map shader_cache; + std::unordered_map shader_map; + std::unordered_map shader_cache; }; using ProgrammableVertexShaders = - ShaderDoubleCache; + ShaderDoubleCache; using FixedGeometryShaders = ShaderCache; @@ -326,6 +328,23 @@ public: std::unordered_map program_cache; OGLPipeline pipeline; ShaderDiskCache disk_cache; + + Pica::Shader::Generator::ExtraVSConfig CalcExtraConfig( + const Pica::Shader::Generator::PicaVSConfig& config, bool accurate_mul) { + auto res = ExtraVSConfig(); + + // Enable the geometry-shader only if we are actually doing per-fragment lighting + // and care about proper quaternions. Otherwise just use standard vertex+fragment shaders. + const bool use_geometry_shader = !config.state.lighting_disable; + + res.use_clip_planes = profile.has_clip_planes; + res.use_geometry_shader = use_geometry_shader; + res.sanitize_mul = accurate_mul; + res.separable_shader = separable; + res.load_flags.fill(AttribLoadFlags::Float); + + return res; + } }; ShaderProgramManager::ShaderProgramManager(Frontend::EmuWindow& emu_window_, const Driver& driver_, @@ -339,17 +358,15 @@ ShaderProgramManager::~ShaderProgramManager() = default; bool ShaderProgramManager::UseProgrammableVertexShader(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, bool accurate_mul) { - // Enable the geometry-shader only if we are actually doing per-fragment lighting - // and care about proper quaternions. Otherwise just use standard vertex+fragment shaders - const bool use_geometry_shader = !regs.lighting.disable; - PicaVSConfig config{regs, setup, driver.HasClipCullDistance(), use_geometry_shader, - accurate_mul}; - auto [handle, result] = impl->programmable_vertex_shaders.Get(config, setup); + PicaVSConfig config{regs, setup}; + ExtraVSConfig extra = impl->CalcExtraConfig(config, accurate_mul); + + auto [hash, handle, result] = impl->programmable_vertex_shaders.Get(config, extra, setup); if (handle == 0) return false; impl->current.vs = handle; - impl->current.vs_hash = config.Hash(); + impl->current.vs_hash = hash; // Save VS to the disk cache if its a new shader if (result) { @@ -373,10 +390,15 @@ void ShaderProgramManager::UseTrivialVertexShader() { } void ShaderProgramManager::UseFixedGeometryShader(const Pica::RegsInternal& regs) { - PicaFixedGSConfig gs_config(regs, driver.HasClipCullDistance()); - auto [handle, _] = impl->fixed_geometry_shaders.Get(gs_config, impl->separable); + PicaFixedGSConfig gs_config(regs); + ExtraFixedGSConfig extra{ + .use_clip_planes = driver.HasClipCullDistance(), + .separable_shader = impl->separable, + }; + + auto [hash, handle, _] = impl->fixed_geometry_shaders.Get(gs_config, extra); impl->current.gs = handle; - impl->current.gs_hash = gs_config.Hash(); + impl->current.gs_hash = hash; } void ShaderProgramManager::UseTrivialGeometryShader() { @@ -386,10 +408,10 @@ void ShaderProgramManager::UseTrivialGeometryShader() { void ShaderProgramManager::UseFragmentShader(const Pica::RegsInternal& regs, const Pica::Shader::UserConfig& user) { - const FSConfig fs_config{regs, user, impl->profile}; - auto [handle, result] = impl->fragment_shaders.Get(fs_config, impl->profile); + const FSConfig fs_config{regs}; + auto [hash, handle, result] = impl->fragment_shaders.Get(fs_config, user, impl->profile); impl->current.fs = handle; - impl->current.fs_hash = fs_config.Hash(); + impl->current.fs_hash = hash; // Save FS to the disk cache if its a new shader if (result) { auto& disk_cache = impl->disk_cache; @@ -457,7 +479,7 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, std::mutex mutex; std::atomic_bool compilation_failed = false; if (callback) { - callback(VideoCore::LoadCallbackStage::Decompile, 0, raws.size()); + callback(VideoCore::LoadCallbackStage::Decompile, 0, raws.size(), ""); } std::vector load_raws_index; // Loads both decompiled and precompiled shaders from the cache. If either one is missing for @@ -513,7 +535,7 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, std::move(shader)); } else if (raw.GetProgramType() == ProgramType::FS) { // TODO: Support UserConfig in disk shader cache - const FSConfig conf(raw.GetRawShaderConfig(), {}, impl->profile); + const FSConfig conf(raw.GetRawShaderConfig()); std::scoped_lock lock(mutex); impl->fragment_shaders.Inject(conf, std::move(shader)); } else { @@ -530,7 +552,7 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, load_raws_index.push_back(i); } if (callback) { - callback(VideoCore::LoadCallbackStage::Decompile, i, raw_cache.size()); + callback(VideoCore::LoadCallbackStage::Decompile, i, raw_cache.size(), ""); } } }; @@ -561,7 +583,7 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, break; } if (callback) { - callback(VideoCore::LoadCallbackStage::Decompile, ++i, dump_map.size()); + callback(VideoCore::LoadCallbackStage::Decompile, ++i, dump_map.size(), ""); } } }; @@ -590,7 +612,7 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, const std::size_t load_raws_size = load_all_raws ? raws.size() : load_raws_index.size(); if (callback) { - callback(VideoCore::LoadCallbackStage::Build, 0, load_raws_size); + callback(VideoCore::LoadCallbackStage::Build, 0, load_raws_size, "Shader"); } compilation_failed = false; @@ -615,17 +637,18 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, // precompiled file if (raw.GetProgramType() == ProgramType::VS) { auto [conf, setup] = BuildVSConfigFromRaw(raw, driver, accurate_mul); - code = GLSL::GenerateVertexShader(setup, conf, impl->separable); + ExtraVSConfig extra = impl->CalcExtraConfig(conf, accurate_mul); + code = GLSL::GenerateVertexShader(setup, conf, extra); OGLShaderStage stage{impl->separable}; stage.Create(code.c_str(), GL_VERTEX_SHADER); handle = stage.GetHandle(); - sanitize_mul = conf.state.sanitize_mul; + sanitize_mul = accurate_mul; std::scoped_lock lock(mutex); impl->programmable_vertex_shaders.Inject(conf, code, std::move(stage)); } else if (raw.GetProgramType() == ProgramType::FS) { // TODO: Support UserConfig in disk shader cache - const FSConfig fs_config{raw.GetRawShaderConfig(), {}, impl->profile}; - code = GLSL::GenerateFragmentShader(fs_config, impl->profile); + const FSConfig fs_config{raw.GetRawShaderConfig()}; + code = GLSL::GenerateFragmentShader(fs_config, {}, impl->profile); OGLShaderStage stage{impl->separable}; stage.Create(code.c_str(), GL_FRAGMENT_SHADER); handle = stage.GetHandle(); @@ -653,7 +676,8 @@ void ShaderProgramManager::LoadDiskCache(const std::atomic_bool& stop_loading, } if (callback) { - callback(VideoCore::LoadCallbackStage::Build, ++built_shaders, load_raws_size); + callback(VideoCore::LoadCallbackStage::Build, ++built_shaders, load_raws_size, + "Shader"); } } }; diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp index adcad826e..7c078802d 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp @@ -4,6 +4,7 @@ #include +#include "common/alignment.h" #include "common/hash.h" #include "common/microprofile.h" #include "video_core/renderer_vulkan/pica_to_vk.h" @@ -31,25 +32,32 @@ vk::ShaderStageFlagBits MakeShaderStage(std::size_t index) { return vk::ShaderStageFlagBits::eVertex; } -u64 PipelineInfo::Hash(const Instance& instance) const { - u64 info_hash = 0; - const auto append_hash = [&info_hash](const auto& data) { - const u64 data_hash = Common::ComputeStructHash64(data); - info_hash = Common::HashCombine(info_hash, data_hash); - }; - - append_hash(vertex_layout); - append_hash(attachments); - append_hash(blending); +u64 StaticPipelineInfo::OptimizedHash(const Instance& instance) const { + u64 info_hash = Common::HashCombine( + shader_ids[0], shader_ids[1], shader_ids[2], Common::ComputeStructHash64(vertex_layout), + Common::ComputeStructHash64(attachments), Common::ComputeStructHash64(blending)); if (!instance.IsExtendedDynamicStateSupported()) { - append_hash(rasterization); - append_hash(depth_stencil); + info_hash = Common::HashCombine(info_hash, Common::ComputeStructHash64(rasterization), + Common::ComputeStructHash64(depth_stencil)); } return info_hash; } +u16 PipelineInfo::GetFinalColorWriteMask(const Instance& instance) { + u16 color_write_mask = state.blending.color_write_mask; + const bool is_logic_op_emulated = + instance.NeedsLogicOpEmulation() && !state.blending.blend_enable; + const bool is_logic_op_noop = state.blending.logic_op == Pica::FramebufferRegs::LogicOp::NoOp; + if (is_logic_op_emulated && is_logic_op_noop) { + // Color output is disabled by logic operation. We use color write mask to skip + // color but allow depth write. + color_write_mask = 0; + } + return color_write_mask; +} + Shader::Shader(const Instance& instance) : device{instance.GetDevice()} {} Shader::Shader(const Instance& instance, vk::ShaderStageFlagBits stage, std::string code) @@ -100,20 +108,22 @@ bool GraphicsPipeline::TryBuild(bool wait_built) { bool GraphicsPipeline::Build(bool fail_on_compile_required) { MICROPROFILE_SCOPE(Vulkan_Pipeline); + + const u32 stride_alignment = instance.GetMinVertexStrideAlignment(); std::array bindings; - for (u32 i = 0; i < info.vertex_layout.binding_count; i++) { - const auto& binding = info.vertex_layout.bindings[i]; + for (u32 i = 0; i < info.state.vertex_layout.binding_count; i++) { + const auto& binding = info.state.vertex_layout.bindings[i]; bindings[i] = vk::VertexInputBindingDescription{ .binding = binding.binding, - .stride = binding.stride, + .stride = Common::AlignUp(binding.byte_count.Value(), stride_alignment), .inputRate = binding.fixed.Value() ? vk::VertexInputRate::eInstance : vk::VertexInputRate::eVertex, }; } std::array attributes; - for (u32 i = 0; i < info.vertex_layout.attribute_count; i++) { - const auto& attr = info.vertex_layout.attributes[i]; + for (u32 i = 0; i < info.state.vertex_layout.attribute_count; i++) { + const auto& attr = info.state.vertex_layout.attributes[i]; const FormatTraits& traits = instance.GetTraits(attr.type, attr.size); attributes[i] = vk::VertexInputAttributeDescription{ .location = attr.location, @@ -131,23 +141,23 @@ bool GraphicsPipeline::Build(bool fail_on_compile_required) { } const vk::PipelineVertexInputStateCreateInfo vertex_input_info = { - .vertexBindingDescriptionCount = info.vertex_layout.binding_count, + .vertexBindingDescriptionCount = info.state.vertex_layout.binding_count, .pVertexBindingDescriptions = bindings.data(), - .vertexAttributeDescriptionCount = info.vertex_layout.attribute_count, + .vertexAttributeDescriptionCount = info.state.vertex_layout.attribute_count, .pVertexAttributeDescriptions = attributes.data(), }; const vk::PipelineInputAssemblyStateCreateInfo input_assembly = { - .topology = PicaToVK::PrimitiveTopology(info.rasterization.topology), + .topology = PicaToVK::PrimitiveTopology(info.state.rasterization.topology), .primitiveRestartEnable = false, }; const vk::PipelineRasterizationStateCreateInfo raster_state = { .depthClampEnable = false, .rasterizerDiscardEnable = false, - .cullMode = - PicaToVK::CullMode(info.rasterization.cull_mode, info.rasterization.flip_viewport), - .frontFace = PicaToVK::FrontFace(info.rasterization.cull_mode), + .cullMode = PicaToVK::CullMode(info.state.rasterization.cull_mode, + info.state.rasterization.flip_viewport), + .frontFace = PicaToVK::FrontFace(info.state.rasterization.cull_mode), .depthBiasEnable = false, .lineWidth = 1.0f, }; @@ -158,19 +168,20 @@ bool GraphicsPipeline::Build(bool fail_on_compile_required) { }; const vk::PipelineColorBlendAttachmentState colorblend_attachment = { - .blendEnable = info.blending.blend_enable, - .srcColorBlendFactor = PicaToVK::BlendFunc(info.blending.src_color_blend_factor), - .dstColorBlendFactor = PicaToVK::BlendFunc(info.blending.dst_color_blend_factor), - .colorBlendOp = PicaToVK::BlendEquation(info.blending.color_blend_eq), - .srcAlphaBlendFactor = PicaToVK::BlendFunc(info.blending.src_alpha_blend_factor), - .dstAlphaBlendFactor = PicaToVK::BlendFunc(info.blending.dst_alpha_blend_factor), - .alphaBlendOp = PicaToVK::BlendEquation(info.blending.alpha_blend_eq), - .colorWriteMask = static_cast(info.blending.color_write_mask), + .blendEnable = info.state.blending.blend_enable, + .srcColorBlendFactor = PicaToVK::BlendFunc(info.state.blending.src_color_blend_factor), + .dstColorBlendFactor = PicaToVK::BlendFunc(info.state.blending.dst_color_blend_factor), + .colorBlendOp = PicaToVK::BlendEquation(info.state.blending.color_blend_eq), + .srcAlphaBlendFactor = PicaToVK::BlendFunc(info.state.blending.src_alpha_blend_factor), + .dstAlphaBlendFactor = PicaToVK::BlendFunc(info.state.blending.dst_alpha_blend_factor), + .alphaBlendOp = PicaToVK::BlendEquation(info.state.blending.alpha_blend_eq), + .colorWriteMask = + static_cast(info.GetFinalColorWriteMask(instance)), }; const vk::PipelineColorBlendStateCreateInfo color_blending = { - .logicOpEnable = !info.blending.blend_enable && !instance.NeedsLogicOpEmulation(), - .logicOp = PicaToVK::LogicOp(info.blending.logic_op), + .logicOpEnable = !info.state.blending.blend_enable && !instance.NeedsLogicOpEmulation(), + .logicOp = PicaToVK::LogicOp(info.state.blending.logic_op), .attachmentCount = 1, .pAttachments = &colorblend_attachment, .blendConstants = std::array{1.0f, 1.0f, 1.0f, 1.0f}, @@ -219,18 +230,18 @@ bool GraphicsPipeline::Build(bool fail_on_compile_required) { }; const vk::StencilOpState stencil_op_state = { - .failOp = PicaToVK::StencilOp(info.depth_stencil.stencil_fail_op), - .passOp = PicaToVK::StencilOp(info.depth_stencil.stencil_pass_op), - .depthFailOp = PicaToVK::StencilOp(info.depth_stencil.stencil_depth_fail_op), - .compareOp = PicaToVK::CompareFunc(info.depth_stencil.stencil_compare_op), + .failOp = PicaToVK::StencilOp(info.state.depth_stencil.stencil_fail_op), + .passOp = PicaToVK::StencilOp(info.state.depth_stencil.stencil_pass_op), + .depthFailOp = PicaToVK::StencilOp(info.state.depth_stencil.stencil_depth_fail_op), + .compareOp = PicaToVK::CompareFunc(info.state.depth_stencil.stencil_compare_op), }; const vk::PipelineDepthStencilStateCreateInfo depth_info = { - .depthTestEnable = static_cast(info.depth_stencil.depth_test_enable.Value()), - .depthWriteEnable = static_cast(info.depth_stencil.depth_write_enable.Value()), - .depthCompareOp = PicaToVK::CompareFunc(info.depth_stencil.depth_compare_op), + .depthTestEnable = static_cast(info.state.depth_stencil.depth_test_enable.Value()), + .depthWriteEnable = static_cast(info.state.depth_stencil.depth_write_enable.Value()), + .depthCompareOp = PicaToVK::CompareFunc(info.state.depth_stencil.depth_compare_op), .depthBoundsTestEnable = false, - .stencilTestEnable = static_cast(info.depth_stencil.stencil_test_enable.Value()), + .stencilTestEnable = static_cast(info.state.depth_stencil.stencil_test_enable.Value()), .front = stencil_op_state, .back = stencil_op_state, }; @@ -263,8 +274,8 @@ bool GraphicsPipeline::Build(bool fail_on_compile_required) { .pColorBlendState = &color_blending, .pDynamicState = &dynamic_info, .layout = pipeline_layout, - .renderPass = - renderpass_cache.GetRenderpass(info.attachments.color, info.attachments.depth, false), + .renderPass = renderpass_cache.GetRenderpass(info.state.attachments.color, + info.state.attachments.depth, false), }; if (fail_on_compile_required) { diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h index ab85771d7..5f817094e 100644 --- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h +++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h @@ -2,12 +2,18 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#pragma once + +#include "common/hash.h" #include "common/thread_worker.h" #include "video_core/pica/regs_pipeline.h" #include "video_core/pica/regs_rasterizer.h" #include "video_core/rasterizer_cache/pixel_format.h" #include "video_core/renderer_vulkan/vk_common.h" +#define LAYOUT_HASH static_cast(sizeof(T)), static_cast(alignof(T)) +#define FIELD_HASH(x) static_cast(offsetof(T, x)), static_cast(sizeof(x)) + namespace Common { struct AsyncHandle { @@ -51,14 +57,29 @@ constexpr u32 MAX_VERTEX_BINDINGS = 13; * the overhead of hashing as much as possible */ union RasterizationState { - u8 value = 0; + u8 value; BitField<0, 2, Pica::PipelineRegs::TriangleTopology> topology; BitField<4, 2, Pica::RasterizerRegs::CullMode> cull_mode; BitField<6, 1, u8> flip_viewport; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = RasterizationState; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(topology), FIELD_HASH(cull_mode), + FIELD_HASH(flip_viewport)); + } }; +static_assert(std::is_trivial_v); union DepthStencilState { - u32 value = 0; + u32 value; BitField<0, 1, u32> depth_test_enable; BitField<1, 1, u32> depth_write_enable; BitField<2, 1, u32> stencil_test_enable; @@ -67,14 +88,32 @@ union DepthStencilState { BitField<9, 3, Pica::FramebufferRegs::StencilAction> stencil_pass_op; BitField<12, 3, Pica::FramebufferRegs::StencilAction> stencil_depth_fail_op; BitField<15, 3, Pica::FramebufferRegs::CompareFunc> stencil_compare_op; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = DepthStencilState; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(depth_test_enable), FIELD_HASH(depth_write_enable), + FIELD_HASH(stencil_test_enable), FIELD_HASH(depth_compare_op), + FIELD_HASH(stencil_fail_op), FIELD_HASH(stencil_pass_op), + FIELD_HASH(stencil_depth_fail_op), + FIELD_HASH(stencil_compare_op)); + } }; +static_assert(std::is_trivial_v); struct BlendingState { u16 blend_enable; u16 color_write_mask; Pica::FramebufferRegs::LogicOp logic_op; union { - u32 value = 0; + u32 value; BitField<0, 4, Pica::FramebufferRegs::BlendFactor> src_color_blend_factor; BitField<4, 4, Pica::FramebufferRegs::BlendFactor> dst_color_blend_factor; BitField<8, 3, Pica::FramebufferRegs::BlendEquation> color_blend_eq; @@ -82,9 +121,149 @@ struct BlendingState { BitField<15, 4, Pica::FramebufferRegs::BlendFactor> dst_alpha_blend_factor; BitField<19, 3, Pica::FramebufferRegs::BlendEquation> alpha_blend_eq; }; -}; -struct DynamicState { + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = BlendingState; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(blend_enable), FIELD_HASH(color_write_mask), + FIELD_HASH(logic_op), FIELD_HASH(src_color_blend_factor), + FIELD_HASH(dst_color_blend_factor), FIELD_HASH(color_blend_eq), + FIELD_HASH(src_alpha_blend_factor), + FIELD_HASH(dst_alpha_blend_factor), FIELD_HASH(alpha_blend_eq)); + } +}; +static_assert(std::is_trivial_v); + +union VertexBinding { + u16 value; + BitField<0, 4, u16> binding; + BitField<4, 1, u16> fixed; + BitField<5, 11, u16> byte_count; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = VertexBinding; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(binding), FIELD_HASH(fixed), FIELD_HASH(byte_count)); + } +}; +static_assert(std::is_trivial_v); + +union VertexAttribute { + u32 value; + BitField<0, 4, u32> binding; + BitField<4, 4, u32> location; + BitField<8, 3, Pica::PipelineRegs::VertexAttributeFormat> type; + BitField<11, 3, u32> size; + BitField<14, 11, u32> offset; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = VertexAttribute; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(binding), FIELD_HASH(location), FIELD_HASH(type), + FIELD_HASH(size), FIELD_HASH(offset)); + } +}; +static_assert(std::is_trivial_v); + +struct VertexLayout { + u8 binding_count; + u8 attribute_count; + std::array bindings; + std::array attributes; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = VertexLayout; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(binding_count), FIELD_HASH(attribute_count), + FIELD_HASH(bindings), FIELD_HASH(attributes), + + // nested layout + VertexBinding::StructHash(), VertexAttribute::StructHash()); + } +}; +static_assert(std::is_trivial_v); + +struct AttachmentInfo { + VideoCore::PixelFormat color; + VideoCore::PixelFormat depth; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = AttachmentInfo; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(color), FIELD_HASH(depth)); + } +}; +static_assert(std::is_trivial_v); + +struct StaticPipelineInfo { + std::array shader_ids; + + BlendingState blending; + AttachmentInfo attachments; + VertexLayout vertex_layout; + + RasterizationState rasterization; + DepthStencilState depth_stencil; + + [[nodiscard]] u64 OptimizedHash(const Instance& instance) const; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = StaticPipelineInfo; + return Common::HashCombine( + STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(shader_ids), FIELD_HASH(blending), FIELD_HASH(attachments), + FIELD_HASH(vertex_layout), FIELD_HASH(rasterization), FIELD_HASH(depth_stencil), + + // nested layout + BlendingState::StructHash(), AttachmentInfo::StructHash(), VertexLayout::StructHash(), + RasterizationState::StructHash(), DepthStencilState::StructHash()); + } +}; +static_assert(std::is_trivial_v); + +struct DynamicPipelineInfo { u32 blend_color = 0; u8 stencil_reference; u8 stencil_compare_mask; @@ -93,61 +272,28 @@ struct DynamicState { Common::Rectangle scissor; Common::Rectangle viewport; - bool operator==(const DynamicState& other) const noexcept { - return std::memcmp(this, &other, sizeof(DynamicState)) == 0; + bool operator==(const DynamicPipelineInfo& other) const noexcept { + return std::memcmp(this, &other, sizeof(DynamicPipelineInfo)) == 0; } }; -union VertexBinding { - u16 value = 0; - BitField<0, 4, u16> binding; - BitField<4, 1, u16> fixed; - BitField<5, 11, u16> stride; -}; - -union VertexAttribute { - u32 value = 0; - BitField<0, 4, u32> binding; - BitField<4, 4, u32> location; - BitField<8, 3, Pica::PipelineRegs::VertexAttributeFormat> type; - BitField<11, 3, u32> size; - BitField<14, 11, u32> offset; -}; - -struct VertexLayout { - u8 binding_count; - u8 attribute_count; - std::array bindings; - std::array attributes; -}; - -struct AttachmentInfo { - VideoCore::PixelFormat color; - VideoCore::PixelFormat depth; -}; - /** * Information about a graphics pipeline */ -struct PipelineInfo { - BlendingState blending; - AttachmentInfo attachments; - RasterizationState rasterization; - DepthStencilState depth_stencil; - DynamicState dynamic; - VertexLayout vertex_layout; - - [[nodiscard]] u64 Hash(const Instance& instance) const; +struct PipelineInfo : Common::HashableStruct { + DynamicPipelineInfo dynamic_info; [[nodiscard]] bool IsDepthWriteEnabled() const noexcept { - const bool has_stencil = attachments.depth == VideoCore::PixelFormat::D24S8; + const bool has_stencil = state.attachments.depth == VideoCore::PixelFormat::D24S8; const bool depth_write = - depth_stencil.depth_test_enable && depth_stencil.depth_write_enable; - const bool stencil_write = - has_stencil && depth_stencil.stencil_test_enable && dynamic.stencil_write_mask != 0; + state.depth_stencil.depth_test_enable && state.depth_stencil.depth_write_enable; + const bool stencil_write = has_stencil && state.depth_stencil.stencil_test_enable && + dynamic_info.stencil_write_mask != 0; return depth_write || stencil_write; } + + [[nodiscard]] u16 GetFinalColorWriteMask(const Instance& instance); }; struct Shader : public Common::AsyncHandle { @@ -195,3 +341,6 @@ private: }; } // namespace Vulkan + +#undef LAYOUT_HASH +#undef FIELD_HASH diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 88c8726f4..09984b53f 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -30,8 +30,10 @@ struct FormatTraits { bool needs_conversion = false; bool needs_emulation = false; vk::ImageUsageFlags usage{}; - vk::ImageAspectFlags aspect; + vk::ImageAspectFlags aspect{}; vk::Format native = vk::Format::eUndefined; + + auto operator<=>(const FormatTraits&) const = default; }; class Instance { @@ -48,6 +50,10 @@ public: const FormatTraits& GetTraits(Pica::PipelineRegs::VertexAttributeFormat format, u32 count) const; + const std::array& GetAllTraits() const { + return attrib_table; + } + /// Returns a formatted string for the driver version std::string GetDriverVersionName(); diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 4100ac509..8c8171495 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -95,6 +95,7 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, GLSL::GenerateTrivialVertexShader(instance.IsShaderClipDistanceSupported(), true)} { scheduler.RegisterOnDispatch([this] { update_queue.Flush(); }); profile = Pica::Shader::Profile{ + .enable_accurate_mul = false, .has_separable_shaders = true, .has_clip_planes = instance.IsShaderClipDistanceSupported(), .has_geometry_shader = instance.UseGeometryShaders(), @@ -104,8 +105,26 @@ PipelineCache::PipelineCache(const Instance& instance_, Scheduler& scheduler_, .has_blend_minmax_factor = false, .has_minus_one_to_one_range = false, .has_logic_op = !instance.NeedsLogicOpEmulation(), + .vk_disable_spirv_optimizer = Settings::values.disable_spirv_optimizer.GetValue(), + .vk_use_spirv_generator = Settings::values.spirv_shader_gen.GetValue(), .is_vulkan = true, }; + + const auto& traits = instance.GetAllTraits(); + size_t i = 0; + for (const auto& it : traits) { + profile.vk_format_traits[i].transfer_support = it.transfer_support; + profile.vk_format_traits[i].blit_support = it.blit_support; + profile.vk_format_traits[i].attachment_support = it.attachment_support; + profile.vk_format_traits[i].storage_support = it.storage_support; + profile.vk_format_traits[i].needs_conversion = it.needs_conversion; + profile.vk_format_traits[i].needs_emulation = it.needs_emulation; + profile.vk_format_traits[i].usage_flags = static_cast(it.usage); + profile.vk_format_traits[i].aspect_flags = static_cast(it.aspect); + profile.vk_format_traits[i].native_format = static_cast(it.native); + ++i; + } + BuildLayout(); } @@ -128,18 +147,18 @@ PipelineCache::~PipelineCache() { SaveDiskCache(); } -void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading, - const VideoCore::DiskResourceLoadCallback& callback) { +void PipelineCache::LoadPipelineDiskCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { vk::PipelineCacheCreateInfo cache_info{}; if (callback) { - callback(VideoCore::LoadCallbackStage::Prepare, 0, 0); + callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); } if (callback) { - callback(VideoCore::LoadCallbackStage::Build, 0, 1); + callback(VideoCore::LoadCallbackStage::Build, 0, 1, "Driver Pipeline Cache"); } - auto load_cache = [this, &cache_info, &callback](bool allow_fallback) { + auto load_cache = [this, &cache_info](bool allow_fallback) { const vk::Device device = instance.GetDevice(); try { pipeline_cache = device.createPipelineCacheUnique(cache_info); @@ -157,9 +176,6 @@ void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading, } } } - if (callback) { - callback(VideoCore::LoadCallbackStage::Complete, 0, 0); - } }; // Try to load existing pipeline cache if disk cache is enabled and directories exist @@ -210,6 +226,16 @@ void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading, load_cache(true); } +void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + + disk_caches.clear(); + curr_disk_cache = + disk_caches.emplace_back(std::make_shared(*this, GetProgramID())); + + curr_disk_cache->Init(stop_loading, callback); +} + void PipelineCache::SaveDiskCache() { // Save Vulkan pipeline cache if (!Settings::values.use_disk_shader_cache || !pipeline_cache) { @@ -239,25 +265,14 @@ void PipelineCache::SaveDiskCache() { } } -bool PipelineCache::BindPipeline(const PipelineInfo& info, bool wait_built) { +bool PipelineCache::BindPipeline(PipelineInfo& info, bool wait_built) { MICROPROFILE_SCOPE(Vulkan_Bind); - u64 shader_hash = 0; for (u32 i = 0; i < MAX_SHADER_STAGES; i++) { - shader_hash = Common::HashCombine(shader_hash, shader_hashes[i]); + info.state.shader_ids[i] = shader_hashes[i]; } - const u64 info_hash = info.Hash(instance); - const u64 pipeline_hash = Common::HashCombine(shader_hash, info_hash); - - auto [it, new_pipeline] = graphics_pipelines.try_emplace(pipeline_hash); - if (new_pipeline) { - it.value() = - std::make_unique(instance, renderpass_cache, info, *pipeline_cache, - *pipeline_layout, current_shaders, &workers); - } - - GraphicsPipeline* const pipeline{it->second.get()}; + GraphicsPipeline* const pipeline = curr_disk_cache->GetPipeline(info); if (!pipeline->IsDone() && !pipeline->TryBuild(wait_built)) { return false; } @@ -265,12 +280,12 @@ bool PipelineCache::BindPipeline(const PipelineInfo& info, bool wait_built) { const bool is_dirty = scheduler.IsStateDirty(StateFlags::Pipeline); const bool pipeline_dirty = (current_pipeline != pipeline) || is_dirty; scheduler.Record([this, is_dirty, pipeline_dirty, pipeline, - current_dynamic = current_info.dynamic, dynamic = info.dynamic, + current_dynamic = current_info.dynamic_info, dynamic = info.dynamic_info, descriptor_sets = bound_descriptor_sets, offsets = offsets, - current_rasterization = current_info.rasterization, - current_depth_stencil = current_info.depth_stencil, - rasterization = info.rasterization, - depth_stencil = info.depth_stencil](vk::CommandBuffer cmdbuf) { + current_rasterization = current_info.state.rasterization, + current_depth_stencil = current_info.state.depth_stencil, + rasterization = info.state.rasterization, + depth_stencil = info.state.depth_stencil](vk::CommandBuffer cmdbuf) { if (dynamic.viewport != current_dynamic.viewport || is_dirty) { const vk::Viewport vk_viewport = { .x = static_cast(dynamic.viewport.left), @@ -383,67 +398,54 @@ bool PipelineCache::BindPipeline(const PipelineInfo& info, bool wait_built) { return true; } -bool PipelineCache::UseProgrammableVertexShader(const Pica::RegsInternal& regs, - Pica::ShaderSetup& setup, - const VertexLayout& layout, bool accurate_mul) { +ExtraVSConfig PipelineCache::CalcExtraConfig(const PicaVSConfig& config) { + auto res = ExtraVSConfig(); + // Enable the geometry-shader only if we are actually doing per-fragment lighting // and care about proper quaternions. Otherwise just use standard vertex+fragment shaders. // We also don't need the geometry shader if we have the barycentric extension. - const bool use_geometry_shader = instance.UseGeometryShaders() && !regs.lighting.disable && + const bool use_geometry_shader = instance.UseGeometryShaders() && + !config.state.lighting_disable && !instance.IsFragmentShaderBarycentricSupported(); - PicaVSConfig config{regs, setup, instance.IsShaderClipDistanceSupported(), use_geometry_shader, - accurate_mul}; - for (u32 i = 0; i < layout.attribute_count; i++) { - const VertexAttribute& attr = layout.attributes[i]; - const FormatTraits& traits = instance.GetTraits(attr.type, attr.size); - const u32 location = attr.location.Value(); - AttribLoadFlags& flags = config.state.load_flags[location]; + res.use_clip_planes = instance.IsShaderClipDistanceSupported(); + res.use_geometry_shader = use_geometry_shader; + res.sanitize_mul = profile.enable_accurate_mul; + res.separable_shader = true; + res.load_flags.fill(AttribLoadFlags::Float); + + for (u32 i = 0; i < config.state.used_input_vertex_attributes; i++) { + const auto& attr = config.state.input_vertex_attributes[i]; + const u32 location = attr.location; + const Pica::PipelineRegs::VertexAttributeFormat type = + static_cast(attr.type); + const FormatTraits& traits = instance.GetTraits(type, attr.size); + AttribLoadFlags& flags = res.load_flags[location]; if (traits.needs_conversion) { - flags = MakeAttribLoadFlag(attr.type); + flags = MakeAttribLoadFlag(type); } if (traits.needs_emulation) { flags |= AttribLoadFlags::ZeroW; } } - const auto config_hash = config.Hash(); + return res; +} - const auto [it, new_config] = programmable_vertex_map.try_emplace(config_hash); - if (new_config) { - auto program = Common::HashableString(GLSL::GenerateVertexShader(setup, config, true)); - if (program.empty()) { - LOG_ERROR(Render_Vulkan, "Failed to retrieve programmable vertex shader"); - programmable_vertex_map[config_hash] = nullptr; - return false; - } +bool PipelineCache::UseProgrammableVertexShader(const Pica::RegsInternal& regs, + Pica::ShaderSetup& setup, + const VertexLayout& layout) { - auto [iter, new_program] = programmable_vertex_cache.try_emplace(program.Hash(), instance); - auto& shader = iter->second; + auto res = curr_disk_cache->UseProgrammableVertexShader(regs, setup, layout); - if (new_program) { - shader.program = std::move(program); - const vk::Device device = instance.GetDevice(); - workers.QueueWork([device, &shader] { - shader.module = Compile(shader.program, vk::ShaderStageFlagBits::eVertex, device); - shader.MarkDone(); - }); - } - - it->second = &shader; + if (res.has_value()) { + current_shaders[ProgramType::VS] = (*res).second; + shader_hashes[ProgramType::VS] = (*res).first; + return true; } - Shader* const shader{it->second}; - if (!shader) { - LOG_ERROR(Render_Vulkan, "Failed to retrieve programmable vertex shader"); - return false; - } - - current_shaders[ProgramType::VS] = shader; - shader_hashes[ProgramType::VS] = config.Hash(); - - return true; + return false; } void PipelineCache::UseTrivialVertexShader() { @@ -452,27 +454,16 @@ void PipelineCache::UseTrivialVertexShader() { } bool PipelineCache::UseFixedGeometryShader(const Pica::RegsInternal& regs) { - if (!instance.UseGeometryShaders()) { - UseTrivialGeometryShader(); + + auto res = curr_disk_cache->UseFixedGeometryShader(regs); + + if (res.has_value()) { + current_shaders[ProgramType::GS] = (*res).second; + shader_hashes[ProgramType::GS] = (*res).first; return true; } - const PicaFixedGSConfig gs_config{regs, instance.IsShaderClipDistanceSupported()}; - auto [it, new_shader] = fixed_geometry_shaders.try_emplace(gs_config.Hash(), instance); - auto& shader = it->second; - - if (new_shader) { - workers.QueueWork([gs_config, device = instance.GetDevice(), &shader]() { - const auto code = GLSL::GenerateFixedGeometryShader(gs_config, true); - shader.module = Compile(code, vk::ShaderStageFlagBits::eGeometry, device); - shader.MarkDone(); - }); - } - - current_shaders[ProgramType::GS] = &shader; - shader_hashes[ProgramType::GS] = gs_config.Hash(); - - return true; + return false; } void PipelineCache::UseTrivialGeometryShader() { @@ -482,27 +473,13 @@ void PipelineCache::UseTrivialGeometryShader() { void PipelineCache::UseFragmentShader(const Pica::RegsInternal& regs, const Pica::Shader::UserConfig& user) { - const FSConfig fs_config{regs, user, profile}; - const auto [it, new_shader] = fragment_shaders.try_emplace(fs_config.Hash(), instance); - auto& shader = it->second; - if (new_shader) { - workers.QueueWork([fs_config, this, &shader]() { - const bool use_spirv = Settings::values.spirv_shader_gen.GetValue(); - if (use_spirv && !fs_config.UsesSpirvIncompatibleConfig()) { - const std::vector code = SPIRV::GenerateFragmentShader(fs_config, profile); - shader.module = CompileSPV(code, instance.GetDevice()); - } else { - const std::string code = GLSL::GenerateFragmentShader(fs_config, profile); - shader.module = - Compile(code, vk::ShaderStageFlagBits::eFragment, instance.GetDevice()); - } - shader.MarkDone(); - }); + auto res = curr_disk_cache->UseFragmentShader(regs, user); + + if (res.has_value()) { + current_shaders[ProgramType::FS] = (*res).second; + shader_hashes[ProgramType::FS] = (*res).first; } - - current_shaders[ProgramType::FS] = &shader; - shader_hashes[ProgramType::FS] = fs_config.Hash(); } bool PipelineCache::IsCacheValid(std::span data) const { @@ -558,7 +535,8 @@ bool PipelineCache::EnsureDirectories() const { }; return create_dir(FileUtil::GetUserPath(FileUtil::UserPath::ShaderDir)) && - create_dir(GetVulkanDir()) && create_dir(GetPipelineCacheDir()); + create_dir(GetVulkanDir()) && create_dir(GetPipelineCacheDir()) && + create_dir(GetTransferableDir()); } std::string PipelineCache::GetVulkanDir() const { @@ -569,6 +547,10 @@ std::string PipelineCache::GetPipelineCacheDir() const { return GetVulkanDir() + "pipeline" + DIR_SEP; } +std::string PipelineCache::GetTransferableDir() const { + return GetVulkanDir() + DIR_SEP + "transferable"; +} + void PipelineCache::SwitchPipelineCache(u64 title_id, const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback) { if (!Settings::values.use_disk_shader_cache || GetProgramID() == title_id) { @@ -579,10 +561,10 @@ void PipelineCache::SwitchPipelineCache(u64 title_id, const std::atomic_bool& st } if (callback) { - callback(VideoCore::LoadCallbackStage::Prepare, 0, 0); + callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); } if (callback) { - callback(VideoCore::LoadCallbackStage::Build, 0, 1); + callback(VideoCore::LoadCallbackStage::Build, 0, 1, "Driver Pipeline Cache"); } // Make sure we have a valid pipeline cache before switching @@ -603,10 +585,77 @@ void PipelineCache::SwitchPipelineCache(u64 title_id, const std::atomic_bool& st // Update program ID and load the new pipeline cache SetProgramID(title_id); - LoadDiskCache(stop_loading, nullptr); + LoadPipelineDiskCache(stop_loading, nullptr); + SwitchDiskCache(title_id, stop_loading, callback); if (callback) { - callback(VideoCore::LoadCallbackStage::Complete, 0, 0); + callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); + } +} + +void PipelineCache::SwitchDiskCache(u64 title_id, const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + // NOTE: curr_disk_cache can be null if emulation restarted without calling + // LoadDefaultDiskResources + + // Check if the current cache is for the specified TID. + if (curr_disk_cache && curr_disk_cache->GetProgramID() == title_id) { + return; + } + + // Search for an existing manager + size_t new_pos = 0; + for (new_pos = 0; new_pos < disk_caches.size(); new_pos++) { + if (disk_caches[new_pos]->GetProgramID() == title_id) { + break; + } + } + // Manager does not exist, create it and append to the end + if (new_pos >= disk_caches.size()) { + new_pos = disk_caches.size(); + auto& new_manager = + disk_caches.emplace_back(std::make_shared(*this, title_id)); + + if (callback) { + callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); + } + + new_manager->Init(stop_loading, callback); + + if (callback) { + callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); + } + } + + auto is_applet = [](u64 tid) { + constexpr u32 APPLET_TID_HIGH = 0x00040030; + return static_cast(tid >> 32) == APPLET_TID_HIGH; + }; + + bool prev_applet = curr_disk_cache ? is_applet(curr_disk_cache->GetProgramID()) : false; + bool new_applet = is_applet(disk_caches[new_pos]->GetProgramID()); + curr_disk_cache = disk_caches[new_pos]; + + if (prev_applet) { + // If we came from an applet, clean up all other applets + for (auto it = disk_caches.begin(); it != disk_caches.end();) { + if (it == disk_caches.begin() || *it == curr_disk_cache || + !is_applet((*it)->GetProgramID())) { + it++; + continue; + } + it = disk_caches.erase(it); + } + } + if (!new_applet) { + // If we are going into a non-applet, clean up everything + for (auto it = disk_caches.begin(); it != disk_caches.end();) { + if (it == disk_caches.begin() || *it == curr_disk_cache) { + it++; + continue; + } + it = disk_caches.erase(it); + } } } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index b7ae7011f..2bb9c750d 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -5,11 +5,11 @@ #pragma once #include -#include #include "video_core/rasterizer_interface.h" #include "video_core/renderer_vulkan/vk_graphics_pipeline.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" +#include "video_core/renderer_vulkan/vk_shader_disk_cache.h" #include "video_core/shader/generator/pica_fs_config.h" #include "video_core/shader/generator/profile.h" #include "video_core/shader/generator/shader_gen.h" @@ -59,6 +59,9 @@ public: } /// Loads the pipeline cache stored to disk + void LoadPipelineDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, + const VideoCore::DiskResourceLoadCallback& callback = {}); + void LoadDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, const VideoCore::DiskResourceLoadCallback& callback = {}); @@ -66,11 +69,14 @@ public: void SaveDiskCache(); /// Binds a pipeline using the provided information - bool BindPipeline(const PipelineInfo& info, bool wait_built = false); + bool BindPipeline(PipelineInfo& info, bool wait_built = false); + + Pica::Shader::Generator::ExtraVSConfig CalcExtraConfig( + const Pica::Shader::Generator::PicaVSConfig& config); /// Binds a PICA decompiled vertex shader bool UseProgrammableVertexShader(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, - const VertexLayout& layout, bool accurate_mul); + const VertexLayout& layout); /// Binds a passthrough vertex shader void UseTrivialVertexShader(); @@ -98,7 +104,17 @@ public: current_program_id = program_id; } + void SetAccurateMul(bool _accurate_mul) { + profile.enable_accurate_mul = _accurate_mul; + } + private: + friend ShaderDiskCache; + + /// Switches the disk cache at runtime to use a different title ID + void SwitchDiskCache(u64 title_id, const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback); + /// Builds the rasterizer pipeline layout void BuildLayout(); @@ -114,6 +130,9 @@ private: /// Returns the pipeline cache storage dir std::string GetPipelineCacheDir() const; + /// Returns the transferable shader dir + std::string GetTransferableDir() const; + private: const Instance& instance; Scheduler& scheduler; @@ -127,8 +146,6 @@ private: Common::ThreadWorker workers; PipelineInfo current_info{}; GraphicsPipeline* current_pipeline{}; - tsl::robin_map, Common::IdentityHash> - graphics_pipelines; std::array descriptor_heaps; std::array bound_descriptor_sets{}; std::array offsets{}; @@ -136,13 +153,11 @@ private: std::array shader_hashes; std::array current_shaders; - std::unordered_map programmable_vertex_map; - std::unordered_map programmable_vertex_cache; - std::unordered_map fixed_geometry_shaders; - std::unordered_map fragment_shaders; Shader trivial_vertex_shader; u64 current_program_id{0}; + std::vector> disk_caches; + std::shared_ptr curr_disk_cache{}; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index e9df994f7..b38818f88 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -87,7 +87,7 @@ RasterizerVulkan::RasterizerVulkan(Memory::MemorySystem& memory, Pica::PicaCore& // Define vertex layout for software shaders MakeSoftwareVertexLayout(); - pipeline_info.vertex_layout = software_layout; + pipeline_info.state.vertex_layout = software_layout; const vk::Device device = instance.GetDevice(); texture_lf_view = device.createBufferViewUnique({ @@ -153,65 +153,63 @@ void RasterizerVulkan::LoadDefaultDiskResources( } pipeline_cache.SetProgramID(program_id); + pipeline_cache.SetAccurateMul(accurate_mul); + pipeline_cache.LoadPipelineDiskCache(stop_loading, callback); pipeline_cache.LoadDiskCache(stop_loading, callback); + + if (callback) { + callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); + } } void RasterizerVulkan::SyncDrawState() { SyncDrawUniforms(); // SyncCullMode(); - pipeline_info.rasterization.cull_mode.Assign(regs.rasterizer.cull_mode); + pipeline_info.state.rasterization.cull_mode.Assign(regs.rasterizer.cull_mode); // If the framebuffer is flipped, request to also flip vulkan viewport const bool is_flipped = regs.framebuffer.framebuffer.IsFlipped(); - pipeline_info.rasterization.flip_viewport.Assign(is_flipped); + pipeline_info.state.rasterization.flip_viewport.Assign(is_flipped); // SyncBlendEnabled(); - pipeline_info.blending.blend_enable = regs.framebuffer.output_merger.alphablend_enable; + pipeline_info.state.blending.blend_enable = regs.framebuffer.output_merger.alphablend_enable; // SyncBlendFuncs(); - pipeline_info.blending.color_blend_eq.Assign( + pipeline_info.state.blending.color_blend_eq.Assign( regs.framebuffer.output_merger.alpha_blending.blend_equation_rgb); - pipeline_info.blending.alpha_blend_eq.Assign( + pipeline_info.state.blending.alpha_blend_eq.Assign( regs.framebuffer.output_merger.alpha_blending.blend_equation_a); - pipeline_info.blending.src_color_blend_factor.Assign( + pipeline_info.state.blending.src_color_blend_factor.Assign( regs.framebuffer.output_merger.alpha_blending.factor_source_rgb); - pipeline_info.blending.dst_color_blend_factor.Assign( + pipeline_info.state.blending.dst_color_blend_factor.Assign( regs.framebuffer.output_merger.alpha_blending.factor_dest_rgb); - pipeline_info.blending.src_alpha_blend_factor.Assign( + pipeline_info.state.blending.src_alpha_blend_factor.Assign( regs.framebuffer.output_merger.alpha_blending.factor_source_a); - pipeline_info.blending.dst_alpha_blend_factor.Assign( + pipeline_info.state.blending.dst_alpha_blend_factor.Assign( regs.framebuffer.output_merger.alpha_blending.factor_dest_a); // SyncBlendColor(); - pipeline_info.dynamic.blend_color = regs.framebuffer.output_merger.blend_const.raw; + pipeline_info.dynamic_info.blend_color = regs.framebuffer.output_merger.blend_const.raw; // SyncLogicOp(); // SyncColorWriteMask(); - pipeline_info.blending.logic_op = regs.framebuffer.output_merger.logic_op; - const bool is_logic_op_emulated = - instance.NeedsLogicOpEmulation() && !regs.framebuffer.output_merger.alphablend_enable; - const bool is_logic_op_noop = - regs.framebuffer.output_merger.logic_op == Pica::FramebufferRegs::LogicOp::NoOp; - if (is_logic_op_emulated && is_logic_op_noop) { - // Color output is disabled by logic operation. We use color write mask to skip - // color but allow depth write. - pipeline_info.blending.color_write_mask = 0; - } else { - const u32 color_mask = regs.framebuffer.framebuffer.allow_color_write != 0 - ? (regs.framebuffer.output_merger.depth_color_mask >> 8) & 0xF - : 0; - pipeline_info.blending.color_write_mask = color_mask; - } + pipeline_info.state.blending.logic_op = regs.framebuffer.output_merger.logic_op; + + const u32 color_mask = regs.framebuffer.framebuffer.allow_color_write != 0 + ? (regs.framebuffer.output_merger.depth_color_mask >> 8) & 0xF + : 0; + pipeline_info.state.blending.color_write_mask = color_mask; + // SyncStencilTest(); const auto& stencil_test = regs.framebuffer.output_merger.stencil_test; const bool test_enable = stencil_test.enable && regs.framebuffer.framebuffer.depth_format == Pica::FramebufferRegs::DepthFormat::D24S8; - pipeline_info.depth_stencil.stencil_test_enable.Assign(test_enable); - pipeline_info.depth_stencil.stencil_fail_op.Assign(stencil_test.action_stencil_fail); - pipeline_info.depth_stencil.stencil_pass_op.Assign(stencil_test.action_depth_pass); - pipeline_info.depth_stencil.stencil_depth_fail_op.Assign(stencil_test.action_depth_fail); - pipeline_info.depth_stencil.stencil_compare_op.Assign(stencil_test.func); - pipeline_info.dynamic.stencil_reference = stencil_test.reference_value; - pipeline_info.dynamic.stencil_compare_mask = stencil_test.input_mask; + pipeline_info.state.depth_stencil.stencil_test_enable.Assign(test_enable); + pipeline_info.state.depth_stencil.stencil_fail_op.Assign(stencil_test.action_stencil_fail); + pipeline_info.state.depth_stencil.stencil_pass_op.Assign(stencil_test.action_depth_pass); + pipeline_info.state.depth_stencil.stencil_depth_fail_op.Assign(stencil_test.action_depth_fail); + pipeline_info.state.depth_stencil.stencil_compare_op.Assign(stencil_test.func); + pipeline_info.dynamic_info.stencil_reference = stencil_test.reference_value; + pipeline_info.dynamic_info.stencil_compare_mask = stencil_test.input_mask; // SyncStencilWriteMask(); - pipeline_info.dynamic.stencil_write_mask = + pipeline_info.dynamic_info.stencil_write_mask = (regs.framebuffer.framebuffer.allow_depth_stencil_write != 0) ? static_cast(regs.framebuffer.output_merger.stencil_test.write_mask) : 0; @@ -222,12 +220,12 @@ void RasterizerVulkan::SyncDrawState() { ? regs.framebuffer.output_merger.depth_test_func.Value() : Pica::FramebufferRegs::CompareFunc::Always; - pipeline_info.depth_stencil.depth_test_enable.Assign(test_enabled); - pipeline_info.depth_stencil.depth_compare_op.Assign(compare_op); + pipeline_info.state.depth_stencil.depth_test_enable.Assign(test_enabled); + pipeline_info.state.depth_stencil.depth_compare_op.Assign(compare_op); // SyncDepthWriteMask(); const bool write_enable = (regs.framebuffer.framebuffer.allow_depth_stencil_write != 0 && regs.framebuffer.output_merger.depth_write_enable); - pipeline_info.depth_stencil.depth_write_enable.Assign(write_enable); + pipeline_info.state.depth_stencil.depth_write_enable.Assign(write_enable); } void RasterizerVulkan::SetupVertexArray() { @@ -246,7 +244,7 @@ void RasterizerVulkan::SetupVertexArray() { const PAddr base_address = vertex_attributes.GetPhysicalBaseAddress(); // GPUREG_ATTR_BUF_BASE const u32 stride_alignment = instance.GetMinVertexStrideAlignment(); - VertexLayout& layout = pipeline_info.vertex_layout; + VertexLayout& layout = pipeline_info.state.vertex_layout; layout.binding_count = 0; layout.attribute_count = 16; enable_attributes.fill(false); @@ -322,7 +320,8 @@ void RasterizerVulkan::SetupVertexArray() { VertexBinding& binding = layout.bindings[layout.binding_count]; binding.binding.Assign(layout.binding_count); binding.fixed.Assign(0); - binding.stride.Assign(aligned_stride); + // Will be adjusted on pipeline build, to keep the info transferable. + binding.byte_count.Assign(loader.byte_count); // Keep track of the binding offsets so we can bind the vertex buffer later binding_offsets[layout.binding_count++] = static_cast(array_offset + buffer_offset); @@ -337,7 +336,7 @@ void RasterizerVulkan::SetupVertexArray() { void RasterizerVulkan::SetupFixedAttribs() { const auto& vertex_attributes = regs.pipeline.vertex_attributes; - VertexLayout& layout = pipeline_info.vertex_layout; + VertexLayout& layout = pipeline_info.state.vertex_layout; auto [fixed_ptr, fixed_offset, _] = stream_buffer.Map(16 * sizeof(Common::Vec4f), 0); binding_offsets[layout.binding_count] = static_cast(fixed_offset); @@ -391,7 +390,7 @@ void RasterizerVulkan::SetupFixedAttribs() { VertexBinding& binding = layout.bindings[layout.binding_count]; binding.binding.Assign(layout.binding_count++); binding.fixed.Assign(1); - binding.stride.Assign(offset); + binding.byte_count.Assign(offset); stream_buffer.Commit(offset); } @@ -399,7 +398,7 @@ void RasterizerVulkan::SetupFixedAttribs() { bool RasterizerVulkan::SetupVertexShader() { MICROPROFILE_SCOPE(Vulkan_VS); return pipeline_cache.UseProgrammableVertexShader(regs, pica.vs_setup, - pipeline_info.vertex_layout, accurate_mul); + pipeline_info.state.vertex_layout); } bool RasterizerVulkan::SetupGeometryShader() { @@ -412,8 +411,9 @@ bool RasterizerVulkan::SetupGeometryShader() { // Enable the quaternion fix-up geometry-shader only if we are actually doing per-fragment // lighting and care about proper quaternions. Otherwise just use standard vertex+fragment - // shaders. We also don't need a geometry shader if the barycentric extension is supported. - if (regs.lighting.disable || instance.IsFragmentShaderBarycentricSupported()) { + // shaders. We also don't need a geometry shader if the barycentric extension is supported, + // but that will be decided later as the GS config needs to be cached anyways. + if (regs.lighting.disable) { pipeline_cache.UseTrivialGeometryShader(); return true; } @@ -431,7 +431,7 @@ bool RasterizerVulkan::AccelerateDrawBatch(bool is_indexed) { } } - pipeline_info.rasterization.topology.Assign(regs.pipeline.triangle_topology); + pipeline_info.state.rasterization.topology.Assign(regs.pipeline.triangle_topology); if (regs.pipeline.triangle_topology == TriangleTopology::Fan && !instance.IsTriangleFanSupported()) { LOG_DEBUG(Render_Vulkan, @@ -467,7 +467,7 @@ bool RasterizerVulkan::AccelerateDrawBatchInternal(bool is_indexed) { const DrawParams params = { .vertex_count = regs.pipeline.num_vertices, .vertex_offset = -static_cast(vertex_info.vs_input_index_min), - .binding_count = pipeline_info.vertex_layout.binding_count, + .binding_count = pipeline_info.state.vertex_layout.binding_count, .bindings = binding_offsets, .is_indexed = is_indexed, }; @@ -521,8 +521,8 @@ void RasterizerVulkan::DrawTriangles() { return; } - pipeline_info.rasterization.topology.Assign(Pica::PipelineRegs::TriangleTopology::List); - pipeline_info.vertex_layout = software_layout; + pipeline_info.state.rasterization.topology.Assign(Pica::PipelineRegs::TriangleTopology::List); + pipeline_info.state.vertex_layout = software_layout; pipeline_cache.UseTrivialVertexShader(); pipeline_cache.UseTrivialGeometryShader(); @@ -537,14 +537,14 @@ bool RasterizerVulkan::Draw(bool accelerate, bool is_indexed) { const bool shadow_rendering = regs.framebuffer.IsShadowRendering(); const bool has_stencil = regs.framebuffer.HasStencil(); - const bool write_color_fb = shadow_rendering || pipeline_info.blending.color_write_mask; + const bool write_color_fb = shadow_rendering || pipeline_info.GetFinalColorWriteMask(instance); const bool write_depth_fb = pipeline_info.IsDepthWriteEnabled(); const bool using_color_fb = regs.framebuffer.framebuffer.GetColorBufferPhysicalAddress() != 0 && write_color_fb; const bool using_depth_fb = !shadow_rendering && regs.framebuffer.framebuffer.GetDepthBufferPhysicalAddress() != 0 && (write_depth_fb || regs.framebuffer.output_merger.depth_test_enable != 0 || - (has_stencil && pipeline_info.depth_stencil.stencil_test_enable)); + (has_stencil && pipeline_info.state.depth_stencil.stencil_test_enable)); const auto fb_helper = res_cache.GetFramebufferSurfaces(using_color_fb, using_depth_fb); const Framebuffer* framebuffer = fb_helper.Framebuffer(); @@ -552,8 +552,8 @@ bool RasterizerVulkan::Draw(bool accelerate, bool is_indexed) { return true; } - pipeline_info.attachments.color = framebuffer->Format(SurfaceType::Color); - pipeline_info.attachments.depth = framebuffer->Format(SurfaceType::Depth); + pipeline_info.state.attachments.color = framebuffer->Format(SurfaceType::Color); + pipeline_info.state.attachments.depth = framebuffer->Format(SurfaceType::Depth); // Update scissor uniforms const auto [scissor_x1, scissor_y2, scissor_x2, scissor_y1] = fb_helper.Scissor(); @@ -585,13 +585,13 @@ bool RasterizerVulkan::Draw(bool accelerate, bool is_indexed) { // Configure viewport and scissor const auto viewport = fb_helper.Viewport(); - pipeline_info.dynamic.viewport = Common::Rectangle{ + pipeline_info.dynamic_info.viewport = Common::Rectangle{ viewport.x, viewport.y, viewport.x + viewport.width, viewport.y + viewport.height, }; - pipeline_info.dynamic.scissor = draw_rect; + pipeline_info.dynamic_info.scissor = draw_rect; // Draw the vertex batch bool succeeded = true; @@ -807,7 +807,7 @@ void RasterizerVulkan::MakeSoftwareVertexLayout() { VertexBinding& binding = software_layout.bindings[i]; binding.binding.Assign(i); binding.fixed.Assign(0); - binding.stride.Assign(sizeof(HardwareVertex)); + binding.byte_count.Assign(sizeof(HardwareVertex)); } u32 offset = 0; @@ -985,6 +985,7 @@ void RasterizerVulkan::UploadUniforms(bool accelerate_draw) { void RasterizerVulkan::SwitchDiskResources(u64 title_id) { std::atomic_bool stop_loading = false; + pipeline_cache.SetAccurateMul(accurate_mul); pipeline_cache.SwitchPipelineCache(title_id, stop_loading, switch_disk_resources_callback); } diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp new file mode 100644 index 000000000..b3f1f0613 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp @@ -0,0 +1,1487 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "common/common_paths.h" +#include "common/file_util.h" +#include "common/scm_rev.h" +#include "common/settings.h" +#include "common/static_lru_cache.h" +#include "common/zstd_compression.h" +#include "video_core/renderer_vulkan/vk_instance.h" +#include "video_core/renderer_vulkan/vk_pipeline_cache.h" +#include "video_core/renderer_vulkan/vk_shader_disk_cache.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/shader/generator/glsl_fs_shader_gen.h" +#include "video_core/shader/generator/glsl_shader_gen.h" +#include "video_core/shader/generator/shader_gen.h" +#include "video_core/shader/generator/spv_fs_shader_gen.h" + +#define MALFORMED_DISK_CACHE \ + do { \ + LOG_ERROR(Render_Vulkan, "Malformed disk shader cache"); \ + cleanup_on_error(); \ + return false; \ + } while (0) + +// Enable to debug when new cache objects are created. +// #define ENABLE_LOG_NEW_OBJECT + +#ifdef ENABLE_LOG_NEW_OBJECT +#define LOG_NEW_OBJECT LOG_DEBUG +#else +#define LOG_NEW_OBJECT(...) ((void)0) +#endif + +namespace Vulkan { + +using namespace Pica::Shader::Generator; +using namespace Pica::Shader; + +static VideoCore::DiskResourceLoadCallback MakeThrottledCallback( + VideoCore::DiskResourceLoadCallback original) { + + if (!original) + return nullptr; + + auto last_call = std::chrono::steady_clock::now() - std::chrono::milliseconds(10); + + return [original, last_call](VideoCore::LoadCallbackStage stage, std::size_t current, + std::size_t total, const std::string& name) mutable { + const auto now = std::chrono::steady_clock::now(); + if (now - last_call >= std::chrono::milliseconds(10)) { + last_call = now; + original(stage, current, total, name); + } + }; +} + +void ShaderDiskCache::Init(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + + auto new_callback = MakeThrottledCallback(callback); + + if (!stop_loading && !InitVSCache(stop_loading, new_callback)) { + RecreateCache(vs_cache, CacheFileType::VS_CACHE); + } + if (!stop_loading && !InitFSCache(stop_loading, new_callback)) { + RecreateCache(fs_cache, CacheFileType::FS_CACHE); + } + if (!stop_loading && !InitGSCache(stop_loading, new_callback)) { + RecreateCache(gs_cache, CacheFileType::GS_CACHE); + } + if (!stop_loading && !InitPLCache(stop_loading, new_callback)) { + RecreateCache(pl_cache, CacheFileType::PL_CACHE); + } +} + +std::optional> ShaderDiskCache::UseProgrammableVertexShader( + const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, const VertexLayout& layout) { + + PicaVSConfig config{regs, setup}; + + // Transfer vertex attributes to the VS config + config.state.used_input_vertex_attributes = layout.attribute_count; + for (u32 i = 0; i < layout.attribute_count; i++) { + auto& dst = config.state.input_vertex_attributes[i]; + const auto& src = layout.attributes[i]; + + dst.location = src.location; + dst.type = static_cast(src.type.Value()); + dst.size = src.size; + } + + const auto config_hash = config.Hash(); + + const auto [iter_config, new_config] = programmable_vertex_map.try_emplace(config_hash); + if (new_config) { + + LOG_NEW_OBJECT(Render_Vulkan, "New VS config {:016X}", config_hash); + + ExtraVSConfig extra_config = parent.CalcExtraConfig(config); + + auto program = + Common::HashableString(GLSL::GenerateVertexShader(setup, config, extra_config)); + + if (program.empty()) { + LOG_ERROR(Render_Vulkan, "Failed to retrieve programmable vertex shader"); + programmable_vertex_map.erase(config_hash); + return {}; + } + + const u64 spirv_id = program.Hash(); + + auto [iter_prog, new_program] = + programmable_vertex_cache.try_emplace(spirv_id, parent.instance); + auto& shader = iter_prog->second; + + if (new_program) { + LOG_NEW_OBJECT(Render_Vulkan, "New VS SPIRV {:016X}", spirv_id); + + shader.program = std::move(program); + const vk::Device device = parent.instance.GetDevice(); + parent.workers.QueueWork([device, &shader, this, spirv_id] { + auto spirv = CompileGLSL(shader.program, vk::ShaderStageFlagBits::eVertex); + AppendVSSPIRV(vs_cache, spirv, spirv_id); + shader.program.clear(); + shader.module = CompileSPV(spirv, device); + shader.MarkDone(); + }); + } + + AppendVSConfigProgram(vs_cache, config, setup, config_hash, spirv_id); + + iter_config->second = &shader; + } + + Shader* const shader{iter_config->second}; + if (!shader) { + LOG_ERROR(Render_Vulkan, "Failed to retrieve programmable vertex shader"); + return {}; + } + + return std::make_pair(config_hash, shader); +} + +std::optional> ShaderDiskCache::UseFragmentShader( + const Pica::RegsInternal& regs, const Pica::Shader::UserConfig& user) { + + const FSConfig fs_config{regs}; + const auto fs_config_hash = fs_config.Hash(); + const auto [it, new_shader] = fragment_shaders.try_emplace(fs_config_hash, parent.instance); + auto& shader = it->second; + + if (new_shader) { + LOG_NEW_OBJECT(Render_Vulkan, "New FS config {:016X}", fs_config_hash); + + parent.workers.QueueWork([fs_config, user, this, &shader, fs_config_hash]() { + std::vector spirv; + const bool use_spirv = parent.profile.vk_use_spirv_generator; + if (use_spirv && !fs_config.UsesSpirvIncompatibleConfig()) { + spirv = SPIRV::GenerateFragmentShader(fs_config, parent.profile); + shader.module = CompileSPV(spirv, parent.instance.GetDevice()); + } else { + const std::string code = + GLSL::GenerateFragmentShader(fs_config, user, parent.profile); + spirv = CompileGLSL(code, vk::ShaderStageFlagBits::eFragment); + shader.module = CompileSPV(spirv, parent.instance.GetDevice()); + } + shader.MarkDone(); + + if (user.IsCacheable()) { + // Only cache to disk if the user config is cacheable + AppendFSSPIRV(fs_cache, spirv, fs_config_hash); + FSConfigEntry entry{.version = FSConfigEntry::EXPECTED_VERSION, + .fs_config = fs_config}; + AppendFSConfig(fs_cache, entry, fs_config_hash); + } + }); + } + + return std::make_pair(fs_config_hash, &shader); +} + +std::optional> ShaderDiskCache::UseFixedGeometryShader( + const Pica::RegsInternal& regs) { + + const PicaFixedGSConfig gs_config{regs}; + const auto gs_config_hash = gs_config.Hash(); + + if (!parent.instance.UseGeometryShaders() || + parent.instance.IsFragmentShaderBarycentricSupported()) { + // Even if we don't support geometry shaders, we still need to cache them + // so that the shader cache is transferable. There is no need to cache + // or build the SPIRV. + + if (known_geometry_shaders.emplace(gs_config_hash).second) { + GSConfigEntry entry{ + .version = GSConfigEntry::EXPECTED_VERSION, + .gs_config = gs_config, + }; + AppendGSConfig(gs_cache, entry, gs_config_hash); + } + + return std::make_pair(gs_config_hash, nullptr); + } else { + auto [it, new_shader] = fixed_geometry_shaders.try_emplace(gs_config_hash, parent.instance); + auto& shader = it->second; + + if (new_shader) { + LOG_NEW_OBJECT(Render_Vulkan, "New GS config {:016X}", gs_config_hash); + + parent.workers.QueueWork([gs_config, this, &shader, gs_config_hash]() { + ExtraFixedGSConfig extra; + extra.use_clip_planes = parent.profile.has_clip_planes; + extra.separable_shader = true; + + const auto code = GLSL::GenerateFixedGeometryShader(gs_config, extra); + const auto spirv = CompileGLSL(code, vk::ShaderStageFlagBits::eGeometry); + shader.module = CompileSPV(spirv, parent.instance.GetDevice()); + shader.MarkDone(); + + AppendGSSPIRV(gs_cache, spirv, gs_config_hash); + GSConfigEntry entry{ + .version = GSConfigEntry::EXPECTED_VERSION, + .gs_config = gs_config, + }; + AppendGSConfig(gs_cache, entry, gs_config_hash); + }); + } + + return std::make_pair(gs_config_hash, &shader); + } +} + +GraphicsPipeline* ShaderDiskCache::GetPipeline(const PipelineInfo& info) { + + u64 hash = info.Hash(); + u64 optimized_hash = info.state.OptimizedHash(parent.instance); + + auto [it, new_pipeline] = graphics_pipelines.try_emplace(optimized_hash); + if (new_pipeline) { + if (!parent.instance.UseGeometryShaders() || + parent.instance.IsFragmentShaderBarycentricSupported()) { + // If we don't need geometry shaders disable + // them before building the pipeline. It's done here + // so that the shader ID could be hashed and saved with + // the pipeline info so that it is transferable. + parent.UseTrivialGeometryShader(); + } + it.value() = std::make_unique( + parent.instance, parent.renderpass_cache, info, *parent.pipeline_cache, + *parent.pipeline_layout, parent.current_shaders, &parent.workers); + } + + if (known_graphic_pipelines.emplace(hash).second) { + LOG_NEW_OBJECT(Render_Vulkan, "New Pipeline {:016X}", hash); + + PLConfigEntry entry{ + .version = PLConfigEntry::EXPECTED_VERSION, + .pl_info = info.state, + }; + + AppendPLConfig(pl_cache, entry, hash); + } + + return it.value().get(); +} + +ShaderDiskCache::SourceFileCacheVersionHash ShaderDiskCache::GetSourceFileCacheVersionHash() { + SourceFileCacheVersionHash hash{}; + const std::size_t length = std::min(std::strlen(Common::g_shader_cache_version), hash.size()); + std::memcpy(hash.data(), Common::g_shader_cache_version, length); + return hash; +} + +ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadFirst() { + return ReadAt(0); +} + +ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadNext(const CacheEntry& previous) { + if (!previous.valid) + return CacheEntry(); + + return ReadAt(previous.position + previous.header.entry_size); +} + +std::pair +ShaderDiskCache::CacheFile::ReadNextHeader( + const ShaderDiskCache::CacheEntry::CacheEntryHeader& previous, size_t previous_position) { + + size_t new_pos = previous_position + previous.entry_size; + + return {new_pos, ReadAtHeader(new_pos)}; +} + +ShaderDiskCache::CacheEntry::CacheEntryHeader ShaderDiskCache::CacheFile::ReadAtHeader( + size_t position) { + + CacheEntry::CacheEntryHeader header; + + if (file.ReadAtArray(&header, 1, position) == sizeof(CacheEntry::CacheEntryHeader)) { + return header; + } + + return CacheEntry::CacheEntryHeader(); +} + +ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) { + CacheEntry res{}; + res.position = position; + + constexpr u32 headers_size = + sizeof(CacheEntry::CacheEntryHeader) + sizeof(CacheEntry::CacheEntryFooter); + + res.header = ReadAtHeader(position); + + if (res.header.Valid()) { + + // We have everything validated, read the data. + u32 payload_size = res.header.entry_size - headers_size; + std::vector payload(payload_size); + + if (file.ReadAtBytes(payload.data(), payload_size, + position + sizeof(CacheEntry::CacheEntryHeader)) == payload_size) { + // Decompress data if needed + if (res.header.zstd_compressed) { + if (Common::Compression::GetDecompressedSize(payload) < + CacheEntry::MAX_ENTRY_SIZE) { + res.data = Common::Compression::DecompressDataZSTD(payload); + res.valid = true; + } + } else { + res.data = std::move(payload); + res.valid = true; + } + } + } + return res; +} + +size_t ShaderDiskCache::CacheFile::GetTotalEntries() { + if (biggest_entry_id != SIZE_MAX) { + return biggest_entry_id + 1; + } + + const size_t file_size = file.GetSize(); + CacheEntry::CacheEntryFooter footer{}; + + if (file.ReadAtArray(&footer, 1, file_size - sizeof(footer)) == sizeof(footer) && + footer.version == CacheEntry::CacheEntryFooter::ENTRY_VERSION) { + biggest_entry_id = footer.entry_id; + } + + return biggest_entry_id + 1; +} + +bool ShaderDiskCache::CacheFile::Append(CacheEntryType type, u64 id, std::span data, + bool compress) { + std::scoped_lock lock(mutex); + + std::span data_final; + std::vector data_compress; + + CacheEntry::CacheEntryHeader header{}; + CacheEntry::CacheEntryFooter footer{}; + + constexpr u32 headers_size = + sizeof(CacheEntry::CacheEntryHeader) + sizeof(CacheEntry::CacheEntryFooter); + + if (compress) { + data_compress = Common::Compression::CompressDataZSTDDefault(data); + data_final = data_compress; + header.zstd_compressed.Assign(true); + } else { + data_final = data; + } + header.entry_version = CacheEntry::CacheEntryHeader::ENTRY_VERSION; + footer.version.Assign(CacheEntry::CacheEntryFooter::ENTRY_VERSION); + header.entry_size = footer.entry_size = data_final.size() + headers_size; + footer.entry_id.Assign(biggest_entry_id++); + + header.type = type; + header.id = id; + + std::vector out_data(data_final.size() + headers_size); + memcpy(out_data.data(), &header, sizeof(header)); + memcpy(out_data.data() + sizeof(header), data_final.data(), data_final.size()); + memcpy(out_data.data() + sizeof(header) + data_final.size(), &footer, sizeof(footer)); + + return file.WriteBytes(out_data.data(), out_data.size()) == out_data.size(); +} + +bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { + switch (mode) { + case CacheOpMode::READ: { + file = FileUtil::IOFile(filepath, "rb"); + bool is_open = file.IsOpen(); + if (is_open) { + GetTotalEntries(); + } + return is_open; + } + case CacheOpMode::APPEND: { + GetTotalEntries(); + file.Close(); + if (biggest_entry_id == SIZE_MAX) { + // Cannot append if getting total items fails + return false; + } + + file = FileUtil::IOFile(filepath, "ab"); + return file.IsOpen(); + } + case CacheOpMode::DELETE: { + biggest_entry_id = 0; + file.Close(); + FileUtil::Delete(filepath); + return true; + } + case CacheOpMode::RECREATE: { + SwitchMode(CacheOpMode::DELETE); + return SwitchMode(CacheOpMode::APPEND); + } + default: + UNREACHABLE(); + } + return false; +} + +std::string ShaderDiskCache::GetVSFile(u64 title_id, bool is_temp) const { + return parent.GetTransferableDir() + DIR_SEP + fmt::format("{:016X}_vs", title_id) + + (is_temp ? "_temp" : "") + ".vkch"; +} + +std::string ShaderDiskCache::GetFSFile(u64 title_id, bool is_temp) const { + return parent.GetTransferableDir() + DIR_SEP + fmt::format("{:016X}_fs", title_id) + + (is_temp ? "_temp" : "") + ".vkch"; +} + +std::string ShaderDiskCache::GetGSFile(u64 title_id, bool is_temp) const { + return parent.GetTransferableDir() + DIR_SEP + fmt::format("{:016X}_gs", title_id) + + (is_temp ? "_temp" : "") + ".vkch"; +} + +std::string ShaderDiskCache::GetPLFile(u64 title_id, bool is_temp) const { + return parent.GetTransferableDir() + DIR_SEP + fmt::format("{:016X}_pl", title_id) + + (is_temp ? "_temp" : "") + ".vkch"; +} + +bool ShaderDiskCache::RecreateCache(CacheFile& file, CacheFileType type) { + file.SwitchMode(CacheFile::CacheOpMode::RECREATE); + + std::array build_name{}; + size_t name_len = std::strlen(Common::g_build_fullname); + memcpy(build_name.data(), Common::g_build_fullname, std::min(name_len, build_name.size())); + + auto get_hash = [](CacheFileType type) { + switch (type) { + case CacheFileType::VS_CACHE: + return PicaVSConfigState::StructHash(); + case CacheFileType::FS_CACHE: + return FSConfig::StructHash(); + case CacheFileType::GS_CACHE: + return PicaGSConfigState::StructHash(); + case CacheFileType::PL_CACHE: + return StaticPipelineInfo::StructHash(); + default: + UNREACHABLE(); + return u64{}; + }; + }; + + FileInfoEntry entry{ + .cache_magic = FileInfoEntry::CACHE_FILE_MAGIC, + .file_version = FileInfoEntry::CACHE_FILE_VERSION, + .config_struct_hash = get_hash(type), + .file_type = type, + .source_hash = GetSourceFileCacheVersionHash(), + .build_name = build_name, + .profile = parent.profile, + }; + + file.Append(CacheEntryType::FILE_INFO, 0, entry, false); + return true; +} + +bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + std::vector pending_configs; + std::unordered_map pending_programs; + std::unique_ptr shader_setup; + std::unique_ptr regenerate_file; + + auto cleanup_on_error = [&]() { + programmable_vertex_cache.clear(); + programmable_vertex_map.clear(); + known_vertex_programs.clear(); + if (regenerate_file) { + regenerate_file->SwitchMode(CacheFile::CacheOpMode::DELETE); + } + }; + + LOG_INFO(Render_Vulkan, "Loading VS disk shader cache for title {:016X}", title_id); + + vs_cache.SetFilePath(GetVSFile(title_id, false)); + + if (!vs_cache.SwitchMode(CacheFile::CacheOpMode::READ)) { + LOG_INFO(Render_Vulkan, "Missing VS disk shader cache for title {:016X}", title_id); + cleanup_on_error(); + return false; + } + + u32 tot_entries = vs_cache.GetTotalEntries(); + auto curr = vs_cache.ReadFirst(); + if (!curr.Valid() || curr.Type() != CacheEntryType::FILE_INFO) { + MALFORMED_DISK_CACHE; + } + + const FileInfoEntry* file_info = curr.Payload(); + if (!file_info || file_info->cache_magic != FileInfoEntry::CACHE_FILE_MAGIC || + file_info->file_version != FileInfoEntry::CACHE_FILE_VERSION || + file_info->file_type != CacheFileType::VS_CACHE) { + MALFORMED_DISK_CACHE; + } + + if (file_info->config_struct_hash != PicaVSConfigState::StructHash()) { + LOG_ERROR(Render_Vulkan, + "Cache was created for a different PicaVSConfigState, resetting..."); + cleanup_on_error(); + return false; + } + + if (file_info->source_hash != GetSourceFileCacheVersionHash()) { + LOG_INFO(Render_Vulkan, "Cache contains old vertex program, cache needs regeneration."); + regenerate_file = std::make_unique(GetVSFile(title_id, true)); + } + + if (file_info->profile != parent.profile && !regenerate_file) { + LOG_INFO(Render_Vulkan, + "Cache has driver and user settings mismatch, cache needs regeneration."); + regenerate_file = std::make_unique(GetVSFile(title_id, true)); + } + + if (regenerate_file) { + RecreateCache(*regenerate_file, CacheFileType::VS_CACHE); + } + + CacheEntry::CacheEntryHeader curr_header = curr.Header(); + size_t curr_offset = curr.Position(); + + size_t current_callback_index = 0; + size_t tot_callback_index = tot_entries - 1; + + // Scan the entire file first, while keeping track of configs and programs. + // SPIRV can be compiled directly and will be linked to the proper config entries + // later. + for (int i = 1; i < tot_entries; i++) { + + if (stop_loading) { + cleanup_on_error(); + return true; + } + + std::tie(curr_offset, curr_header) = vs_cache.ReadNextHeader(curr_header, curr_offset); + + if (!curr_header.Valid()) { + MALFORMED_DISK_CACHE; + } + + LOG_DEBUG(Render_Vulkan, "Processing ID: {:016X} (type {})", curr_header.Id(), + curr_header.Type()); + + if (curr_header.Type() == CacheEntryType::VS_CONFIG) { + pending_configs.push_back(curr_offset); + } else if (curr_header.Type() == CacheEntryType::VS_PROGRAM) { + pending_programs.try_emplace(curr_header.Id(), curr_offset); + + // We won't use this entry sequentially again, so report progress. + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Vertex Shader"); + } + } else if (curr_header.Type() == CacheEntryType::VS_SPIRV) { + + // Only use SPIRV entries if we are not regenerating the cache, as the driver or + // user settings do not match, which could lead to different SPIRV. + // These will be re-created from the cached config and programs later. + if (!regenerate_file) { + LOG_DEBUG(Render_Vulkan, " processing SPIRV."); + + curr = vs_cache.ReadAt(curr_offset); + if (!curr.Valid() || curr.Type() != CacheEntryType::VS_SPIRV) { + MALFORMED_DISK_CACHE; + } + + const u8* spirv_data = curr.Data().data(); + const size_t spirv_size = curr.Data().size(); + + auto [iter_prog, new_program] = + programmable_vertex_cache.try_emplace(curr.Id(), parent.instance); + if (new_program) { + LOG_DEBUG(Render_Vulkan, " compiling SPIRV."); + + const auto spirv = std::span( + reinterpret_cast(spirv_data), spirv_size / sizeof(u32)); + + iter_prog->second.module = CompileSPV(spirv, parent.instance.GetDevice()); + iter_prog->second.MarkDone(); + + if (!iter_prog->second.module) { + // Compilation failed for some reason, remove from cache to let it + // be re-generated at runtime or during config and program processing. + LOG_ERROR(Render_Vulkan, "Unexpected program compilation failure"); + programmable_vertex_cache.erase(iter_prog); + } + } + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Vertex Shader"); + } + } else { + MALFORMED_DISK_CACHE; + } + } + + // Once we have all the shader instances created from SPIRV, we can link them to the VS configs. + LOG_DEBUG(Render_Vulkan, "Linking with config entries."); + + // Mmultiple config entries may point to the same program entry. We could load all program + // entries to memory to prevent having to read them from disk on every config entry, but program + // entries are pretty big (around 50KB each). A LRU cache is a middle point between disk access + // and memory usage. + std::unique_ptr> program_lru = + std::make_unique>(); + + for (auto& offset : pending_configs) { + if (stop_loading) { + cleanup_on_error(); + return true; + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Vertex Shader"); + } + + curr = vs_cache.ReadAt(offset); + const VSConfigEntry* entry; + + if (!curr.Valid() || curr.Type() != CacheEntryType::VS_CONFIG || + !(entry = curr.Payload()) || + entry->version != VSConfigEntry::EXPECTED_VERSION) { + MALFORMED_DISK_CACHE; + } + + if (curr.Id() != entry->vs_config.Hash()) { + LOG_ERROR(Render_Vulkan, "Unexpected PicaVSConfig hash mismatch"); + continue; + } + + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + + auto [iter_config, new_config] = programmable_vertex_map.try_emplace(curr.Id()); + if (new_config) { + // New config entry, usually always taken unless there is duplicate entries on the cache + // for some reason. + + auto shader_it = programmable_vertex_cache.find(entry->spirv_entry_id); + if (shader_it != programmable_vertex_cache.end()) { + // The config entry uses a SPIRV entry that was already compiled (this is the usual + // path when the cache doesn't need to be re-generated). + + LOG_DEBUG(Render_Vulkan, " linked with existing SPIRV {:016X}.", + entry->spirv_entry_id); + + iter_config->second = &shader_it->second; + + if (regenerate_file) { + // In case we are re-generating the cache, we could only have gotten here if the + // SPIRV was already compiled and cached, so only cache the config. + AppendVSConfig(*regenerate_file, *entry, curr.Id()); + } + + bool new_program = known_vertex_programs.emplace(entry->program_entry_id).second; + if (new_program && regenerate_file) { + // If the vertex program is not known at this point we need to save it as well. + // This can happen to config entries that compile to the same SPIRV but use + // different program code (maybe because garbage data was in the program + // buffer). + auto program_it = pending_programs.find(entry->program_entry_id); + if (program_it == pending_programs.end()) { + // Program code not in disk cache, should never happen. + LOG_ERROR(Render_Vulkan, "Missing program code for config entry"); + programmable_vertex_map.erase(iter_config); + continue; + } + + // This is very rare so no need to use the LRU. + auto program_cache_entry = vs_cache.ReadAt(program_it->second); + const VSProgramEntry* program_entry; + + if (!program_cache_entry.Valid() || + program_cache_entry.Type() != CacheEntryType::VS_PROGRAM || + !(program_entry = program_cache_entry.Payload()) || + program_entry->version != VSProgramEntry::EXPECTED_VERSION) { + MALFORMED_DISK_CACHE; + } + + AppendVSProgram(*regenerate_file, *program_entry, entry->program_entry_id); + } + } else { + // Cached SPIRV not found, need to recompile. + + // Search program entry in a LRU first, to prevent having to read from the cache + // file on each separate config entry. + auto [found, program_lru_entry] = program_lru->request(entry->program_entry_id); + if (!found) { + LOG_DEBUG(Render_Vulkan, " reading program {:016X}.", + entry->program_entry_id); + + // Program not on the LRU, need to read it from cache file + auto program_it = pending_programs.find(entry->program_entry_id); + if (program_it == pending_programs.end()) { + // Program code not in disk cache, should never happen. + LOG_ERROR(Render_Vulkan, "Missing program code for config entry"); + programmable_vertex_map.erase(iter_config); + continue; + } + + auto program_cache_entry = vs_cache.ReadAt(program_it->second); + const VSProgramEntry* program_entry; + + if (!program_cache_entry.Valid() || + program_cache_entry.Type() != CacheEntryType::VS_PROGRAM || + !(program_entry = program_cache_entry.Payload()) || + program_entry->version != VSProgramEntry::EXPECTED_VERSION) { + MALFORMED_DISK_CACHE; + } + + program_lru_entry = *program_entry; + + bool new_program = + known_vertex_programs.emplace(entry->program_entry_id).second; + + if (new_program && regenerate_file) { + // When regenerating, only append if it's a new program entry not seen + // before. + AppendVSProgram(*regenerate_file, program_lru_entry, + entry->program_entry_id); + } + } + + // Recompile SPIRV from config and program now. + LOG_DEBUG(Render_Vulkan, " using program {:016X}.", entry->program_entry_id); + + shader_setup = std::make_unique(); + shader_setup->UpdateProgramCode(program_lru_entry.program_code, + program_lru_entry.program_len); + shader_setup->UpdateSwizzleData(program_lru_entry.swizzle_code, + program_lru_entry.swizzle_len); + shader_setup->DoProgramCodeFixup(); + + if (entry->vs_config.state.program_hash != shader_setup->GetProgramCodeHash() || + entry->vs_config.state.swizzle_hash != shader_setup->GetSwizzleDataHash()) { + LOG_ERROR(Render_Vulkan, "Unexpected ShaderSetup hash mismatch"); + programmable_vertex_map.erase(iter_config); + continue; + } + + ExtraVSConfig extra_config = parent.CalcExtraConfig(entry->vs_config); + + auto program_glsl = Common::HashableString( + GLSL::GenerateVertexShader(*shader_setup, entry->vs_config, extra_config)); + if (program_glsl.empty()) { + LOG_ERROR(Render_Vulkan, "Failed to retrieve programmable vertex shader"); + programmable_vertex_map.erase(iter_config); + continue; + } + + const u64 spirv_id = program_glsl.Hash(); + + auto [iter_prog, new_spirv] = + programmable_vertex_cache.try_emplace(spirv_id, parent.instance); + + LOG_DEBUG(Render_Vulkan, " processing SPIRV."); + + if (new_spirv) { + LOG_DEBUG(Render_Vulkan, " compiling SPIRV."); + + auto spirv = CompileGLSL(program_glsl, vk::ShaderStageFlagBits::eVertex); + + iter_prog->second.module = CompileSPV(spirv, parent.instance.GetDevice()); + iter_prog->second.MarkDone(); + + if (regenerate_file) { + // If we are regenerating, save the new spirv to disk. + AppendVSSPIRV(*regenerate_file, spirv, spirv_id); + } + } + + if (regenerate_file) { + // If we are regenerating, save the config entry to the cache. We need to make a + // copy first because it's possible the SPIRV id has changed and we need to + // adjust it. + std::unique_ptr entry_copy = + std::make_unique(*entry); + entry_copy->spirv_entry_id = spirv_id; + AppendVSConfig(*regenerate_file, *entry_copy, curr.Id()); + } + + // Asign the SPIRV shader to the config + iter_config->second = &iter_prog->second; + + LOG_DEBUG(Render_Vulkan, " linked with new SPIRV {:016X}.", + entry->spirv_entry_id); + } + } + } + + if (regenerate_file) { + // If we are regenerating, replace the old file with the new one. + vs_cache.SwitchMode(CacheFile::CacheOpMode::DELETE); + regenerate_file.reset(); + FileUtil::Rename(GetVSFile(title_id, true), GetVSFile(title_id, false)); + } + + // Switch to append mode to receive new entries. + vs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); + return true; +} + +bool ShaderDiskCache::InitFSCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + std::vector> pending_configs; + std::unique_ptr regenerate_file; + + auto cleanup_on_error = [&]() { + fragment_shaders.clear(); + if (regenerate_file) { + regenerate_file->SwitchMode(CacheFile::CacheOpMode::DELETE); + } + }; + + LOG_INFO(Render_Vulkan, "Loading FS disk shader cache for title {:016X}", title_id); + + fs_cache.SetFilePath(GetFSFile(title_id, false)); + + if (!fs_cache.SwitchMode(CacheFile::CacheOpMode::READ)) { + LOG_INFO(Render_Vulkan, "Missing FS disk shader cache for title {:016X}", title_id); + cleanup_on_error(); + return false; + } + + u32 tot_entries = fs_cache.GetTotalEntries(); + auto curr = fs_cache.ReadFirst(); + if (!curr.Valid() || curr.Type() != CacheEntryType::FILE_INFO) { + MALFORMED_DISK_CACHE; + } + + const FileInfoEntry* file_info = curr.Payload(); + if (!file_info || file_info->cache_magic != FileInfoEntry::CACHE_FILE_MAGIC || + file_info->file_version != FileInfoEntry::CACHE_FILE_VERSION || + file_info->file_type != CacheFileType::FS_CACHE) { + MALFORMED_DISK_CACHE; + } + + if (file_info->config_struct_hash != FSConfig::StructHash()) { + LOG_ERROR(Render_Vulkan, "Cache was created for a different FSConfig, resetting..."); + cleanup_on_error(); + return false; + } + + if (file_info->source_hash != GetSourceFileCacheVersionHash()) { + LOG_INFO(Render_Vulkan, "Cache contains old fragment program, cache needs regeneration."); + regenerate_file = std::make_unique(GetFSFile(title_id, true)); + } + + if (file_info->profile != parent.profile && !regenerate_file) { + LOG_INFO(Render_Vulkan, + "Cache has driver and user settings mismatch, cache needs regeneration."); + regenerate_file = std::make_unique(GetFSFile(title_id, true)); + } + + if (regenerate_file) { + RecreateCache(*regenerate_file, CacheFileType::FS_CACHE); + } + + CacheEntry::CacheEntryHeader curr_header = curr.Header(); + size_t curr_offset = curr.Position(); + + size_t current_callback_index = 0; + size_t tot_callback_index = tot_entries - 1; + + // Scan the entire file first, while keeping track of configs. + // SPIRV can be compiled directly, if a config has a missing + // SPIRV entry it can be regenerated later. + for (int i = 1; i < tot_entries; i++) { + if (stop_loading) { + cleanup_on_error(); + return true; + } + + std::tie(curr_offset, curr_header) = fs_cache.ReadNextHeader(curr_header, curr_offset); + + if (!curr_header.Valid()) { + MALFORMED_DISK_CACHE; + } + + LOG_DEBUG(Render_Vulkan, "Processing ID: {:016X} (type {})", curr_header.Id(), + curr_header.Type()); + + if (curr_header.Type() == CacheEntryType::FS_CONFIG) { + pending_configs.push_back({curr_header.Id(), curr_offset}); + } else if (curr_header.Type() == CacheEntryType::FS_SPIRV) { + + // Only use SPIRV entries if we are not regenerating the cache, as the driver or + // user settings do not match, which could lead to different SPIRV. + // These will be regenerated from the cached config later. + if (!regenerate_file) { + LOG_DEBUG(Render_Vulkan, " processing SPIRV."); + + curr = fs_cache.ReadAt(curr_offset); + if (!curr.Valid() || curr.Type() != CacheEntryType::FS_SPIRV) { + MALFORMED_DISK_CACHE; + } + + const u8* spirv_data = curr.Data().data(); + const size_t spirv_size = curr.Data().size(); + + auto [iter_spirv, new_program] = + fragment_shaders.try_emplace(curr.Id(), parent.instance); + if (new_program) { + LOG_DEBUG(Render_Vulkan, " compiling SPIRV."); + + const auto spirv = std::span( + reinterpret_cast(spirv_data), spirv_size / sizeof(u32)); + + iter_spirv->second.module = CompileSPV(spirv, parent.instance.GetDevice()); + iter_spirv->second.MarkDone(); + + if (!iter_spirv->second.module) { + // Compilation failed for some reason, remove from cache to let it + // be regenerated at runtime or during config processing. + LOG_ERROR(Render_Vulkan, "Unexpected program compilation failure"); + fragment_shaders.erase(iter_spirv); + } + } + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Fragment Shader"); + } + } else { + MALFORMED_DISK_CACHE; + } + } + + // Once we have all the shader instances created from SPIRV, we can link them to the FS configs. + LOG_DEBUG(Render_Vulkan, "Linking with config entries."); + + for (auto& offset : pending_configs) { + if (stop_loading) { + cleanup_on_error(); + return true; + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Fragment Shader"); + } + + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + + if (fragment_shaders.find(offset.first) != fragment_shaders.end()) { + // SPIRV of config was already compiled, no need to regenerate + // it from the cache. This can only happen if we are not regenerating + // the cache. + LOG_DEBUG(Render_Vulkan, " linked with existing SPIRV."); + continue; + } + + // Cached SPIRV not found, need to recompile. Should only happen if + // we are regenerating the cache. + + curr = fs_cache.ReadAt(offset.second); + const FSConfigEntry* entry; + + if (!curr.Valid() || curr.Type() != CacheEntryType::FS_CONFIG || + !(entry = curr.Payload()) || + entry->version != FSConfigEntry::EXPECTED_VERSION) { + MALFORMED_DISK_CACHE; + } + + const auto fs_config_hash = entry->fs_config.Hash(); + if (curr.Id() != fs_config_hash) { + LOG_ERROR(Render_Vulkan, "Unexpected FSConfig hash mismatch"); + continue; + } + + const auto [it, new_shader] = fragment_shaders.try_emplace(fs_config_hash, parent.instance); + auto& shader = it->second; + + std::vector spirv; + if (parent.profile.vk_use_spirv_generator && + !entry->fs_config.UsesSpirvIncompatibleConfig()) { + // Use SPIRV generator directly + + spirv = SPIRV::GenerateFragmentShader(entry->fs_config, parent.profile); + shader.module = CompileSPV(spirv, parent.instance.GetDevice()); + } else { + // Use GLSL generator then convert to SPIRV + + UserConfig user{}; + const std::string code_glsl = + GLSL::GenerateFragmentShader(entry->fs_config, user, parent.profile); + + if (code_glsl.empty()) { + LOG_ERROR(Render_Vulkan, "Failed to retrieve programmable vertex shader"); + fragment_shaders.erase(it); + continue; + } + + spirv = CompileGLSL(code_glsl, vk::ShaderStageFlagBits::eFragment); + shader.module = CompileSPV(spirv, parent.instance.GetDevice()); + } + shader.MarkDone(); + + if (regenerate_file) { + // Append the config and SPIRV to the new file. + AppendFSSPIRV(*regenerate_file, spirv, fs_config_hash); + AppendFSConfig(*regenerate_file, *entry, fs_config_hash); + } + + LOG_DEBUG(Render_Vulkan, " linked with new SPIRV."); + } + + if (regenerate_file) { + // If we are regenerating, replace the old file with the new one. + fs_cache.SwitchMode(CacheFile::CacheOpMode::DELETE); + regenerate_file.reset(); + FileUtil::Rename(GetFSFile(title_id, true), GetFSFile(title_id, false)); + } + + // Switch to append mode to receive new entries. + fs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); + return true; +} + +bool ShaderDiskCache::InitGSCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + std::vector> pending_configs; + std::unique_ptr regenerate_file; + + auto cleanup_on_error = [&]() { + fixed_geometry_shaders.clear(); + if (regenerate_file) { + regenerate_file->SwitchMode(CacheFile::CacheOpMode::DELETE); + } + }; + + LOG_INFO(Render_Vulkan, "Loading GS disk shader cache for title {:016X}", title_id); + + gs_cache.SetFilePath(GetGSFile(title_id, false)); + + if (!gs_cache.SwitchMode(CacheFile::CacheOpMode::READ)) { + LOG_INFO(Render_Vulkan, "Missing GS disk shader cache for title {:016X}", title_id); + cleanup_on_error(); + return false; + } + + u32 tot_entries = gs_cache.GetTotalEntries(); + auto curr = gs_cache.ReadFirst(); + if (!curr.Valid() || curr.Type() != CacheEntryType::FILE_INFO) { + MALFORMED_DISK_CACHE; + } + + const FileInfoEntry* file_info = curr.Payload(); + if (!file_info || file_info->cache_magic != FileInfoEntry::CACHE_FILE_MAGIC || + file_info->file_version != FileInfoEntry::CACHE_FILE_VERSION || + file_info->file_type != CacheFileType::GS_CACHE) { + MALFORMED_DISK_CACHE; + } + + if (file_info->config_struct_hash != PicaGSConfigState::StructHash()) { + LOG_ERROR(Render_Vulkan, + "Cache was created for a different PicaGSConfigState, resetting..."); + cleanup_on_error(); + return false; + } + + // There is no need to load geometry shaders if we don't support them. + // We can just load the known IDs and skip SPIRV and cache regeneration. + const auto geo_shaders_needed = parent.instance.UseGeometryShaders() && + !parent.instance.IsFragmentShaderBarycentricSupported(); + + if (geo_shaders_needed && file_info->source_hash != GetSourceFileCacheVersionHash()) { + LOG_INFO(Render_Vulkan, "Cache contains old fragment program, cache needs regeneration."); + regenerate_file = std::make_unique(GetGSFile(title_id, true)); + } + + if (geo_shaders_needed && file_info->profile != parent.profile && !regenerate_file) { + LOG_INFO(Render_Vulkan, + "Cache has driver and user settings mismatch, cache needs regeneration."); + regenerate_file = std::make_unique(GetGSFile(title_id, true)); + } + + if (regenerate_file) { + RecreateCache(*regenerate_file, CacheFileType::GS_CACHE); + } + + CacheEntry::CacheEntryHeader curr_header = curr.Header(); + size_t curr_offset = curr.Position(); + + size_t current_callback_index = 0; + size_t tot_callback_index = tot_entries - 1; + + // Scan the entire file first, while keeping track of configs. + // SPIRV can be compiled directly, if a config has a missing + // SPIRV entry it can be regenerated later. + for (int i = 1; i < tot_entries; i++) { + if (stop_loading) { + cleanup_on_error(); + return true; + } + + std::tie(curr_offset, curr_header) = gs_cache.ReadNextHeader(curr_header, curr_offset); + + if (!curr_header.Valid()) { + MALFORMED_DISK_CACHE; + } + + LOG_DEBUG(Render_Vulkan, "Processing ID: {:016X} (type {})", curr_header.Id(), + curr_header.Type()); + + if (curr_header.Type() == CacheEntryType::GS_CONFIG) { + if (geo_shaders_needed) { + pending_configs.push_back({curr_header.Id(), curr_offset}); + } else { + known_geometry_shaders.emplace(curr_header.Id()); + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Geometry Shader"); + } + } + } else if (curr_header.Type() == CacheEntryType::GS_SPIRV) { + + // Only use SPIRV entries if we are not regenerating the cache, as the driver or + // user settings do not match, which could lead to different SPIRV. + // These will be regenerated from the cached config later. + // Also, only use SPIRV entries if we support geometry shaders on this device. + if (geo_shaders_needed && !regenerate_file) { + LOG_DEBUG(Render_Vulkan, " processing SPIRV."); + + curr = gs_cache.ReadAt(curr_offset); + if (!curr.Valid() || curr.Type() != CacheEntryType::GS_SPIRV) { + MALFORMED_DISK_CACHE; + } + + const u8* spirv_data = curr.Data().data(); + const size_t spirv_size = curr.Data().size(); + + auto [iter_spirv, new_program] = + fixed_geometry_shaders.try_emplace(curr.Id(), parent.instance); + if (new_program) { + LOG_DEBUG(Render_Vulkan, " compiling SPIRV."); + + const auto spirv = std::span( + reinterpret_cast(spirv_data), spirv_size / sizeof(u32)); + + iter_spirv->second.module = CompileSPV(spirv, parent.instance.GetDevice()); + iter_spirv->second.MarkDone(); + + if (!iter_spirv->second.module) { + // Compilation failed for some reason, remove from cache to let it + // be regenerated at runtime or during config processing. + LOG_ERROR(Render_Vulkan, "Unexpected program compilation failure"); + fixed_geometry_shaders.erase(iter_spirv); + } + } + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Geometry Shader"); + } + } else { + MALFORMED_DISK_CACHE; + } + } + + // Once we have all the shader instances created from SPIRV, we can link them to the FS configs. + LOG_DEBUG(Render_Vulkan, "Linking with config entries."); + + for (auto& offset : pending_configs) { + if (stop_loading) { + cleanup_on_error(); + return true; + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Geometry Shader"); + } + + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + + if (fixed_geometry_shaders.find(offset.first) != fixed_geometry_shaders.end()) { + // SPIRV of config was already compiled, no need to regenerate + // it from the cache. This can only happen if we are not regenerating + // the cache. + LOG_DEBUG(Render_Vulkan, " linked with existing SPIRV."); + continue; + } + + // Cached SPIRV not found, need to recompile. Should only happen if + // we are regenerating the cache. + + curr = gs_cache.ReadAt(offset.second); + const GSConfigEntry* entry; + + if (!curr.Valid() || curr.Type() != CacheEntryType::GS_CONFIG || + !(entry = curr.Payload()) || + entry->version != GSConfigEntry::EXPECTED_VERSION) { + MALFORMED_DISK_CACHE; + } + + const auto gs_config_hash = entry->gs_config.Hash(); + if (curr.Id() != gs_config_hash) { + LOG_ERROR(Render_Vulkan, "Unexpected PicaGSConfigState hash mismatch"); + continue; + } + + const auto [it, new_shader] = + fixed_geometry_shaders.try_emplace(gs_config_hash, parent.instance); + auto& shader = it->second; + + std::vector spirv; + ExtraFixedGSConfig extra; + extra.use_clip_planes = parent.profile.has_clip_planes; + extra.separable_shader = true; + + const auto code_glsl = GLSL::GenerateFixedGeometryShader(entry->gs_config, extra); + + if (code_glsl.empty()) { + LOG_ERROR(Render_Vulkan, "Failed to retrieve fixed geometry shader"); + fixed_geometry_shaders.erase(it); + continue; + } + + spirv = CompileGLSL(code_glsl, vk::ShaderStageFlagBits::eGeometry); + shader.module = CompileSPV(spirv, parent.instance.GetDevice()); + shader.MarkDone(); + + if (regenerate_file) { + // Append the config and SPIRV to the new file. + AppendGSSPIRV(*regenerate_file, spirv, gs_config_hash); + AppendGSConfig(*regenerate_file, *entry, gs_config_hash); + } + + LOG_DEBUG(Render_Vulkan, " linked with new SPIRV."); + } + + if (regenerate_file) { + // If we are regenerating, replace the old file with the new one. + gs_cache.SwitchMode(CacheFile::CacheOpMode::DELETE); + regenerate_file.reset(); + FileUtil::Rename(GetGSFile(title_id, true), GetGSFile(title_id, false)); + } + + // Switch to append mode to receive new entries. + gs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); + return true; +} + +bool ShaderDiskCache::InitPLCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + + auto cleanup_on_error = [&]() { graphics_pipelines.clear(); }; + + LOG_INFO(Render_Vulkan, "Loading PL disk shader cache for title {:016X}", title_id); + + pl_cache.SetFilePath(GetPLFile(title_id, false)); + + if (!pl_cache.SwitchMode(CacheFile::CacheOpMode::READ)) { + LOG_INFO(Render_Vulkan, "Missing PL disk shader cache for title {:016X}", title_id); + cleanup_on_error(); + return false; + } + + u32 tot_entries = pl_cache.GetTotalEntries(); + auto curr = pl_cache.ReadFirst(); + if (!curr.Valid() || curr.Type() != CacheEntryType::FILE_INFO) { + MALFORMED_DISK_CACHE; + } + + const FileInfoEntry* file_info = curr.Payload(); + if (!file_info || file_info->cache_magic != FileInfoEntry::CACHE_FILE_MAGIC || + file_info->file_version != FileInfoEntry::CACHE_FILE_VERSION || + file_info->file_type != CacheFileType::PL_CACHE) { + MALFORMED_DISK_CACHE; + } + + if (file_info->config_struct_hash != StaticPipelineInfo::StructHash()) { + LOG_ERROR(Render_Vulkan, + "Cache was created for a different StaticPipelineInfo, resetting..."); + cleanup_on_error(); + return false; + } + + size_t current_callback_index = 0; + size_t tot_callback_index = tot_entries - 1; + + // There is only one entry type in the pipeline info cache, + // no need to keep track of anything. + for (int i = 1; i < tot_entries; i++) { + if (stop_loading) { + cleanup_on_error(); + return true; + } + + curr = pl_cache.ReadNext(curr); + + if (!curr.Valid()) { + MALFORMED_DISK_CACHE; + } + + LOG_DEBUG(Render_Vulkan, "Processing ID: {:016X} (type {})", curr.Id(), curr.Type()); + + if (curr.Type() == CacheEntryType::PL_CONFIG) { + + const PLConfigEntry* entry; + + if (!(entry = curr.Payload()) || + entry->version != PLConfigEntry::EXPECTED_VERSION) { + MALFORMED_DISK_CACHE; + } + + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, current_callback_index++, + tot_callback_index, "Pipeline"); + } + + known_graphic_pipelines.emplace(curr.Id()); + auto pl_hash_opt = entry->pl_info.OptimizedHash(parent.instance); + + if (graphics_pipelines.find(pl_hash_opt) != graphics_pipelines.end()) { + // Multiple entries can have the same optimized hash. Skip if that's the case. + LOG_DEBUG(Render_Vulkan, " skipping.", curr.Id()); + continue; + } + + // Fetch all the shaders used in the pipeline, + // if any is missing we cannot build it. + std::array shaders; + + if (entry->pl_info.shader_ids[ProgramType::VS]) { + auto it_vs = + programmable_vertex_map.find(entry->pl_info.shader_ids[ProgramType::VS]); + if (it_vs == programmable_vertex_map.end()) { + LOG_ERROR(Render_Vulkan, "Missing vertex shader {:016X} for pipeline {:016X}", + entry->pl_info.shader_ids[ProgramType::VS], curr.Id()); + continue; + } + shaders[ProgramType::VS] = it_vs->second; + } else { + shaders[ProgramType::VS] = &parent.trivial_vertex_shader; + } + + auto it_fs = fragment_shaders.find(entry->pl_info.shader_ids[ProgramType::FS]); + if (it_fs == fragment_shaders.end()) { + LOG_ERROR(Render_Vulkan, "Missing fragment shader {:016X} for pipeline {:016X}", + entry->pl_info.shader_ids[ProgramType::FS], curr.Id()); + continue; + } + shaders[ProgramType::FS] = &it_fs->second; + + if (parent.instance.UseGeometryShaders() && + !parent.instance.IsFragmentShaderBarycentricSupported() && + entry->pl_info.shader_ids[ProgramType::GS]) { + auto it_gs = + fixed_geometry_shaders.find(entry->pl_info.shader_ids[ProgramType::GS]); + if (it_gs == fixed_geometry_shaders.end()) { + LOG_ERROR(Render_Vulkan, "Missing geometry shader {:016X} for pipeline {:016X}", + entry->pl_info.shader_ids[ProgramType::GS], curr.Id()); + continue; + } + shaders[ProgramType::GS] = &it_gs->second; + } else { + shaders[ProgramType::GS] = nullptr; + } + + // Build the pipeline using the cached pipeline info. + // The dynamic state can be left default initialized. + PipelineInfo info{}; + info.state = entry->pl_info; + + auto [it_pl, _] = graphics_pipelines.try_emplace(pl_hash_opt); + it_pl.value() = std::make_unique( + parent.instance, parent.renderpass_cache, info, *parent.pipeline_cache, + *parent.pipeline_layout, shaders, &parent.workers); + + it_pl.value()->TryBuild(false); + + LOG_DEBUG(Render_Vulkan, " built."); + + } else { + MALFORMED_DISK_CACHE; + } + } + + // Switch to append mode to receive new entries. + pl_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); + return true; +} + +bool ShaderDiskCache::AppendVSConfigProgram(CacheFile& file, + const Pica::Shader::Generator::PicaVSConfig& config, + const Pica::ShaderSetup& setup, u64 config_id, + u64 spirv_id) { + + VSConfigEntry entry; + entry.version = VSConfigEntry::EXPECTED_VERSION; + entry.vs_config = config; + entry.spirv_entry_id = spirv_id; + entry.program_entry_id = + Common::HashCombine(config.state.program_hash, config.state.swizzle_hash); + + bool new_entry = known_vertex_programs.emplace(entry.program_entry_id).second; + bool prog_res = true; + if (new_entry) { + std::unique_ptr prog_entry = std::make_unique(); + prog_entry->version = VSProgramEntry::EXPECTED_VERSION; + prog_entry->program_len = setup.GetBiggestProgramSize(); + prog_entry->program_code = setup.GetProgramCode(); + prog_entry->swizzle_len = setup.GetBiggestSwizzleSize(); + prog_entry->swizzle_code = setup.GetSwizzleData(); + + prog_res = AppendVSProgram(file, *prog_entry, entry.program_entry_id); + } + + return AppendVSConfig(file, entry, config_id) && prog_res; +} + +bool ShaderDiskCache::AppendVSProgram(CacheFile& file, const VSProgramEntry& entry, + u64 program_id) { + return file.Append(CacheEntryType::VS_PROGRAM, program_id, entry, true); +} + +bool ShaderDiskCache::AppendVSConfig(CacheFile& file, const VSConfigEntry& entry, u64 config_id) { + return file.Append(CacheEntryType::VS_CONFIG, config_id, entry, true); +} + +bool ShaderDiskCache::AppendVSSPIRV(CacheFile& file, std::span program, u64 program_id) { + return file.Append(CacheEntryType::VS_SPIRV, program_id, + {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, + true); +} + +bool ShaderDiskCache::AppendFSConfig(CacheFile& file, const FSConfigEntry& entry, u64 config_id) { + return file.Append(CacheEntryType::FS_CONFIG, config_id, entry, true); +} + +bool ShaderDiskCache::AppendFSSPIRV(CacheFile& file, std::span program, u64 program_id) { + return file.Append(CacheEntryType::FS_SPIRV, program_id, + {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, + true); +} + +bool ShaderDiskCache::AppendGSConfig(CacheFile& file, const GSConfigEntry& entry, u64 config_id) { + return file.Append(CacheEntryType::GS_CONFIG, config_id, entry, true); +} + +bool ShaderDiskCache::AppendGSSPIRV(CacheFile& file, std::span program, u64 program_id) { + return file.Append(CacheEntryType::GS_SPIRV, program_id, + {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, + true); +} + +bool ShaderDiskCache::AppendPLConfig(CacheFile& file, const PLConfigEntry& entry, u64 config_id) { + return file.Append(CacheEntryType::PL_CONFIG, config_id, entry, true); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h new file mode 100644 index 000000000..451b9e078 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -0,0 +1,346 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +#include "common/common_types.h" +#include "common/file_util.h" +#include "video_core/pica/shader_setup.h" +#include "video_core/rasterizer_interface.h" +#include "video_core/renderer_vulkan/vk_graphics_pipeline.h" +#include "video_core/renderer_vulkan/vk_instance.h" +#include "video_core/shader/generator/pica_fs_config.h" +#include "video_core/shader/generator/profile.h" +#include "video_core/shader/generator/shader_gen.h" + +namespace Vulkan { + +class PipelineCache; + +class ShaderDiskCache { +public: + ShaderDiskCache(PipelineCache& _parent, u64 _title_id) : parent(_parent), title_id(_title_id) {} + + void Init(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback); + + std::optional> UseProgrammableVertexShader( + const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, const VertexLayout& layout); + std::optional> UseFragmentShader( + const Pica::RegsInternal& regs, const Pica::Shader::UserConfig& user); + std::optional> UseFixedGeometryShader( + const Pica::RegsInternal& regs); + + GraphicsPipeline* GetPipeline(const PipelineInfo& info); + + u64 GetProgramID() const { + return title_id; + } + +private: + static constexpr std::size_t SOURCE_FILE_HASH_LENGTH = 64; + using SourceFileCacheVersionHash = std::array; + + static SourceFileCacheVersionHash GetSourceFileCacheVersionHash(); + + enum class CacheFileType : u32 { + VS_CACHE = 0, + FS_CACHE = 1, + GS_CACHE = 2, + PL_CACHE = 3, + + MAX, + }; + + enum class CacheEntryType : u16 { + // Common + FILE_INFO = 0, + + // VS_CACHE + VS_CONFIG = 1, + VS_PROGRAM = 2, + VS_SPIRV = 3, + + // FS_CACHE + FS_CONFIG = 4, + FS_SPIRV = 5, + + // GS_CACHE + GS_CONFIG = 6, + GS_SPIRV = 7, + + // PL_CACHE + PL_CONFIG = 8, + + MAX, + }; + + struct FileInfoEntry { + static constexpr u32 CACHE_FILE_MAGIC = 0x48434B56; + static constexpr u32 CACHE_FILE_VERSION = 0; + + u32_le cache_magic; + u32 file_version; + u64 config_struct_hash; + CacheFileType file_type; + SourceFileCacheVersionHash source_hash; + std::array build_name; + + union { + u8 reserved[0x400]; + Pica::Shader::Profile profile; + }; + }; + static_assert(sizeof(FileInfoEntry) == 1144); + + struct VSConfigEntry { + static constexpr u8 EXPECTED_VERSION = 0; + + u8 version; // Surprise tool that can help us later + u64 program_entry_id; + u64 spirv_entry_id; + Pica::Shader::Generator::PicaVSConfig vs_config; + }; + static_assert(sizeof(VSConfigEntry) == 216); + + struct VSProgramEntry { + static constexpr u8 EXPECTED_VERSION = 0; + + u8 version; // Surprise tool that can help us later + u32 program_len; + u32 swizzle_len; + Pica::ProgramCode program_code; + Pica::SwizzleData swizzle_code; + }; + static_assert(sizeof(VSProgramEntry) == 32780); + + struct FSConfigEntry { + static constexpr u8 EXPECTED_VERSION = 0; + + u8 version; // Surprise tool that can help us later + Pica::Shader::FSConfig fs_config; + }; + static_assert(sizeof(FSConfigEntry) == 276); + + struct GSConfigEntry { + static constexpr u8 EXPECTED_VERSION = 0; + + u8 version; // Surprise tool that can help us later + Pica::Shader::Generator::PicaFixedGSConfig gs_config; + }; + static_assert(sizeof(GSConfigEntry) == 44); + + struct PLConfigEntry { + static constexpr u8 EXPECTED_VERSION = 0; + + u8 version; // Surprise tool that can help us later + StaticPipelineInfo pl_info; + }; + static_assert(sizeof(PLConfigEntry) == 152); + + class CacheFile; + class CacheEntry { + public: + static constexpr u32 MAX_ENTRY_SIZE = 4 * 1024 * 1024; + + struct CacheEntryFooter { + static constexpr u8 ENTRY_VERSION = 0x24; + union { + u32 first_word{}; + + BitField<0, 8, u32> version; + BitField<8, 24, u32> entry_id; + }; + u32 entry_size{}; + u64 reserved{}; + }; + static_assert(sizeof(CacheEntryFooter) == 0x10); + + struct CacheEntryHeader { + static constexpr u8 ENTRY_VERSION = 0x42; + u8 entry_version{}; + union { + u8 flags{}; + + BitField<0, 1, u8> zstd_compressed; + BitField<1, 7, u8> reserved; + }; + CacheEntryType type{}; + u32 entry_size{}; + u64 id{}; + + CacheEntryType Type() const { + return type; + } + + u64 Id() const { + return id; + } + + bool Valid() { + constexpr u32 headers_size = + sizeof(CacheEntry::CacheEntryHeader) + sizeof(CacheEntry::CacheEntryFooter); + + return entry_version == ENTRY_VERSION && type < CacheEntryType::MAX && + entry_size < CacheEntry::MAX_ENTRY_SIZE && entry_size >= headers_size; + } + }; + static_assert(sizeof(CacheEntryHeader) == 0x10); + + bool Valid() const { + return valid; + } + + CacheEntryType Type() const { + return header.Type(); + } + + u64 Id() const { + return header.Id(); + } + + const std::span Data() const { + return data; + } + + template + const T* Payload() const { + if (data.size() != sizeof(T)) { + return nullptr; + } + + return reinterpret_cast(data.data()); + } + + size_t Position() const { + return position; + } + + const CacheEntryHeader& Header() const { + return header; + } + + private: + friend CacheFile; + + CacheEntry() = default; + + CacheEntryHeader header{}; + + size_t position = SIZE_MAX; + bool valid = false; + std::vector data{}; + }; + + class CacheFile { + public: + enum class CacheOpMode { + READ, + APPEND, + DELETE, + RECREATE, + }; + + CacheFile() = default; + CacheFile(const std::string& _filepath) : filepath(_filepath) {} + + void SetFilePath(const std::string& path) { + filepath = path; + } + + CacheEntry ReadFirst(); + CacheEntry ReadNext(const CacheEntry& previous); + + CacheEntry ReadAt(size_t position); + + std::pair ReadNextHeader( + const ShaderDiskCache::CacheEntry::CacheEntryHeader& previous, + size_t previous_position); + + CacheEntry::CacheEntryHeader ReadAtHeader(size_t position); + + size_t GetTotalEntries(); + + template + bool Append(CacheEntryType type, u64 id, const T& object, bool compress) { + static_assert(std::is_trivially_copyable_v); + + auto bytes = std::as_bytes(std::span{&object, 1}); + auto u8_span = + std::span(reinterpret_cast(bytes.data()), bytes.size()); + return Append(type, id, u8_span, compress); + } + + bool Append(CacheEntryType type, u64 id, std::span data, bool compress); + + bool SwitchMode(CacheOpMode mode); + + private: + std::string filepath; + std::mutex mutex; + FileUtil::IOFile file{}; + size_t biggest_entry_id = SIZE_MAX; + }; + + std::string GetVSFile(u64 title_id, bool is_temp) const; + std::string GetFSFile(u64 title_id, bool is_temp) const; + std::string GetGSFile(u64 title_id, bool is_temp) const; + std::string GetPLFile(u64 title_id, bool is_temp) const; + + bool RecreateCache(CacheFile& file, CacheFileType type); + + bool InitVSCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback); + + bool InitFSCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback); + + bool InitGSCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback); + + bool InitPLCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback); + + bool AppendVSConfigProgram(CacheFile& file, const Pica::Shader::Generator::PicaVSConfig& config, + const Pica::ShaderSetup& setup, u64 config_id, u64 program_id); + bool AppendVSProgram(CacheFile& file, const VSProgramEntry& entry, u64 program_id); + bool AppendVSConfig(CacheFile& file, const VSConfigEntry& entry, u64 config_id); + bool AppendVSSPIRV(CacheFile& file, std::span program, u64 program_id); + + bool AppendFSConfig(CacheFile& file, const FSConfigEntry& entry, u64 config_id); + bool AppendFSSPIRV(CacheFile& file, std::span program, u64 program_id); + + bool AppendGSConfig(CacheFile& file, const GSConfigEntry& entry, u64 config_id); + bool AppendGSSPIRV(CacheFile& file, std::span program, u64 program_id); + + bool AppendPLConfig(CacheFile& file, const PLConfigEntry& entry, u64 config_id); + + CacheFile vs_cache; + CacheFile fs_cache; + CacheFile gs_cache; + CacheFile pl_cache; + + PipelineCache& parent; + u64 title_id; + + std::unordered_map programmable_vertex_cache; + std::unordered_map programmable_vertex_map; + std::unordered_set known_vertex_programs; + + std::unordered_map fragment_shaders; + + std::unordered_map fixed_geometry_shaders; + std::unordered_set known_geometry_shaders; + + tsl::robin_map, Common::IdentityHash> + graphics_pipelines; + std::unordered_set known_graphic_pipelines; +}; + +} // namespace Vulkan \ No newline at end of file diff --git a/src/video_core/renderer_vulkan/vk_shader_util.cpp b/src/video_core/renderer_vulkan/vk_shader_util.cpp index ee3db73de..1a5a71827 100644 --- a/src/video_core/renderer_vulkan/vk_shader_util.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_util.cpp @@ -160,8 +160,8 @@ bool InitializeCompiler() { } } // Anonymous namespace -vk::ShaderModule Compile(std::string_view code, vk::ShaderStageFlagBits stage, vk::Device device, - std::string_view premable) { +std::vector CompileGLSL(std::string_view code, vk::ShaderStageFlagBits stage, + std::string_view premable) { if (!InitializeCompiler()) { return {}; } @@ -217,7 +217,7 @@ vk::ShaderModule Compile(std::string_view code, vk::ShaderStageFlagBits stage, v LOG_INFO(Render_Vulkan, "SPIR-V conversion messages: {}", spv_messages); } - return CompileSPV(out_code, device); + return out_code; } vk::ShaderModule CompileSPV(std::span code, vk::Device device) { @@ -229,10 +229,15 @@ vk::ShaderModule CompileSPV(std::span code, vk::Device device) { try { return device.createShaderModule(shader_info); } catch (vk::SystemError& err) { - UNREACHABLE_MSG("{}", err.what()); + LOG_ERROR(Render_Vulkan, "{}", err.what()); } return {}; } +vk::ShaderModule Compile(std::string_view code, vk::ShaderStageFlagBits stage, vk::Device device, + std::string_view premable) { + return CompileSPV(CompileGLSL(code, stage, premable), device); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_shader_util.h b/src/video_core/renderer_vulkan/vk_shader_util.h index cb91aedf8..91fe9709d 100644 --- a/src/video_core/renderer_vulkan/vk_shader_util.h +++ b/src/video_core/renderer_vulkan/vk_shader_util.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -14,10 +14,9 @@ namespace Vulkan { * @brief Creates a vulkan shader module from GLSL by converting it to SPIR-V using glslang. * @param code The string containing GLSL code. * @param stage The pipeline stage the shader will be used in. - * @param device The vulkan device handle. */ -vk::ShaderModule Compile(std::string_view code, vk::ShaderStageFlagBits stage, vk::Device device, - std::string_view premable = ""); +std::vector CompileGLSL(std::string_view code, vk::ShaderStageFlagBits stage, + std::string_view premable = ""); /** * @brief Creates a vulkan shader module from SPIR-V bytecode. @@ -26,4 +25,7 @@ vk::ShaderModule Compile(std::string_view code, vk::ShaderStageFlagBits stage, v */ vk::ShaderModule CompileSPV(std::span code, vk::Device device); +vk::ShaderModule Compile(std::string_view code, vk::ShaderStageFlagBits stage, vk::Device device, + std::string_view premable = ""); + } // namespace Vulkan diff --git a/src/video_core/shader/generator/glsl_fs_shader_gen.cpp b/src/video_core/shader/generator/glsl_fs_shader_gen.cpp index 463fe1950..7d7ae63bb 100644 --- a/src/video_core/shader/generator/glsl_fs_shader_gen.cpp +++ b/src/video_core/shader/generator/glsl_fs_shader_gen.cpp @@ -101,8 +101,10 @@ layout (binding = 2, std140) uniform fs_data { }; )"; -FragmentModule::FragmentModule(const FSConfig& config_, const Profile& profile_) - : config{config_}, profile{profile_} { +FragmentModule::FragmentModule(const FSConfig& config_, const UserConfig& user_, + const Profile& profile_) + : config{config_}, user{user_}, profile{profile_} { + config.ApplyProfile(profile_); out.reserve(RESERVE_SIZE); DefineExtensions(); DefineInterface(); @@ -504,7 +506,7 @@ void FragmentModule::WriteLighting() { return fmt::format("2.0 * (sampleTexUnit{}()).rgb - 1.0", lighting.bump_selector.Value()); }; - if (config.user.use_custom_normal) { + if (user.use_custom_normal) { const auto texel = fmt::format("2.0 * (texture(tex_normal, texcoord0)).rgb - 1.0"); out += fmt::format("vec3 surface_normal = {};\n", texel); out += "vec3 surface_tangent = vec3(1.0, 0.0, 0.0);\n"; @@ -665,7 +667,7 @@ void FragmentModule::WriteLighting() { const std::string value = get_lut_value(LightingRegs::SpotlightAttenuationSampler(light_config.num), light_config.num, lighting.lut_sp.type, lighting.lut_sp.abs_input); - spot_atten = fmt::format("({:#} * {})", lighting.lut_sp.scale, value); + spot_atten = fmt::format("({:#} * {})", lighting.lut_sp.GetScale(), value); } // If enabled, compute distance attenuation value @@ -693,7 +695,7 @@ void FragmentModule::WriteLighting() { const std::string value = get_lut_value(LightingRegs::LightingSampler::Distribution0, light_config.num, lighting.lut_d0.type, lighting.lut_d0.abs_input); - d0_lut_value = fmt::format("({:#} * {})", lighting.lut_d0.scale, value); + d0_lut_value = fmt::format("({:#} * {})", lighting.lut_d0.GetScale(), value); } std::string specular_0 = fmt::format("({} * {}.specular_0)", d0_lut_value, light_src); if (light_config.geometric_factor_0) { @@ -707,7 +709,7 @@ void FragmentModule::WriteLighting() { std::string value = get_lut_value(LightingRegs::LightingSampler::ReflectRed, light_config.num, lighting.lut_rr.type, lighting.lut_rr.abs_input); - value = fmt::format("({:#} * {})", lighting.lut_rr.scale, value); + value = fmt::format("({:#} * {})", lighting.lut_rr.GetScale(), value); out += fmt::format("refl_value.r = {};\n", value); } else { out += "refl_value.r = 1.0;\n"; @@ -720,7 +722,7 @@ void FragmentModule::WriteLighting() { std::string value = get_lut_value(LightingRegs::LightingSampler::ReflectGreen, light_config.num, lighting.lut_rg.type, lighting.lut_rg.abs_input); - value = fmt::format("({:#} * {})", lighting.lut_rg.scale, value); + value = fmt::format("({:#} * {})", lighting.lut_rg.GetScale(), value); out += fmt::format("refl_value.g = {};\n", value); } else { out += "refl_value.g = refl_value.r;\n"; @@ -733,7 +735,7 @@ void FragmentModule::WriteLighting() { std::string value = get_lut_value(LightingRegs::LightingSampler::ReflectBlue, light_config.num, lighting.lut_rb.type, lighting.lut_rb.abs_input); - value = fmt::format("({:#} * {})", lighting.lut_rb.scale, value); + value = fmt::format("({:#} * {})", lighting.lut_rb.GetScale(), value); out += fmt::format("refl_value.b = {};\n", value); } else { out += "refl_value.b = refl_value.r;\n"; @@ -748,7 +750,7 @@ void FragmentModule::WriteLighting() { const std::string value = get_lut_value(LightingRegs::LightingSampler::Distribution1, light_config.num, lighting.lut_d1.type, lighting.lut_d1.abs_input); - d1_lut_value = fmt::format("({:#} * {})", lighting.lut_d1.scale, value); + d1_lut_value = fmt::format("({:#} * {})", lighting.lut_d1.GetScale(), value); } std::string specular_1 = fmt::format("({} * refl_value * {}.specular_1)", d1_lut_value, light_src); @@ -765,7 +767,7 @@ void FragmentModule::WriteLighting() { std::string value = get_lut_value(LightingRegs::LightingSampler::Fresnel, light_config.num, lighting.lut_fr.type, lighting.lut_fr.abs_input); - value = fmt::format("({:#} * {})", lighting.lut_fr.scale, value); + value = fmt::format("({:#} * {})", lighting.lut_fr.GetScale(), value); // Enabled for diffuse lighting alpha component if (lighting.enable_primary_alpha) { @@ -1311,7 +1313,7 @@ void FragmentModule::DefineBindingsVK() { if (config.framebuffer.shadow_rendering) { out += "layout(set = 2, binding = 0, r32ui) uniform uimage2D shadow_buffer;\n\n"; } - if (config.user.use_custom_normal) { + if (user.use_custom_normal) { out += "layout(set = 2, binding = 1) uniform sampler2D tex_normal;\n"; } } @@ -1332,7 +1334,7 @@ void FragmentModule::DefineBindingsGL() { } // Utility textures - if (config.user.use_custom_normal) { + if (user.use_custom_normal) { out += "layout(binding = 6) uniform sampler2D tex_normal;\n"; } if (use_blend_fallback) { @@ -1752,8 +1754,9 @@ void FragmentModule::DefineTexUnitSampler(u32 texture_unit) { out += "\n}\n"; } -std::string GenerateFragmentShader(const FSConfig& config, const Profile& profile) { - FragmentModule module{config, profile}; +std::string GenerateFragmentShader(const FSConfig& config, const UserConfig& user, + const Profile& profile) { + FragmentModule module{config, user, profile}; return module.Generate(); } diff --git a/src/video_core/shader/generator/glsl_fs_shader_gen.h b/src/video_core/shader/generator/glsl_fs_shader_gen.h index 10dab5b26..55d5b5256 100644 --- a/src/video_core/shader/generator/glsl_fs_shader_gen.h +++ b/src/video_core/shader/generator/glsl_fs_shader_gen.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,7 +10,7 @@ namespace Pica::Shader::Generator::GLSL { class FragmentModule { public: - explicit FragmentModule(const FSConfig& config, const Profile& profile); + explicit FragmentModule(const FSConfig& config, const UserConfig& user, const Profile& profile); ~FragmentModule(); /// Emits GLSL source corresponding to the provided pica fragment configuration @@ -83,7 +83,8 @@ private: void DefineTexUnitSampler(u32 i); private: - const FSConfig& config; + FSConfig config; + const UserConfig& user; const Profile& profile; std::string out; bool use_blend_fallback{}; @@ -97,6 +98,7 @@ private: * configuration (NOTE: Use state in this struct only, not the Pica registers!) * @returns String of the shader source code */ -std::string GenerateFragmentShader(const FSConfig& config, const Profile& profile); +std::string GenerateFragmentShader(const FSConfig& config, const UserConfig& user, + const Profile& profile); } // namespace Pica::Shader::Generator::GLSL diff --git a/src/video_core/shader/generator/glsl_shader_gen.cpp b/src/video_core/shader/generator/glsl_shader_gen.cpp index d6cc390cc..b537df375 100644 --- a/src/video_core/shader/generator/glsl_shader_gen.cpp +++ b/src/video_core/shader/generator/glsl_shader_gen.cpp @@ -153,7 +153,9 @@ std::string_view MakeLoadPrefix(AttribLoadFlags flag) { } std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& config, - bool separable_shader) { + const ExtraVSConfig& extra) { + const bool separable_shader = extra.separable_shader; + std::string out; if (separable_shader) { out += "#extension GL_ARB_separate_shader_objects : enable\n"; @@ -179,7 +181,7 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c auto program_source = DecompileProgram(setup.GetProgramCode(), setup.GetSwizzleData(), config.state.main_offset, - get_input_reg, get_output_reg, config.state.sanitize_mul); + get_input_reg, get_output_reg, extra.sanitize_mul); if (program_source.empty()) { return ""; @@ -188,7 +190,7 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c // input attributes declaration for (std::size_t i = 0; i < used_regs.size(); ++i) { if (used_regs[i]) { - const auto flags = config.state.load_flags[i]; + const auto flags = extra.load_flags[i]; const std::string_view prefix = MakeLoadPrefix(flags); out += fmt::format("layout(location = {0}) in {1}vec4 vs_in_typed_reg{0};\n", i, prefix); @@ -197,7 +199,7 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c } out += '\n'; - if (config.state.use_geometry_shader) { + if (extra.use_geometry_shader) { // output attributes declaration for (u32 i = 0; i < config.state.num_outputs; ++i) { if (separable_shader) { @@ -207,19 +209,21 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c } out += "void EmitVtx() {}\n"; } else { - out += GetVertexInterfaceDeclaration(true, config.state.use_clip_planes, separable_shader); + out += GetVertexInterfaceDeclaration(true, extra.use_clip_planes, separable_shader); // output attributes declaration for (u32 i = 0; i < config.state.num_outputs; ++i) { out += fmt::format("vec4 vs_out_attr{};\n", i); } - const auto semantic = - [&state = config.state](VSOutputAttributes::Semantic slot_semantic) -> std::string { + const auto semantic_maps = config.state.gs_state.GetSemanticMaps(); + + const auto semantic = [&state = config.state, &semantic_maps]( + VSOutputAttributes::Semantic slot_semantic) -> std::string { const u32 slot = static_cast(slot_semantic); - const u32 attrib = state.gs_state.semantic_maps[slot].attribute_index; - const u32 comp = state.gs_state.semantic_maps[slot].component_index; - if (attrib < state.gs_state.gs_output_attributes) { + const u32 attrib = semantic_maps[slot].attribute_index; + const u32 comp = semantic_maps[slot].component_index; + if (attrib < state.gs_state.gs_output_attributes_count) { return fmt::format("vs_out_attr{}.{}", attrib, "xyzw"[comp]); } return "1.0"; @@ -242,7 +246,7 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c out += " vtx_pos.y = -vtx_pos.y;\n"; out += " }\n"; out += " gl_Position = vec4(vtx_pos.x, vtx_pos.y, -vtx_pos.z, vtx_pos.w);\n"; - if (config.state.use_clip_planes) { + if (extra.use_clip_planes) { out += " gl_ClipDistance[0] = -vtx_pos.z;\n"; // fixed PICA clipping plane z <= 0 out += " if (enable_clip1) {\n"; out += " gl_ClipDistance[1] = dot(clip_coef, vtx_pos);\n"; @@ -279,7 +283,7 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c for (std::size_t i = 0; i < used_regs.size(); ++i) { if (used_regs[i]) { out += fmt::format("vs_in_reg{0} = vec4(vs_in_typed_reg{0});\n", i); - if (True(config.state.load_flags[i] & AttribLoadFlags::ZeroW)) { + if (True(extra.load_flags[i] & AttribLoadFlags::ZeroW)) { out += fmt::format("vs_in_reg{0}.w = 0;\n", i); } } @@ -294,13 +298,15 @@ std::string GenerateVertexShader(const ShaderSetup& setup, const PicaVSConfig& c return out; } -static std::string GetGSCommonSource(const PicaGSConfigState& state, bool separable_shader) { - std::string out = GetVertexInterfaceDeclaration(true, state.use_clip_planes, separable_shader); +static std::string GetGSCommonSource(const PicaGSConfigState& state, + const ExtraFixedGSConfig& extra) { + std::string out = + GetVertexInterfaceDeclaration(true, extra.use_clip_planes, extra.separable_shader); out += VSUniformBlockDef; out += '\n'; - for (u32 i = 0; i < state.vs_output_attributes; ++i) { - if (separable_shader) { + for (u32 i = 0; i < state.vs_output_attributes_count; ++i) { + if (extra.separable_shader) { out += fmt::format("layout(location = {}) ", i); } out += fmt::format("in vec4 vs_out_attr{}[];\n", i); @@ -309,14 +315,17 @@ static std::string GetGSCommonSource(const PicaGSConfigState& state, bool separa out += R"( struct Vertex { )"; - out += fmt::format(" vec4 attributes[{}];\n", state.gs_output_attributes); + out += fmt::format(" vec4 attributes[{}];\n", state.gs_output_attributes_count); out += "};\n\n"; - const auto semantic = [&state](VSOutputAttributes::Semantic slot_semantic) -> std::string { + const auto semantic_maps = state.GetSemanticMaps(); + + const auto semantic = + [&state, &semantic_maps](VSOutputAttributes::Semantic slot_semantic) -> std::string { const u32 slot = static_cast(slot_semantic); - const u32 attrib = state.semantic_maps[slot].attribute_index; - const u32 comp = state.semantic_maps[slot].component_index; - if (attrib < state.gs_output_attributes) { + const u32 attrib = semantic_maps[slot].attribute_index; + const u32 comp = semantic_maps[slot].component_index; + if (attrib < state.gs_output_attributes_count) { return fmt::format("vtx.attributes[{}].{}", attrib, "xyzw"[comp]); } return "1.0"; @@ -339,7 +348,7 @@ struct Vertex { out += " vtx_pos.y = -vtx_pos.y;\n"; out += " }\n"; out += " gl_Position = vec4(vtx_pos.x, vtx_pos.y, -vtx_pos.z, vtx_pos.w);\n"; - if (state.use_clip_planes) { + if (extra.use_clip_planes) { out += " gl_ClipDistance[0] = -vtx_pos.z;\n"; // fixed PICA clipping plane z <= 0 out += " if (enable_clip1) {\n"; out += " gl_ClipDistance[1] = dot(clip_coef, vtx_pos);\n"; @@ -388,7 +397,10 @@ void EmitPrim(Vertex vtx0, Vertex vtx1, Vertex vtx2) { return out; }; -std::string GenerateFixedGeometryShader(const PicaFixedGSConfig& config, bool separable_shader) { +std::string GenerateFixedGeometryShader(const PicaFixedGSConfig& config, + const ExtraFixedGSConfig& extra) { + const bool separable_shader = extra.separable_shader; + std::string out; if (separable_shader) { out += "#extension GL_ARB_separate_shader_objects : enable\n"; @@ -400,7 +412,7 @@ layout(triangle_strip, max_vertices = 3) out; )"; - out += GetGSCommonSource(config.state, separable_shader); + out += GetGSCommonSource(config.state, extra); out += R"( void main() { @@ -408,8 +420,8 @@ void main() { )"; for (u32 vtx = 0; vtx < 3; ++vtx) { out += fmt::format(" prim_buffer[{}].attributes = vec4[{}](", vtx, - config.state.gs_output_attributes); - for (u32 i = 0; i < config.state.vs_output_attributes; ++i) { + config.state.gs_output_attributes_count); + for (u32 i = 0; i < config.state.vs_output_attributes_count; ++i) { out += fmt::format("{}vs_out_attr{}[{}]", i == 0 ? "" : ", ", i, vtx); } out += ");\n"; diff --git a/src/video_core/shader/generator/glsl_shader_gen.h b/src/video_core/shader/generator/glsl_shader_gen.h index 20d826f90..5d6ad8661 100644 --- a/src/video_core/shader/generator/glsl_shader_gen.h +++ b/src/video_core/shader/generator/glsl_shader_gen.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -27,7 +27,9 @@ struct ShaderSetup; namespace Pica::Shader::Generator { struct PicaVSConfig; +struct ExtraVSConfig; struct PicaFixedGSConfig; +struct ExtraFixedGSConfig; } // namespace Pica::Shader::Generator namespace Pica::Shader::Generator::GLSL { @@ -44,12 +46,13 @@ std::string GenerateTrivialVertexShader(bool use_clip_planes, bool separable_sha * @returns String of the shader source code; empty on failure */ std::string GenerateVertexShader(const Pica::ShaderSetup& setup, const PicaVSConfig& config, - bool separable_shader); + const ExtraVSConfig& extra); /** * Generates the GLSL fixed geometry shader program source code for non-GS PICA pipeline * @returns String of the shader source code */ -std::string GenerateFixedGeometryShader(const PicaFixedGSConfig& config, bool separable_shader); +std::string GenerateFixedGeometryShader(const PicaFixedGSConfig& config, + const ExtraFixedGSConfig& extra_config); } // namespace Pica::Shader::Generator::GLSL diff --git a/src/video_core/shader/generator/pica_fs_config.cpp b/src/video_core/shader/generator/pica_fs_config.cpp index 34dee8712..0f3854ce8 100644 --- a/src/video_core/shader/generator/pica_fs_config.cpp +++ b/src/video_core/shader/generator/pica_fs_config.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -6,7 +6,7 @@ namespace Pica::Shader { -FramebufferConfig::FramebufferConfig(const Pica::RegsInternal& regs, const Profile& profile) { +FramebufferConfig::FramebufferConfig(const Pica::RegsInternal& regs) { const auto& output_merger = regs.framebuffer.output_merger; scissor_test_mode.Assign(regs.rasterizer.scissor_test.mode); depthmap_enable.Assign(regs.rasterizer.depthmap_enable); @@ -15,31 +15,43 @@ FramebufferConfig::FramebufferConfig(const Pica::RegsInternal& regs, const Profi ? output_merger.alpha_test.func.Value() : Pica::FramebufferRegs::CompareFunc::Always); - // Emulate logic op in the shader if needed and not supported. + alphablend_enable.Assign(output_merger.alphablend_enable); + requested_logic_op = output_merger.logic_op; + logic_op.Assign(Pica::FramebufferRegs::LogicOp::Copy); - if (!profile.has_logic_op && !regs.framebuffer.output_merger.alphablend_enable) { - logic_op.Assign(regs.framebuffer.output_merger.logic_op); + + if (alphablend_enable) { + rgb_blend.eq = output_merger.alpha_blending.blend_equation_rgb.Value(); + rgb_blend.src_factor = output_merger.alpha_blending.factor_source_rgb; + rgb_blend.dst_factor = output_merger.alpha_blending.factor_dest_rgb; + + alpha_blend.eq = output_merger.alpha_blending.blend_equation_a.Value(); + alpha_blend.src_factor = output_merger.alpha_blending.factor_source_a; + alpha_blend.dst_factor = output_merger.alpha_blending.factor_dest_a; + } +} + +void FramebufferConfig::ApplyProfile(const Profile& profile) { + // Emulate logic op in the shader if needed and not supported. + if (!profile.has_logic_op && !alphablend_enable) { + logic_op.Assign(requested_logic_op); } - const auto alpha_eq = output_merger.alpha_blending.blend_equation_a.Value(); - const auto rgb_eq = output_merger.alpha_blending.blend_equation_rgb.Value(); - if (!profile.has_blend_minmax_factor && output_merger.alphablend_enable) { - if (rgb_eq == Pica::FramebufferRegs::BlendEquation::Max || - rgb_eq == Pica::FramebufferRegs::BlendEquation::Min) { - rgb_blend.eq = rgb_eq; - rgb_blend.src_factor = output_merger.alpha_blending.factor_source_rgb; - rgb_blend.dst_factor = output_merger.alpha_blending.factor_dest_rgb; + // Min/max blend emulation + if (!profile.has_blend_minmax_factor && alphablend_enable) { + if (rgb_blend.eq != Pica::FramebufferRegs::BlendEquation::Min && + rgb_blend.eq != Pica::FramebufferRegs::BlendEquation::Max) { + rgb_blend = {}; } - if (alpha_eq == Pica::FramebufferRegs::BlendEquation::Max || - alpha_eq == Pica::FramebufferRegs::BlendEquation::Min) { - alpha_blend.eq = alpha_eq; - alpha_blend.src_factor = output_merger.alpha_blending.factor_source_a; - alpha_blend.dst_factor = output_merger.alpha_blending.factor_dest_a; + + if (alpha_blend.eq != Pica::FramebufferRegs::BlendEquation::Min && + alpha_blend.eq != Pica::FramebufferRegs::BlendEquation::Max) { + alpha_blend = {}; } } } -TextureConfig::TextureConfig(const Pica::TexturingRegs& regs, const Profile& profile) { +TextureConfig::TextureConfig(const Pica::TexturingRegs& regs) { texture0_type.Assign(regs.texture0.type); texture2_use_coord1.Assign(regs.main_config.texture2_use_coord1 != 0); combiner_buffer_input.Assign(regs.tev_combiner_buffer_input.update_mask_rgb.Value() | @@ -48,16 +60,13 @@ TextureConfig::TextureConfig(const Pica::TexturingRegs& regs, const Profile& pro fog_flip.Assign(regs.fog_flip != 0); shadow_texture_orthographic.Assign(regs.shadow.orthographic != 0); - // Emulate custom border color if needed and not supported. const auto pica_textures = regs.GetTextures(); for (u32 tex_index = 0; tex_index < 3; tex_index++) { - const auto& config = pica_textures[tex_index].config; - texture_border_color[tex_index].enable_s.Assign( - !profile.has_custom_border_color && - config.wrap_s == Pica::TexturingRegs::TextureConfig::WrapMode::ClampToBorder); - texture_border_color[tex_index].enable_t.Assign( - !profile.has_custom_border_color && - config.wrap_t == Pica::TexturingRegs::TextureConfig::WrapMode::ClampToBorder); + requested_wrap[tex_index].s = pica_textures[tex_index].config.wrap_s; + requested_wrap[tex_index].t = pica_textures[tex_index].config.wrap_t; + + texture_border_color[tex_index].enable_s.Assign(false); + texture_border_color[tex_index].enable_t.Assign(false); } const auto& stages = regs.GetTevStages(); @@ -75,6 +84,21 @@ TextureConfig::TextureConfig(const Pica::TexturingRegs& regs, const Profile& pro } } +void TextureConfig::ApplyProfile(const Profile& profile) { + // Emulate custom border color if needed and not supported. + if (profile.has_custom_border_color) { + return; + } + + for (u32 i = 0; i < 3; i++) { + texture_border_color[i].enable_s.Assign( + requested_wrap[i].s == Pica::TexturingRegs::TextureConfig::WrapMode::ClampToBorder); + + texture_border_color[i].enable_t.Assign( + requested_wrap[i].t == Pica::TexturingRegs::TextureConfig::WrapMode::ClampToBorder); + } +} + LightConfig::LightConfig(const Pica::LightingRegs& regs) { if (regs.disable) { return; @@ -116,48 +140,48 @@ LightConfig::LightConfig(const Pica::LightingRegs& regs) { if (lut_d0.enable) { lut_d0.abs_input.Assign(regs.abs_lut_input.disable_d0 == 0); lut_d0.type.Assign(regs.lut_input.d0.Value()); - lut_d0.scale = regs.lut_scale.GetScale(regs.lut_scale.d0); + lut_d0.SetScale(regs.lut_scale.GetScale(regs.lut_scale.d0)); } lut_d1.enable.Assign(regs.config1.disable_lut_d1 == 0); if (lut_d1.enable) { lut_d1.abs_input.Assign(regs.abs_lut_input.disable_d1 == 0); lut_d1.type.Assign(regs.lut_input.d1.Value()); - lut_d1.scale = regs.lut_scale.GetScale(regs.lut_scale.d1); + lut_d1.SetScale(regs.lut_scale.GetScale(regs.lut_scale.d1)); } // This is a dummy field due to lack of the corresponding register lut_sp.enable.Assign(1); lut_sp.abs_input.Assign(regs.abs_lut_input.disable_sp == 0); lut_sp.type.Assign(regs.lut_input.sp.Value()); - lut_sp.scale = regs.lut_scale.GetScale(regs.lut_scale.sp); + lut_sp.SetScale(regs.lut_scale.GetScale(regs.lut_scale.sp)); lut_fr.enable.Assign(regs.config1.disable_lut_fr == 0); if (lut_fr.enable) { lut_fr.abs_input.Assign(regs.abs_lut_input.disable_fr == 0); lut_fr.type.Assign(regs.lut_input.fr.Value()); - lut_fr.scale = regs.lut_scale.GetScale(regs.lut_scale.fr); + lut_fr.SetScale(regs.lut_scale.GetScale(regs.lut_scale.fr)); } lut_rr.enable.Assign(regs.config1.disable_lut_rr == 0); if (lut_rr.enable) { lut_rr.abs_input.Assign(regs.abs_lut_input.disable_rr == 0); lut_rr.type.Assign(regs.lut_input.rr.Value()); - lut_rr.scale = regs.lut_scale.GetScale(regs.lut_scale.rr); + lut_rr.SetScale(regs.lut_scale.GetScale(regs.lut_scale.rr)); } lut_rg.enable.Assign(regs.config1.disable_lut_rg == 0); if (lut_rg.enable) { lut_rg.abs_input.Assign(regs.abs_lut_input.disable_rg == 0); lut_rg.type.Assign(regs.lut_input.rg.Value()); - lut_rg.scale = regs.lut_scale.GetScale(regs.lut_scale.rg); + lut_rg.SetScale(regs.lut_scale.GetScale(regs.lut_scale.rg)); } lut_rb.enable.Assign(regs.config1.disable_lut_rb == 0); if (lut_rb.enable) { lut_rb.abs_input.Assign(regs.abs_lut_input.disable_rb == 0); lut_rb.type.Assign(regs.lut_input.rb.Value()); - lut_rb.scale = regs.lut_scale.GetScale(regs.lut_scale.rb); + lut_rb.SetScale(regs.lut_scale.GetScale(regs.lut_scale.rb)); } } @@ -186,8 +210,8 @@ ProcTexConfig::ProcTexConfig(const Pica::TexturingRegs& regs) { lut_filter.Assign(regs.proctex_lut.filter); } -FSConfig::FSConfig(const Pica::RegsInternal& regs, const UserConfig& user_, const Profile& profile) - : framebuffer{regs, profile}, texture{regs.texturing, profile}, lighting{regs.lighting}, - proctex{regs.texturing}, user{user_} {} +FSConfig::FSConfig(const Pica::RegsInternal& regs) + : framebuffer{regs}, texture{regs.texturing}, lighting{regs.lighting}, proctex{regs.texturing} { +} } // namespace Pica::Shader diff --git a/src/video_core/shader/generator/pica_fs_config.h b/src/video_core/shader/generator/pica_fs_config.h index 18d6d92d7..736880a5b 100644 --- a/src/video_core/shader/generator/pica_fs_config.h +++ b/src/video_core/shader/generator/pica_fs_config.h @@ -8,16 +8,46 @@ #include "video_core/pica/regs_internal.h" #include "video_core/shader/generator/profile.h" +#define LAYOUT_HASH static_cast(sizeof(T)), static_cast(alignof(T)) +#define FIELD_HASH(x) static_cast(offsetof(T, x)), static_cast(sizeof(x)) + namespace Pica::Shader { +/** + * WARNING! + * + * The following structs are saved to the disk as cache entries! + * Any modification to their members will invalidate the cache, breaking their + * transferable properties. + * + * Only modify the entries if such modifications are justified. + * If the struct is modified in a way that results in the exact same layout + * (for example, replacing an u8 with another u8 in the same place), then bump + * the struct's STRUCT_VERSION value. + */ + struct BlendConfig { Pica::FramebufferRegs::BlendEquation eq; Pica::FramebufferRegs::BlendFactor src_factor; Pica::FramebufferRegs::BlendFactor dst_factor; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = BlendConfig; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(eq), FIELD_HASH(src_factor), FIELD_HASH(dst_factor)); + } }; +static_assert(std::has_unique_object_representations_v); struct FramebufferConfig { - explicit FramebufferConfig(const Pica::RegsInternal& regs, const Profile& profile); + explicit FramebufferConfig(const Pica::RegsInternal& regs); union { u32 raw{}; @@ -26,9 +56,33 @@ struct FramebufferConfig { BitField<5, 1, Pica::RasterizerRegs::DepthBuffering> depthmap_enable; BitField<6, 4, Pica::FramebufferRegs::LogicOp> logic_op; BitField<10, 1, u32> shadow_rendering; + BitField<11, 1, u32> alphablend_enable; }; BlendConfig rgb_blend{}; BlendConfig alpha_blend{}; + + Pica::FramebufferRegs::LogicOp requested_logic_op{}; + + void ApplyProfile(const Profile& profile); + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = FramebufferConfig; + return Common::HashCombine( + STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(alpha_test_func), FIELD_HASH(scissor_test_mode), FIELD_HASH(depthmap_enable), + FIELD_HASH(logic_op), FIELD_HASH(shadow_rendering), FIELD_HASH(alphablend_enable), + FIELD_HASH(rgb_blend), FIELD_HASH(alpha_blend), FIELD_HASH(requested_logic_op), + + // nested layout + BlendConfig::StructHash()); + } }; static_assert(std::has_unique_object_representations_v); @@ -46,15 +100,48 @@ struct TevStageConfigRaw { .scales_raw = scales_raw, }; } + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = TevStageConfigRaw; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(sources_raw), FIELD_HASH(modifiers_raw), + FIELD_HASH(ops_raw), FIELD_HASH(scales_raw)); + } }; +static_assert(std::has_unique_object_representations_v); union TextureBorder { + u32 raw{}; BitField<0, 1, u32> enable_s; BitField<1, 1, u32> enable_t; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = TextureBorder; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(enable_s), FIELD_HASH(enable_t), + + // nested layout + BlendConfig::StructHash()); + } }; +static_assert(std::has_unique_object_representations_v); struct TextureConfig { - explicit TextureConfig(const Pica::TexturingRegs& regs, const Profile& profile); + explicit TextureConfig(const Pica::TexturingRegs& regs); union { u32 raw{}; @@ -67,6 +154,48 @@ struct TextureConfig { }; std::array texture_border_color{}; std::array tev_stages{}; + + struct TextureWrap { + Pica::TexturingRegs::TextureConfig::WrapMode s; + Pica::TexturingRegs::TextureConfig::WrapMode t; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = TextureWrap; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(s), FIELD_HASH(t)); + } + }; + std::array requested_wrap{}; + + void ApplyProfile(const Profile& profile); + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = TextureConfig; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(texture0_type), FIELD_HASH(texture2_use_coord1), + FIELD_HASH(combiner_buffer_input), FIELD_HASH(fog_mode), + FIELD_HASH(fog_flip), FIELD_HASH(shadow_texture_orthographic), + FIELD_HASH(texture_border_color), FIELD_HASH(tev_stages), + FIELD_HASH(requested_wrap), + + // nested layout + TextureBorder::StructHash(), TevStageConfigRaw::StructHash(), + TextureWrap::StructHash()); + } }; static_assert(std::has_unique_object_representations_v); @@ -80,6 +209,22 @@ union Light { BitField<7, 1, u16> geometric_factor_0; BitField<8, 1, u16> geometric_factor_1; BitField<9, 1, u16> shadow_enable; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = Light; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(num), FIELD_HASH(directional), + FIELD_HASH(two_sided_diffuse), FIELD_HASH(dist_atten_enable), + FIELD_HASH(spot_atten_enable), FIELD_HASH(geometric_factor_0), + FIELD_HASH(geometric_factor_1), FIELD_HASH(shadow_enable)); + } }; static_assert(std::has_unique_object_representations_v); @@ -90,8 +235,31 @@ struct LutConfig { BitField<1, 1, u32> abs_input; BitField<2, 3, Pica::LightingRegs::LightingLutInput> type; }; - f32 scale; + + // Needed for std::has_unique_object_representations_v + u32 scale_bits; + inline f32 GetScale() const noexcept { + return std::bit_cast(scale_bits); + } + inline void SetScale(f32 value) noexcept { + scale_bits = std::bit_cast(value); + } + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = LutConfig; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(enable), FIELD_HASH(abs_input), FIELD_HASH(type), + FIELD_HASH(scale_bits)); + } }; +static_assert(std::has_unique_object_representations_v); struct LightConfig { explicit LightConfig(const Pica::LightingRegs& regs); @@ -122,7 +290,32 @@ struct LightConfig { LutConfig lut_rg{}; LutConfig lut_rb{}; std::array lights{}; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = LightConfig; + return Common::HashCombine( + STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(enable), FIELD_HASH(src_num), FIELD_HASH(bump_mode), + FIELD_HASH(bump_selector), FIELD_HASH(bump_renorm), FIELD_HASH(clamp_highlights), + FIELD_HASH(config), FIELD_HASH(enable_primary_alpha), + FIELD_HASH(enable_secondary_alpha), FIELD_HASH(enable_shadow), + FIELD_HASH(shadow_primary), FIELD_HASH(shadow_secondary), FIELD_HASH(shadow_invert), + FIELD_HASH(shadow_alpha), FIELD_HASH(shadow_selector), FIELD_HASH(lut_d0), + FIELD_HASH(lut_d1), FIELD_HASH(lut_sp), FIELD_HASH(lut_fr), FIELD_HASH(lut_rr), + FIELD_HASH(lut_rg), FIELD_HASH(lut_rb), FIELD_HASH(lights), + + // nested layout + LutConfig::StructHash(), Light::StructHash()); + } }; +static_assert(std::has_unique_object_representations_v); struct ProcTexConfig { explicit ProcTexConfig(const Pica::TexturingRegs& regs); @@ -148,18 +341,43 @@ struct ProcTexConfig { s32 lut_offset3{}; u16 lod_min{}; u16 lod_max{}; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = ProcTexConfig; + return Common::HashCombine( + STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(enable), FIELD_HASH(coord), FIELD_HASH(u_clamp), FIELD_HASH(v_clamp), + FIELD_HASH(color_combiner), FIELD_HASH(alpha_combiner), FIELD_HASH(lut_filter), + FIELD_HASH(separate_alpha), FIELD_HASH(noise_enable), FIELD_HASH(u_shift), + FIELD_HASH(v_shift), FIELD_HASH(lut_width), FIELD_HASH(lut_offset0), + FIELD_HASH(lut_offset1), FIELD_HASH(lut_offset2), FIELD_HASH(lut_offset3), + FIELD_HASH(lod_min), FIELD_HASH(lod_max)); + } }; static_assert(std::has_unique_object_representations_v); union UserConfig { u32 raw{}; BitField<0, 1, u32> use_custom_normal; + + // Whether a FSConfig + UserConfig combination can be + // cached to disk. Right now, this is true if the + // UserConfig was default constructed + bool IsCacheable() const { + return raw == u32{}; + } }; static_assert(std::has_unique_object_representations_v); struct FSConfig { - explicit FSConfig(const Pica::RegsInternal& regs, const UserConfig& user, - const Profile& profile); + explicit FSConfig(const Pica::RegsInternal& regs); [[nodiscard]] bool TevStageUpdatesCombinerBufferColor(u32 stage_index) const { return (stage_index < 4) && (texture.combiner_buffer_input & (1 << stage_index)); @@ -180,6 +398,11 @@ struct FSConfig { framebuffer.shadow_rendering.Value(); } + void ApplyProfile(const Profile& profile) { + framebuffer.ApplyProfile(profile); + texture.ApplyProfile(profile); + } + bool operator==(const FSConfig& other) const noexcept { return std::memcmp(this, &other, sizeof(FSConfig)) == 0; } @@ -192,8 +415,27 @@ struct FSConfig { TextureConfig texture; LightConfig lighting; ProcTexConfig proctex; - UserConfig user; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = FSConfig; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(framebuffer), FIELD_HASH(texture), + FIELD_HASH(lighting), FIELD_HASH(proctex), + + // nested layout + FramebufferConfig::StructHash(), TextureConfig::StructHash(), + LightConfig::StructHash(), ProcTexConfig::StructHash()); + } }; +static_assert(std::has_unique_object_representations_v); +static_assert(std::is_trivially_copyable_v); } // namespace Pica::Shader @@ -205,3 +447,6 @@ struct hash { } }; } // namespace std + +#undef FIELD_HASH +#undef LAYOUT_HASH diff --git a/src/video_core/shader/generator/profile.h b/src/video_core/shader/generator/profile.h index 819baae33..a8fb21a8f 100644 --- a/src/video_core/shader/generator/profile.h +++ b/src/video_core/shader/generator/profile.h @@ -1,27 +1,53 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include +#include "common/common_types.h" + namespace Pica::Shader { +struct VKFormatTraits { + u8 transfer_support{}; + u8 blit_support{}; + u8 attachment_support{}; + u8 storage_support{}; + u8 needs_conversion{}; + u8 needs_emulation{}; + u32 usage_flags{}; + u32 aspect_flags{}; + u32 native_format{}; + + auto operator<=>(const VKFormatTraits&) const = default; +}; + struct Profile { - bool has_separable_shaders{}; - bool has_clip_planes{}; - bool has_geometry_shader{}; - bool has_custom_border_color{}; - bool has_fragment_shader_interlock{}; - bool has_fragment_shader_barycentric{}; - bool has_blend_minmax_factor{}; - bool has_minus_one_to_one_range{}; - bool has_logic_op{}; - bool has_gl_ext_framebuffer_fetch{}; - bool has_gl_arm_framebuffer_fetch{}; - bool has_gl_nv_fragment_shader_interlock{}; - bool has_gl_intel_fragment_shader_ordering{}; - bool has_gl_nv_fragment_shader_barycentric{}; - bool is_vulkan{}; + u8 enable_accurate_mul{}; + u8 has_separable_shaders{}; + u8 has_clip_planes{}; + u8 has_geometry_shader{}; + u8 has_custom_border_color{}; + u8 has_fragment_shader_interlock{}; + u8 has_fragment_shader_barycentric{}; + u8 has_blend_minmax_factor{}; + u8 has_minus_one_to_one_range{}; + u8 has_logic_op{}; + + u8 has_gl_ext_framebuffer_fetch{}; + u8 has_gl_arm_framebuffer_fetch{}; + u8 has_gl_nv_fragment_shader_interlock{}; + u8 has_gl_intel_fragment_shader_ordering{}; + u8 has_gl_nv_fragment_shader_barycentric{}; + + u8 vk_disable_spirv_optimizer{}; + u8 vk_use_spirv_generator{}; + std::array vk_format_traits{}; + + u8 is_vulkan{}; + + auto operator<=>(const Profile&) const = default; }; } // namespace Pica::Shader diff --git a/src/video_core/shader/generator/shader_gen.cpp b/src/video_core/shader/generator/shader_gen.cpp index dafbdd8e8..70af303fb 100644 --- a/src/video_core/shader/generator/shader_gen.cpp +++ b/src/video_core/shader/generator/shader_gen.cpp @@ -11,19 +11,25 @@ namespace Pica::Shader::Generator { -void PicaGSConfigState::Init(const Pica::RegsInternal& regs, bool use_clip_planes_) { - use_clip_planes = use_clip_planes_; +void PicaGSConfigState::Init(const Pica::RegsInternal& regs) { + vs_output_attributes_count = Common::BitSet(regs.vs.output_mask).Count(); + gs_output_attributes_count = vs_output_attributes_count; + vs_output_total = regs.rasterizer.vs_output_total; - vs_output_attributes = Common::BitSet(regs.vs.output_mask).Count(); - gs_output_attributes = vs_output_attributes; + memcpy(vs_output_attributes.data(), regs.rasterizer.vs_output_attributes, + vs_output_total * sizeof(Pica::RasterizerRegs::VSOutputAttributes)); +} + +std::array PicaGSConfigState::GetSemanticMaps() const { + std::array semantic_maps{}; semantic_maps.fill({16, 0}); - for (u32 attrib = 0; attrib < regs.rasterizer.vs_output_total; ++attrib) { + for (u32 attrib = 0; attrib < vs_output_total; ++attrib) { const std::array semantics{ - regs.rasterizer.vs_output_attributes[attrib].map_x.Value(), - regs.rasterizer.vs_output_attributes[attrib].map_y.Value(), - regs.rasterizer.vs_output_attributes[attrib].map_z.Value(), - regs.rasterizer.vs_output_attributes[attrib].map_w.Value(), + vs_output_attributes[attrib].map_x.Value(), + vs_output_attributes[attrib].map_y.Value(), + vs_output_attributes[attrib].map_z.Value(), + vs_output_attributes[attrib].map_w.Value(), }; for (u32 comp = 0; comp < 4; ++comp) { const auto semantic = semantics[comp]; @@ -34,39 +40,34 @@ void PicaGSConfigState::Init(const Pica::RegsInternal& regs, bool use_clip_plane } } } + + return semantic_maps; } -void PicaVSConfigState::Init(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, - bool use_clip_planes_, bool use_geometry_shader_, bool accurate_mul_) { - use_clip_planes = use_clip_planes_; - use_geometry_shader = use_geometry_shader_; - sanitize_mul = accurate_mul_; - +void PicaVSConfigState::Init(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup) { setup.DoProgramCodeFixup(); program_hash = setup.GetProgramCodeHash(); swizzle_hash = setup.GetSwizzleDataHash(); main_offset = regs.vs.main_offset; + lighting_disable = regs.lighting.disable; + num_outputs = 0; - load_flags.fill(AttribLoadFlags::Float); output_map.fill(16); for (u32 reg : Common::BitSet(regs.vs.output_mask)) { output_map[reg] = num_outputs++; } - if (!use_geometry_shader_) { - gs_state.Init(regs, use_clip_planes_); - } + gs_state.Init(regs); } -PicaVSConfig::PicaVSConfig(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, - bool use_clip_planes_, bool use_geometry_shader_, bool accurate_mul_) { - state.Init(regs, setup, use_clip_planes_, use_geometry_shader_, accurate_mul_); +PicaVSConfig::PicaVSConfig(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup) { + state.Init(regs, setup); } -PicaFixedGSConfig::PicaFixedGSConfig(const Pica::RegsInternal& regs, bool use_clip_planes_) { - state.Init(regs, use_clip_planes_); +PicaFixedGSConfig::PicaFixedGSConfig(const Pica::RegsInternal& regs) { + state.Init(regs); } } // namespace Pica::Shader::Generator diff --git a/src/video_core/shader/generator/shader_gen.h b/src/video_core/shader/generator/shader_gen.h index 560dfc6a1..babb50111 100644 --- a/src/video_core/shader/generator/shader_gen.h +++ b/src/video_core/shader/generator/shader_gen.h @@ -1,10 +1,14 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "common/hash.h" +#include "video_core/pica/regs_rasterizer.h" + +#define LAYOUT_HASH static_cast(sizeof(T)), static_cast(alignof(T)) +#define FIELD_HASH(x) static_cast(offsetof(T, x)), static_cast(sizeof(x)) namespace Pica { struct RegsInternal; @@ -31,7 +35,7 @@ enum Attributes { ATTRIBUTE_VIEW, }; -enum class AttribLoadFlags { +enum class AttribLoadFlags : u32 { Float = 1 << 0, Sint = 1 << 1, Uint = 1 << 2, @@ -39,25 +43,53 @@ enum class AttribLoadFlags { }; DECLARE_ENUM_FLAG_OPERATORS(AttribLoadFlags) +/** + * WARNING! + * + * The following structs are saved to the disk as cache entries! + * Any modification to their members will invalidate the cache, breaking their + * transferable properties. + * + * Only modify the entries if such modifications are justified. + * If the struct is modified in a way that results in the exact same layout + * (for example, replacing an u8 with another u8 in the same place), then bump + * the struct's STRUCT_VERSION value. + */ + /** * This struct contains common information to identify a GLSL geometry shader generated from * PICA geometry shader. */ struct PicaGSConfigState { - void Init(const Pica::RegsInternal& regs, bool use_clip_planes_); + void Init(const Pica::RegsInternal& regs); - bool use_clip_planes; + u32 vs_output_attributes_count; + u32 gs_output_attributes_count; + u32 vs_output_total; - u32 vs_output_attributes; - u32 gs_output_attributes; + std::array vs_output_attributes; + // semantic_maps[semantic name] -> GS output attribute index + component index struct SemanticMap { u32 attribute_index; u32 component_index; }; + std::array GetSemanticMaps() const; - // semantic_maps[semantic name] -> GS output attribute index + component index - std::array semantic_maps; + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = PicaGSConfigState; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(vs_output_attributes_count), + FIELD_HASH(gs_output_attributes_count), + FIELD_HASH(vs_output_total), FIELD_HASH(vs_output_attributes)); + } }; /** @@ -65,25 +97,48 @@ struct PicaGSConfigState { * PICA vertex shader. */ struct PicaVSConfigState { - void Init(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, bool use_clip_planes_, - bool use_geometry_shader_, bool accurate_mul_); - - bool use_clip_planes; - bool use_geometry_shader; + void Init(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup); + u8 lighting_disable; u64 program_hash; u64 swizzle_hash; u32 main_offset; - bool sanitize_mul; u32 num_outputs; - // Load operations to apply to the input vertex data - std::array load_flags; // output_map[output register index] -> output attribute index std::array output_map; + // These represent relevant input vertex attributes + struct VAttr { + u8 location; + u8 type; + u8 size; + }; + u8 used_input_vertex_attributes; + std::array input_vertex_attributes; + PicaGSConfigState gs_state; + + static consteval u64 StructHash() { + constexpr u64 STRUCT_VERSION = 0; + + using T = PicaVSConfigState; + return Common::HashCombine(STRUCT_VERSION, + + // layout + LAYOUT_HASH, + + // fields + FIELD_HASH(lighting_disable), FIELD_HASH(program_hash), + FIELD_HASH(swizzle_hash), FIELD_HASH(main_offset), + FIELD_HASH(num_outputs), FIELD_HASH(output_map), + FIELD_HASH(used_input_vertex_attributes), + FIELD_HASH(input_vertex_attributes), FIELD_HASH(gs_state), + + // nested layout + PicaGSConfigState::StructHash()); + } }; /** @@ -91,8 +146,21 @@ struct PicaVSConfigState { * shader. */ struct PicaVSConfig : Common::HashableStruct { - explicit PicaVSConfig(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup, - bool use_clip_planes_, bool use_geometry_shader_, bool accurate_mul_); + PicaVSConfig() = default; + explicit PicaVSConfig(const Pica::RegsInternal& regs, Pica::ShaderSetup& setup); +}; + +/** + * This struct contains complementary user/driver information to generate a vertex shader. + */ +struct ExtraVSConfig { + u8 use_clip_planes; + u8 use_geometry_shader; + u8 sanitize_mul; + u8 separable_shader; + + // Load operations to apply to the input vertex data + std::array load_flags; }; /** @@ -100,7 +168,12 @@ struct PicaVSConfig : Common::HashableStruct { * shader pipeline */ struct PicaFixedGSConfig : Common::HashableStruct { - explicit PicaFixedGSConfig(const Pica::RegsInternal& regs, bool use_clip_planes_); + explicit PicaFixedGSConfig(const Pica::RegsInternal& regs); +}; + +struct ExtraFixedGSConfig { + u8 use_clip_planes; + u8 separable_shader; }; } // namespace Pica::Shader::Generator @@ -120,3 +193,6 @@ struct hash { } }; } // namespace std + +#undef FIELD_HASH +#undef LAYOUT_HASH \ No newline at end of file diff --git a/src/video_core/shader/generator/spv_fs_shader_gen.cpp b/src/video_core/shader/generator/spv_fs_shader_gen.cpp index 0c8d32623..93ab3e367 100644 --- a/src/video_core/shader/generator/spv_fs_shader_gen.cpp +++ b/src/video_core/shader/generator/spv_fs_shader_gen.cpp @@ -21,6 +21,7 @@ FragmentModule::FragmentModule(const FSConfig& config_, const Profile& profile_) : Sirit::Module{SPIRV_VERSION_1_3}, config{config_}, profile{profile_}, use_fragment_shader_barycentric{profile.has_fragment_shader_barycentric && config.lighting.enable} { + config.ApplyProfile(profile_); DefineArithmeticTypes(); DefineUniformStructs(); DefineInterface(); @@ -454,7 +455,7 @@ void FragmentModule::WriteLighting() { const Id value{ get_lut_value(LightingRegs::SpotlightAttenuationSampler(light_config.num), light_config.num, lighting.lut_sp.type, lighting.lut_sp.abs_input)}; - spot_atten = OpFMul(f32_id, ConstF32(lighting.lut_sp.scale), value); + spot_atten = OpFMul(f32_id, ConstF32(lighting.lut_sp.GetScale()), value); } // If enabled, compute distance attenuation value @@ -486,7 +487,7 @@ void FragmentModule::WriteLighting() { const Id value{get_lut_value(LightingRegs::LightingSampler::Distribution0, light_config.num, lighting.lut_d0.type, lighting.lut_d0.abs_input)}; - d0_lut_value = OpFMul(f32_id, ConstF32(lighting.lut_d0.scale), value); + d0_lut_value = OpFMul(f32_id, ConstF32(lighting.lut_d0.GetScale()), value); } Id specular_0{OpVectorTimesScalar(vec_ids.Get(3), GetLightMember(0), d0_lut_value)}; @@ -503,7 +504,7 @@ void FragmentModule::WriteLighting() { light_config.num, lighting.lut_rr.type, lighting.lut_rr.abs_input)}; - refl_value_r = OpFMul(f32_id, ConstF32(lighting.lut_rr.scale), value); + refl_value_r = OpFMul(f32_id, ConstF32(lighting.lut_rr.GetScale()), value); } // If enabled, lookup ReflectGreen value, otherwise, ReflectRed value is used @@ -515,7 +516,7 @@ void FragmentModule::WriteLighting() { light_config.num, lighting.lut_rg.type, lighting.lut_rg.abs_input)}; - refl_value_g = OpFMul(f32_id, ConstF32(lighting.lut_rg.scale), value); + refl_value_g = OpFMul(f32_id, ConstF32(lighting.lut_rg.GetScale()), value); } // If enabled, lookup ReflectBlue value, otherwise, ReflectRed value is used @@ -526,7 +527,7 @@ void FragmentModule::WriteLighting() { const Id value{get_lut_value(LightingRegs::LightingSampler::ReflectBlue, light_config.num, lighting.lut_rb.type, lighting.lut_rb.abs_input)}; - refl_value_b = OpFMul(f32_id, ConstF32(lighting.lut_rb.scale), value); + refl_value_b = OpFMul(f32_id, ConstF32(lighting.lut_rb.GetScale()), value); } // Specular 1 component @@ -538,7 +539,7 @@ void FragmentModule::WriteLighting() { const Id value{get_lut_value(LightingRegs::LightingSampler::Distribution1, light_config.num, lighting.lut_d1.type, lighting.lut_d1.abs_input)}; - d1_lut_value = OpFMul(f32_id, ConstF32(lighting.lut_d1.scale), value); + d1_lut_value = OpFMul(f32_id, ConstF32(lighting.lut_d1.GetScale()), value); } const Id refl_value{ @@ -559,7 +560,7 @@ void FragmentModule::WriteLighting() { // Lookup fresnel LUT value Id value{get_lut_value(LightingRegs::LightingSampler::Fresnel, light_config.num, lighting.lut_fr.type, lighting.lut_fr.abs_input)}; - value = OpFMul(f32_id, ConstF32(lighting.lut_fr.scale), value); + value = OpFMul(f32_id, ConstF32(lighting.lut_fr.GetScale()), value); // Enabled for diffuse lighting alpha component if (lighting.enable_primary_alpha) { diff --git a/src/video_core/shader/generator/spv_fs_shader_gen.h b/src/video_core/shader/generator/spv_fs_shader_gen.h index cbbede01f..f19769682 100644 --- a/src/video_core/shader/generator/spv_fs_shader_gen.h +++ b/src/video_core/shader/generator/spv_fs_shader_gen.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -220,7 +220,7 @@ private: Id CompareShadow(Id pixel, Id z); private: - const FSConfig& config; + FSConfig config; const Profile& profile; bool use_fragment_shader_barycentric{}; From 6c6dd6878045199a7aa6abf8f1c7c7e5e782632a Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:32:01 +0100 Subject: [PATCH 03/94] Windows: Fix game shortcut character corruption (#1745) --- src/citra_qt/citra_qt.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index aab00987b..a66bd3786 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -1918,7 +1918,9 @@ bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); return false; } - hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE); + hres = persist_file->Save( + std::filesystem::path{shortcut_path / (Common::UTF8ToUTF16W(name) + L".lnk")}.c_str(), + TRUE); if (FAILED(hres)) { LOG_ERROR(Frontend, "Failed to save shortcut"); return false; From 5d4aef81fe7c0672fc7229fa6da39e94194c9ef4 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:21:21 +0100 Subject: [PATCH 04/94] common/cpu_detect: Remove FMA4 detection (#1746) --- src/citra_qt/citra_qt.cpp | 2 +- src/common/x64/cpu_detect.cpp | 12 ++++-------- src/common/x64/cpu_detect.h | 6 ++++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index a66bd3786..d8e207ec9 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -396,7 +396,7 @@ GMainWindow::GMainWindow(Core::System& system_) } else if (caps.avx2) { cpu_string += '2'; } - if (caps.fma || caps.fma4) { + if (caps.fma) { cpu_string += " | FMA"; } } diff --git a/src/common/x64/cpu_detect.cpp b/src/common/x64/cpu_detect.cpp index 0791a3f20..08290eb7f 100644 --- a/src/common/x64/cpu_detect.cpp +++ b/src/common/x64/cpu_detect.cpp @@ -1,7 +1,10 @@ -// Copyright 2013 Dolphin Emulator Project / 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. #include "common/arch.h" #if CITRA_ARCH(x86_64) @@ -131,13 +134,6 @@ static CPUCaps Detect() { std::memcpy(caps.cpu_string + 32, cpu_id, sizeof(cpu_id)); } - if (max_ex_fn >= 0x80000001) { - // Check for more features - __cpuid(cpu_id, 0x80000001); - if ((cpu_id[2] >> 16) & 1) - caps.fma4 = true; - } - return caps; } diff --git a/src/common/x64/cpu_detect.h b/src/common/x64/cpu_detect.h index 31ed1c584..7018ec0c1 100644 --- a/src/common/x64/cpu_detect.h +++ b/src/common/x64/cpu_detect.h @@ -1,7 +1,10 @@ -// Copyright 2013 Dolphin Emulator Project / 2015 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +// Copyright 2013 Dolphin Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. #pragma once #include "common/arch.h" @@ -25,7 +28,6 @@ struct CPUCaps { bool bmi1; bool bmi2; bool fma; - bool fma4; bool aes; }; From c43f24e489f8ff77a79e490ed84db370d0f88d75 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Tue, 17 Feb 2026 14:22:48 +0100 Subject: [PATCH 05/94] video_core: Fixes to vulkan disk cache (#1748) --- src/common/file_util.cpp | 20 +- src/common/file_util.h | 15 +- src/common/zstd_compression.cpp | 11 +- src/common/zstd_compression.h | 6 +- src/core/file_sys/ncch_container.cpp | 1 - .../renderer_vulkan/vk_pipeline_cache.cpp | 248 +++++++++--------- .../renderer_vulkan/vk_pipeline_cache.h | 32 ++- .../renderer_vulkan/vk_rasterizer.cpp | 18 +- .../renderer_vulkan/vk_shader_disk_cache.cpp | 192 ++++++++------ .../renderer_vulkan/vk_shader_disk_cache.h | 34 ++- 10 files changed, 310 insertions(+), 267 deletions(-) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index a9793c43c..79c0afaca 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -1254,20 +1254,19 @@ static std::size_t pread(int fd, void* buf, std::size_t count, uint64_t offset) #define pread ::pread #endif -std::size_t IOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) { +std::size_t IOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) { if (!IsOpen()) { m_good = false; return std::numeric_limits::max(); } - if (length == 0) { + if (byte_count == 0) { return 0; } DEBUG_ASSERT(data != nullptr); - return pread(fileno(m_file), data, data_size * length, offset); + return pread(fileno(m_file), data, byte_count, offset); } std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { @@ -1315,19 +1314,19 @@ struct CryptoIOFileImpl { std::size_t res = f.IOFile::ReadImpl(data, length, data_size); if (res != std::numeric_limits::max() && res != 0) { d.ProcessData(reinterpret_cast(data), - reinterpret_cast(data), length * data_size); + reinterpret_cast(data), res * data_size); e.Seek(f.IOFile::Tell()); } return res; } - std::size_t ReadAtImpl(CryptoIOFile& f, void* data, std::size_t length, std::size_t data_size, + std::size_t ReadAtImpl(CryptoIOFile& f, void* data, std::size_t byte_count, std::size_t offset) { - std::size_t res = f.IOFile::ReadAtImpl(data, length, data_size, offset); + std::size_t res = f.IOFile::ReadAtImpl(data, byte_count, offset); if (res != std::numeric_limits::max() && res != 0) { d.Seek(offset); d.ProcessData(reinterpret_cast(data), - reinterpret_cast(data), length * data_size); + reinterpret_cast(data), res); e.Seek(f.IOFile::Tell()); } return res; @@ -1378,9 +1377,8 @@ std::size_t CryptoIOFile::ReadImpl(void* data, std::size_t length, std::size_t d return impl->ReadImpl(*this, data, length, data_size); } -std::size_t CryptoIOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) { - return impl->ReadAtImpl(*this, data, length, data_size, offset); +std::size_t CryptoIOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) { + return impl->ReadAtImpl(*this, data, byte_count, offset); } std::size_t CryptoIOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { diff --git a/src/common/file_util.h b/src/common/file_util.h index 4c7d21349..98d232dcf 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -302,6 +302,7 @@ public: virtual bool Close(); + /// Returns the amount of T items read template std::size_t ReadArray(T* data, std::size_t length) { static_assert(std::is_trivially_copyable_v, @@ -314,16 +315,18 @@ public: return items_read; } + /// Returns the amount of bytes read template std::size_t ReadAtArray(T* data, std::size_t length, std::size_t offset) { static_assert(std::is_trivially_copyable_v, "Given array does not consist of trivially copyable objects"); - std::size_t items_read = ReadAtImpl(data, length, sizeof(T), offset); - if (items_read != length) + const size_t bytes = length * sizeof(T); + std::size_t size_read = ReadAtImpl(data, bytes, offset); + if (size_read != bytes) m_good = false; - return items_read; + return size_read; } template @@ -466,8 +469,7 @@ protected: virtual bool Open(); virtual std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size); - virtual std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset); + virtual std::size_t ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset); virtual std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size); virtual bool SeekImpl(s64 off, int origin); @@ -520,8 +522,7 @@ private: std::unique_ptr impl; std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) override; - std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) override; + std::size_t ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) override; std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size) override; bool SeekImpl(s64 off, int origin) override; diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index e177f24d9..3257fa2d8 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -357,8 +357,7 @@ std::size_t Z3DSWriteIOFile::ReadImpl(void* data, std::size_t length, std::size_ return 0; } -std::size_t Z3DSWriteIOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) { +std::size_t Z3DSWriteIOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) { // Stubbed UNIMPLEMENTED(); return 0; @@ -619,12 +618,12 @@ bool Z3DSReadIOFile::Open() { } std::size_t Z3DSReadIOFile::ReadImpl(void* data, std::size_t length, std::size_t data_size) { - return impl->Read(data, length * data_size); + size_t res = impl->Read(data, length * data_size); + return res == std::numeric_limits::max() ? res : (res / data_size); } -std::size_t Z3DSReadIOFile::ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) { - return impl->ReadAt(data, length * data_size, offset); +std::size_t Z3DSReadIOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) { + return impl->ReadAt(data, byte_count, offset); } std::size_t Z3DSReadIOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { diff --git a/src/common/zstd_compression.h b/src/common/zstd_compression.h index bd1d990dd..0d3372bd5 100644 --- a/src/common/zstd_compression.h +++ b/src/common/zstd_compression.h @@ -183,8 +183,7 @@ private: bool Open() override; std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) override; - std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) override; + std::size_t ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) override; std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size) override; bool SeekImpl(s64 off, int origin) override; @@ -250,8 +249,7 @@ private: bool Open() override; std::size_t ReadImpl(void* data, std::size_t length, std::size_t data_size) override; - std::size_t ReadAtImpl(void* data, std::size_t length, std::size_t data_size, - std::size_t offset) override; + std::size_t ReadAtImpl(void* data, std::size_t byte_count, std::size_t offset) override; std::size_t WriteImpl(const void* data, std::size_t length, std::size_t data_size) override; bool SeekImpl(s64 off, int origin) override; diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 29232d4be..1438a766a 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -168,7 +168,6 @@ Loader::ResultStatus NCCHContainer::LoadHeader() { ASSERT(Loader::MakeMagic('N', 'C', 'S', 'D') == ncsd_header.magic); ASSERT(partition < 8); ncch_offset = ncsd_header.partitions[partition].offset * kBlockSize; - LOG_ERROR(Service_FS, "{}", ncch_offset); file->Seek(ncch_offset, SEEK_SET); file->ReadBytes(&ncch_header, sizeof(NCCH_Header)); } diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 8c8171495..97b7cc8b0 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -144,24 +144,59 @@ void PipelineCache::BuildLayout() { } PipelineCache::~PipelineCache() { - SaveDiskCache(); + workers.WaitForRequests(); + SaveDriverPipelineDiskCache(); } -void PipelineCache::LoadPipelineDiskCache(const std::atomic_bool& stop_loading, - const VideoCore::DiskResourceLoadCallback& callback) { +void PipelineCache::LoadCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + LoadDriverPipelineDiskCache(stop_loading, callback); + LoadDiskCache(stop_loading, callback); +} + +void PipelineCache::SwitchCache(u64 title_id, const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + if (GetProgramID() == title_id) { + LOG_DEBUG(Render_Vulkan, + "Skipping pipeline cache switch - already using cache for title_id={:016X}", + title_id); + return; + } + + // Make sure we have a valid pipeline cache before switching + if (!driver_pipeline_cache) { + vk::PipelineCacheCreateInfo cache_info{}; + try { + driver_pipeline_cache = instance.GetDevice().createPipelineCacheUnique(cache_info); + } catch (const vk::SystemError& err) { + LOG_ERROR(Render_Vulkan, "Failed to create pipeline cache: {}", err.what()); + return; + } + } + + LOG_INFO(Render_Vulkan, "Switching pipeline cache to title_id={:016X}", title_id); + + // Save current driver cache, update program ID and load the new driver cache + SaveDriverPipelineDiskCache(); + SetProgramID(title_id); + LoadDriverPipelineDiskCache(stop_loading, nullptr); + + // Switch the disk shader cache after driver cache is switched + SwitchDiskCache(title_id, stop_loading, callback); +} + +void PipelineCache::LoadDriverPipelineDiskCache( + const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback) { vk::PipelineCacheCreateInfo cache_info{}; - if (callback) { - callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); - } if (callback) { callback(VideoCore::LoadCallbackStage::Build, 0, 1, "Driver Pipeline Cache"); } - auto load_cache = [this, &cache_info](bool allow_fallback) { + auto load_cache = [this, &cache_info, &callback](bool allow_fallback) { const vk::Device device = instance.GetDevice(); try { - pipeline_cache = device.createPipelineCacheUnique(cache_info); + driver_pipeline_cache = device.createPipelineCacheUnique(cache_info); } catch (const vk::SystemError& err) { LOG_ERROR(Render_Vulkan, "Failed to create pipeline cache: {}", err.what()); if (allow_fallback) { @@ -169,13 +204,16 @@ void PipelineCache::LoadPipelineDiskCache(const std::atomic_bool& stop_loading, cache_info.initialDataSize = 0; cache_info.pInitialData = nullptr; try { - pipeline_cache = device.createPipelineCacheUnique(cache_info); + driver_pipeline_cache = device.createPipelineCacheUnique(cache_info); } catch (const vk::SystemError& err) { LOG_ERROR(Render_Vulkan, "Failed to create fallback pipeline cache: {}", err.what()); } } } + if (callback) { + callback(VideoCore::LoadCallbackStage::Build, 1, 1, "Driver Pipeline Cache"); + } }; // Try to load existing pipeline cache if disk cache is enabled and directories exist @@ -226,19 +264,9 @@ void PipelineCache::LoadPipelineDiskCache(const std::atomic_bool& stop_loading, load_cache(true); } -void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading, - const VideoCore::DiskResourceLoadCallback& callback) { - - disk_caches.clear(); - curr_disk_cache = - disk_caches.emplace_back(std::make_shared(*this, GetProgramID())); - - curr_disk_cache->Init(stop_loading, callback); -} - -void PipelineCache::SaveDiskCache() { +void PipelineCache::SaveDriverPipelineDiskCache() { // Save Vulkan pipeline cache - if (!Settings::values.use_disk_shader_cache || !pipeline_cache) { + if (!Settings::values.use_disk_shader_cache || !driver_pipeline_cache) { return; } @@ -258,13 +286,81 @@ void PipelineCache::SaveDiskCache() { } const vk::Device device = instance.GetDevice(); - const auto cache_data = device.getPipelineCacheData(*pipeline_cache); + const auto cache_data = device.getPipelineCacheData(*driver_pipeline_cache); if (cache_file.WriteBytes(cache_data.data(), cache_data.size()) != cache_data.size()) { LOG_ERROR(Render_Vulkan, "Error during pipeline cache write"); return; } } +void PipelineCache::LoadDiskCache(const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + + disk_caches.clear(); + curr_disk_cache = + disk_caches.emplace_back(std::make_shared(*this, GetProgramID())); + + curr_disk_cache->Init(stop_loading, callback); +} + +void PipelineCache::SwitchDiskCache(u64 title_id, const std::atomic_bool& stop_loading, + const VideoCore::DiskResourceLoadCallback& callback) { + // NOTE: curr_disk_cache can be null if emulation restarted without calling + // LoadDefaultDiskResources + + // Check if the current cache is for the specified TID. + if (curr_disk_cache && curr_disk_cache->GetProgramID() == title_id) { + return; + } + + // Search for an existing manager + size_t new_pos = 0; + for (new_pos = 0; new_pos < disk_caches.size(); new_pos++) { + if (disk_caches[new_pos]->GetProgramID() == title_id) { + break; + } + } + // Manager does not exist, create it and append to the end + if (new_pos >= disk_caches.size()) { + new_pos = disk_caches.size(); + auto& new_manager = + disk_caches.emplace_back(std::make_shared(*this, title_id)); + + new_manager->Init(stop_loading, callback); + } + + auto is_applet = [](u64 tid) { + constexpr u32 APPLET_TID_HIGH = 0x00040030; + return static_cast(tid >> 32) == APPLET_TID_HIGH; + }; + + bool prev_applet = curr_disk_cache ? is_applet(curr_disk_cache->GetProgramID()) : false; + bool new_applet = is_applet(disk_caches[new_pos]->GetProgramID()); + curr_disk_cache = disk_caches[new_pos]; + + if (prev_applet) { + // If we came from an applet, clean up all other applets + for (auto it = disk_caches.begin(); it != disk_caches.end();) { + if (it == disk_caches.begin() || *it == curr_disk_cache || + !is_applet((*it)->GetProgramID())) { + it++; + continue; + } + it = disk_caches.erase(it); + } + } + if (!new_applet) { + // If we are going into a non-applet, clean up everything + for (auto it = disk_caches.begin(); it != disk_caches.end();) { + if (it == disk_caches.begin() || *it == curr_disk_cache) { + it++; + continue; + } + it = disk_caches.erase(it); + } + } +} + bool PipelineCache::BindPipeline(PipelineInfo& info, bool wait_built) { MICROPROFILE_SCOPE(Vulkan_Bind); @@ -551,112 +647,4 @@ std::string PipelineCache::GetTransferableDir() const { return GetVulkanDir() + DIR_SEP + "transferable"; } -void PipelineCache::SwitchPipelineCache(u64 title_id, const std::atomic_bool& stop_loading, - const VideoCore::DiskResourceLoadCallback& callback) { - if (!Settings::values.use_disk_shader_cache || GetProgramID() == title_id) { - LOG_DEBUG(Render_Vulkan, - "Skipping pipeline cache switch - already using cache for title_id={:016X}", - title_id); - return; - } - - if (callback) { - callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); - } - if (callback) { - callback(VideoCore::LoadCallbackStage::Build, 0, 1, "Driver Pipeline Cache"); - } - - // Make sure we have a valid pipeline cache before switching - if (!pipeline_cache) { - vk::PipelineCacheCreateInfo cache_info{}; - try { - pipeline_cache = instance.GetDevice().createPipelineCacheUnique(cache_info); - } catch (const vk::SystemError& err) { - LOG_ERROR(Render_Vulkan, "Failed to create pipeline cache: {}", err.what()); - return; - } - } - - LOG_INFO(Render_Vulkan, "Switching pipeline cache to title_id={:016X}", title_id); - - // Save current cache before switching - SaveDiskCache(); - - // Update program ID and load the new pipeline cache - SetProgramID(title_id); - LoadPipelineDiskCache(stop_loading, nullptr); - SwitchDiskCache(title_id, stop_loading, callback); - - if (callback) { - callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); - } -} - -void PipelineCache::SwitchDiskCache(u64 title_id, const std::atomic_bool& stop_loading, - const VideoCore::DiskResourceLoadCallback& callback) { - // NOTE: curr_disk_cache can be null if emulation restarted without calling - // LoadDefaultDiskResources - - // Check if the current cache is for the specified TID. - if (curr_disk_cache && curr_disk_cache->GetProgramID() == title_id) { - return; - } - - // Search for an existing manager - size_t new_pos = 0; - for (new_pos = 0; new_pos < disk_caches.size(); new_pos++) { - if (disk_caches[new_pos]->GetProgramID() == title_id) { - break; - } - } - // Manager does not exist, create it and append to the end - if (new_pos >= disk_caches.size()) { - new_pos = disk_caches.size(); - auto& new_manager = - disk_caches.emplace_back(std::make_shared(*this, title_id)); - - if (callback) { - callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); - } - - new_manager->Init(stop_loading, callback); - - if (callback) { - callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); - } - } - - auto is_applet = [](u64 tid) { - constexpr u32 APPLET_TID_HIGH = 0x00040030; - return static_cast(tid >> 32) == APPLET_TID_HIGH; - }; - - bool prev_applet = curr_disk_cache ? is_applet(curr_disk_cache->GetProgramID()) : false; - bool new_applet = is_applet(disk_caches[new_pos]->GetProgramID()); - curr_disk_cache = disk_caches[new_pos]; - - if (prev_applet) { - // If we came from an applet, clean up all other applets - for (auto it = disk_caches.begin(); it != disk_caches.end();) { - if (it == disk_caches.begin() || *it == curr_disk_cache || - !is_applet((*it)->GetProgramID())) { - it++; - continue; - } - it = disk_caches.erase(it); - } - } - if (!new_applet) { - // If we are going into a non-applet, clean up everything - for (auto it = disk_caches.begin(); it != disk_caches.end();) { - if (it == disk_caches.begin() || *it == curr_disk_cache) { - it++; - continue; - } - it = disk_caches.erase(it); - } - } -} - } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index 2bb9c750d..111960073 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -58,15 +58,13 @@ public: offsets[binding] = offset; } - /// Loads the pipeline cache stored to disk - void LoadPipelineDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, - const VideoCore::DiskResourceLoadCallback& callback = {}); + /// Loads the driver pipeline cache and the disk shader cache + void LoadCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, + const VideoCore::DiskResourceLoadCallback& callback = {}); - void LoadDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, - const VideoCore::DiskResourceLoadCallback& callback = {}); - - /// Stores the generated pipeline cache to disk - void SaveDiskCache(); + /// Switches the driver pipeline cache and the shader disk cache to the specified title + void SwitchCache(u64 title_id, const std::atomic_bool& stop_loading = std::atomic_bool{false}, + const VideoCore::DiskResourceLoadCallback& callback = {}); /// Binds a pipeline using the provided information bool BindPipeline(PipelineInfo& info, bool wait_built = false); @@ -90,11 +88,6 @@ public: /// Binds a fragment shader generated from PICA state void UseFragmentShader(const Pica::RegsInternal& regs, const Pica::Shader::UserConfig& user); - /// Switches the shader disk cache to the specified title - void SwitchPipelineCache(u64 title_id, - const std::atomic_bool& stop_loading = std::atomic_bool{false}, - const VideoCore::DiskResourceLoadCallback& callback = {}); - /// Gets the current program ID u64 GetProgramID() const { return current_program_id; @@ -111,6 +104,17 @@ public: private: friend ShaderDiskCache; + /// Loads the driver pipeline cache + void LoadDriverPipelineDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, + const VideoCore::DiskResourceLoadCallback& callback = {}); + + /// Stores the generated pipeline cache + void SaveDriverPipelineDiskCache(); + + /// Loads the shader disk cache + void LoadDiskCache(const std::atomic_bool& stop_loading = std::atomic_bool{false}, + const VideoCore::DiskResourceLoadCallback& callback = {}); + /// Switches the disk cache at runtime to use a different title ID void SwitchDiskCache(u64 title_id, const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback); @@ -140,7 +144,7 @@ private: DescriptorUpdateQueue& update_queue; Pica::Shader::Profile profile{}; - vk::UniquePipelineCache pipeline_cache; + vk::UniquePipelineCache driver_pipeline_cache; vk::UniquePipelineLayout pipeline_layout; std::size_t num_worker_threads; Common::ThreadWorker workers; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index b38818f88..7a27032e9 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -152,10 +152,13 @@ void RasterizerVulkan::LoadDefaultDiskResources( program_id = 0; } + if (callback) { + callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); + } + pipeline_cache.SetProgramID(program_id); pipeline_cache.SetAccurateMul(accurate_mul); - pipeline_cache.LoadPipelineDiskCache(stop_loading, callback); - pipeline_cache.LoadDiskCache(stop_loading, callback); + pipeline_cache.LoadCache(stop_loading, callback); if (callback) { callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); @@ -985,8 +988,17 @@ void RasterizerVulkan::UploadUniforms(bool accelerate_draw) { void RasterizerVulkan::SwitchDiskResources(u64 title_id) { std::atomic_bool stop_loading = false; + + if (switch_disk_resources_callback) { + switch_disk_resources_callback(VideoCore::LoadCallbackStage::Prepare, 0, 0, ""); + } + pipeline_cache.SetAccurateMul(accurate_mul); - pipeline_cache.SwitchPipelineCache(title_id, stop_loading, switch_disk_resources_callback); + pipeline_cache.SwitchCache(title_id, stop_loading, switch_disk_resources_callback); + + if (switch_disk_resources_callback) { + switch_disk_resources_callback(VideoCore::LoadCallbackStage::Complete, 0, 0, ""); + } } } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp index b3f1f0613..15e45ce81 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp @@ -59,6 +59,9 @@ static VideoCore::DiskResourceLoadCallback MakeThrottledCallback( void ShaderDiskCache::Init(const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback) { + if (!Settings::values.use_disk_shader_cache) + return; + auto new_callback = MakeThrottledCallback(callback); if (!stop_loading && !InitVSCache(stop_loading, new_callback)) { @@ -248,7 +251,7 @@ GraphicsPipeline* ShaderDiskCache::GetPipeline(const PipelineInfo& info) { parent.UseTrivialGeometryShader(); } it.value() = std::make_unique( - parent.instance, parent.renderpass_cache, info, *parent.pipeline_cache, + parent.instance, parent.renderpass_cache, info, *parent.driver_pipeline_cache, *parent.pipeline_layout, parent.current_shaders, &parent.workers); } @@ -339,86 +342,129 @@ ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) } size_t ShaderDiskCache::CacheFile::GetTotalEntries() { - if (biggest_entry_id != SIZE_MAX) { - return biggest_entry_id + 1; + if (!file.IsGood()) { + next_entry_id = SIZE_MAX; + return next_entry_id; + } + + if (next_entry_id != SIZE_MAX) { + return next_entry_id; } const size_t file_size = file.GetSize(); + if (file_size == 0) { + next_entry_id = 0; + return next_entry_id; + } + CacheEntry::CacheEntryFooter footer{}; if (file.ReadAtArray(&footer, 1, file_size - sizeof(footer)) == sizeof(footer) && footer.version == CacheEntry::CacheEntryFooter::ENTRY_VERSION) { - biggest_entry_id = footer.entry_id; + next_entry_id = footer.entry_id + 1; + } else { + return SIZE_MAX; } - return biggest_entry_id + 1; + return next_entry_id; } -bool ShaderDiskCache::CacheFile::Append(CacheEntryType type, u64 id, std::span data, +void ShaderDiskCache::CacheFile::Append(CacheEntryType type, u64 id, std::span data, bool compress) { - std::scoped_lock lock(mutex); - - std::span data_final; - std::vector data_compress; - - CacheEntry::CacheEntryHeader header{}; - CacheEntry::CacheEntryFooter footer{}; - - constexpr u32 headers_size = - sizeof(CacheEntry::CacheEntryHeader) + sizeof(CacheEntry::CacheEntryFooter); - - if (compress) { - data_compress = Common::Compression::CompressDataZSTDDefault(data); - data_final = data_compress; - header.zstd_compressed.Assign(true); - } else { - data_final = data; + if (curr_mode != CacheOpMode::APPEND) { + return; } - header.entry_version = CacheEntry::CacheEntryHeader::ENTRY_VERSION; - footer.version.Assign(CacheEntry::CacheEntryFooter::ENTRY_VERSION); - header.entry_size = footer.entry_size = data_final.size() + headers_size; - footer.entry_id.Assign(biggest_entry_id++); - header.type = type; - header.id = id; + std::vector copy_data(data.begin(), data.end()); - std::vector out_data(data_final.size() + headers_size); - memcpy(out_data.data(), &header, sizeof(header)); - memcpy(out_data.data() + sizeof(header), data_final.data(), data_final.size()); - memcpy(out_data.data() + sizeof(header) + data_final.size(), &footer, sizeof(footer)); + append_worker.QueueWork([this, type, id, compress, data = std::move(copy_data)]() { + if (next_entry_id == SIZE_MAX || !file.IsGood()) { + return; + } - return file.WriteBytes(out_data.data(), out_data.size()) == out_data.size(); + std::span data_final; + std::vector data_compress; + + CacheEntry::CacheEntryHeader header{}; + CacheEntry::CacheEntryFooter footer{}; + + constexpr u32 headers_size = + sizeof(CacheEntry::CacheEntryHeader) + sizeof(CacheEntry::CacheEntryFooter); + + if (compress) { + data_compress = Common::Compression::CompressDataZSTDDefault(data); + data_final = data_compress; + header.zstd_compressed.Assign(true); + } else { + data_final = data; + } + header.entry_version = CacheEntry::CacheEntryHeader::ENTRY_VERSION; + footer.version.Assign(CacheEntry::CacheEntryFooter::ENTRY_VERSION); + header.entry_size = footer.entry_size = data_final.size() + headers_size; + footer.entry_id.Assign(next_entry_id++); + + header.type = type; + header.id = id; + + std::vector out_data(data_final.size() + headers_size); + memcpy(out_data.data(), &header, sizeof(header)); + memcpy(out_data.data() + sizeof(header), data_final.data(), data_final.size()); + memcpy(out_data.data() + sizeof(header) + data_final.size(), &footer, sizeof(footer)); + + file.WriteBytes(out_data.data(), out_data.size()); + if (file.IsGood()) { + file.Flush(); + } + }); } bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { + if (curr_mode == mode) { + return true; + } + if (curr_mode == CacheOpMode::APPEND) { + append_worker.WaitForRequests(); + } + switch (mode) { case CacheOpMode::READ: { + next_entry_id = SIZE_MAX; // Force reading entries agains file = FileUtil::IOFile(filepath, "rb"); - bool is_open = file.IsOpen(); + bool is_open = file.IsGood(); if (is_open) { GetTotalEntries(); } + curr_mode = mode; return is_open; } case CacheOpMode::APPEND: { - GetTotalEntries(); + if (!SwitchMode(CacheOpMode::READ)) { + curr_mode = mode; + return false; + } file.Close(); - if (biggest_entry_id == SIZE_MAX) { + curr_mode = mode; + if (next_entry_id == SIZE_MAX) { // Cannot append if getting total items fails return false; } file = FileUtil::IOFile(filepath, "ab"); - return file.IsOpen(); + return file.IsGood(); } case CacheOpMode::DELETE: { - biggest_entry_id = 0; + next_entry_id = SIZE_MAX; file.Close(); - FileUtil::Delete(filepath); - return true; + curr_mode = mode; + return FileUtil::Delete(filepath); } case CacheOpMode::RECREATE: { - SwitchMode(CacheOpMode::DELETE); + if (!SwitchMode(CacheOpMode::DELETE)) { + return false; + } + if (!FileUtil::CreateEmptyFile(filepath)) { + return false; + } return SwitchMode(CacheOpMode::APPEND); } default: @@ -827,8 +873,7 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, } // Switch to append mode to receive new entries. - vs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); - return true; + return vs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); } bool ShaderDiskCache::InitFSCache(const std::atomic_bool& stop_loading, @@ -1045,8 +1090,7 @@ bool ShaderDiskCache::InitFSCache(const std::atomic_bool& stop_loading, } // Switch to append mode to receive new entries. - fs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); - return true; + return fs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); } bool ShaderDiskCache::InitGSCache(const std::atomic_bool& stop_loading, @@ -1272,8 +1316,7 @@ bool ShaderDiskCache::InitGSCache(const std::atomic_bool& stop_loading, } // Switch to append mode to receive new entries. - gs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); - return true; + return gs_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); } bool ShaderDiskCache::InitPLCache(const std::atomic_bool& stop_loading, @@ -1400,7 +1443,7 @@ bool ShaderDiskCache::InitPLCache(const std::atomic_bool& stop_loading, auto [it_pl, _] = graphics_pipelines.try_emplace(pl_hash_opt); it_pl.value() = std::make_unique( - parent.instance, parent.renderpass_cache, info, *parent.pipeline_cache, + parent.instance, parent.renderpass_cache, info, *parent.driver_pipeline_cache, *parent.pipeline_layout, shaders, &parent.workers); it_pl.value()->TryBuild(false); @@ -1413,11 +1456,10 @@ bool ShaderDiskCache::InitPLCache(const std::atomic_bool& stop_loading, } // Switch to append mode to receive new entries. - pl_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); - return true; + return pl_cache.SwitchMode(CacheFile::CacheOpMode::APPEND); } -bool ShaderDiskCache::AppendVSConfigProgram(CacheFile& file, +void ShaderDiskCache::AppendVSConfigProgram(CacheFile& file, const Pica::Shader::Generator::PicaVSConfig& config, const Pica::ShaderSetup& setup, u64 config_id, u64 spirv_id) { @@ -1430,7 +1472,6 @@ bool ShaderDiskCache::AppendVSConfigProgram(CacheFile& file, Common::HashCombine(config.state.program_hash, config.state.swizzle_hash); bool new_entry = known_vertex_programs.emplace(entry.program_entry_id).second; - bool prog_res = true; if (new_entry) { std::unique_ptr prog_entry = std::make_unique(); prog_entry->version = VSProgramEntry::EXPECTED_VERSION; @@ -1439,49 +1480,46 @@ bool ShaderDiskCache::AppendVSConfigProgram(CacheFile& file, prog_entry->swizzle_len = setup.GetBiggestSwizzleSize(); prog_entry->swizzle_code = setup.GetSwizzleData(); - prog_res = AppendVSProgram(file, *prog_entry, entry.program_entry_id); + AppendVSProgram(file, *prog_entry, entry.program_entry_id); } - return AppendVSConfig(file, entry, config_id) && prog_res; + AppendVSConfig(file, entry, config_id); } -bool ShaderDiskCache::AppendVSProgram(CacheFile& file, const VSProgramEntry& entry, +void ShaderDiskCache::AppendVSProgram(CacheFile& file, const VSProgramEntry& entry, u64 program_id) { - return file.Append(CacheEntryType::VS_PROGRAM, program_id, entry, true); + file.Append(CacheEntryType::VS_PROGRAM, program_id, entry, true); } -bool ShaderDiskCache::AppendVSConfig(CacheFile& file, const VSConfigEntry& entry, u64 config_id) { - return file.Append(CacheEntryType::VS_CONFIG, config_id, entry, true); +void ShaderDiskCache::AppendVSConfig(CacheFile& file, const VSConfigEntry& entry, u64 config_id) { + file.Append(CacheEntryType::VS_CONFIG, config_id, entry, true); } -bool ShaderDiskCache::AppendVSSPIRV(CacheFile& file, std::span program, u64 program_id) { - return file.Append(CacheEntryType::VS_SPIRV, program_id, - {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, - true); +void ShaderDiskCache::AppendVSSPIRV(CacheFile& file, std::span program, u64 program_id) { + file.Append(CacheEntryType::VS_SPIRV, program_id, + {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, true); } -bool ShaderDiskCache::AppendFSConfig(CacheFile& file, const FSConfigEntry& entry, u64 config_id) { - return file.Append(CacheEntryType::FS_CONFIG, config_id, entry, true); +void ShaderDiskCache::AppendFSConfig(CacheFile& file, const FSConfigEntry& entry, u64 config_id) { + file.Append(CacheEntryType::FS_CONFIG, config_id, entry, true); } -bool ShaderDiskCache::AppendFSSPIRV(CacheFile& file, std::span program, u64 program_id) { - return file.Append(CacheEntryType::FS_SPIRV, program_id, - {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, - true); +void ShaderDiskCache::AppendFSSPIRV(CacheFile& file, std::span program, u64 program_id) { + file.Append(CacheEntryType::FS_SPIRV, program_id, + {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, true); } -bool ShaderDiskCache::AppendGSConfig(CacheFile& file, const GSConfigEntry& entry, u64 config_id) { - return file.Append(CacheEntryType::GS_CONFIG, config_id, entry, true); +void ShaderDiskCache::AppendGSConfig(CacheFile& file, const GSConfigEntry& entry, u64 config_id) { + file.Append(CacheEntryType::GS_CONFIG, config_id, entry, true); } -bool ShaderDiskCache::AppendGSSPIRV(CacheFile& file, std::span program, u64 program_id) { - return file.Append(CacheEntryType::GS_SPIRV, program_id, - {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, - true); +void ShaderDiskCache::AppendGSSPIRV(CacheFile& file, std::span program, u64 program_id) { + file.Append(CacheEntryType::GS_SPIRV, program_id, + {reinterpret_cast(program.data()), program.size() * sizeof(u32)}, true); } -bool ShaderDiskCache::AppendPLConfig(CacheFile& file, const PLConfigEntry& entry, u64 config_id) { - return file.Append(CacheEntryType::PL_CONFIG, config_id, entry, true); +void ShaderDiskCache::AppendPLConfig(CacheFile& file, const PLConfigEntry& entry, u64 config_id) { + file.Append(CacheEntryType::PL_CONFIG, config_id, entry, true); } } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h index 451b9e078..d6b487f18 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -11,6 +11,7 @@ #include "common/common_types.h" #include "common/file_util.h" +#include "common/thread_worker.h" #include "video_core/pica/shader_setup.h" #include "video_core/rasterizer_interface.h" #include "video_core/renderer_vulkan/vk_graphics_pipeline.h" @@ -241,6 +242,7 @@ private: class CacheFile { public: enum class CacheOpMode { + NONE = 0, READ, APPEND, DELETE, @@ -249,6 +251,9 @@ private: CacheFile() = default; CacheFile(const std::string& _filepath) : filepath(_filepath) {} + ~CacheFile() { + append_worker.WaitForRequests(); + } void SetFilePath(const std::string& path) { filepath = path; @@ -268,24 +273,25 @@ private: size_t GetTotalEntries(); template - bool Append(CacheEntryType type, u64 id, const T& object, bool compress) { + void Append(CacheEntryType type, u64 id, const T& object, bool compress) { static_assert(std::is_trivially_copyable_v); auto bytes = std::as_bytes(std::span{&object, 1}); auto u8_span = std::span(reinterpret_cast(bytes.data()), bytes.size()); - return Append(type, id, u8_span, compress); + Append(type, id, u8_span, compress); } - bool Append(CacheEntryType type, u64 id, std::span data, bool compress); + void Append(CacheEntryType type, u64 id, std::span data, bool compress); bool SwitchMode(CacheOpMode mode); private: + CacheOpMode curr_mode = CacheOpMode::NONE; std::string filepath; - std::mutex mutex; FileUtil::IOFile file{}; - size_t biggest_entry_id = SIZE_MAX; + std::atomic next_entry_id = SIZE_MAX; + Common::ThreadWorker append_worker{1, "Disk Shader Cache Append Worker"}; }; std::string GetVSFile(u64 title_id, bool is_temp) const; @@ -307,19 +313,19 @@ private: bool InitPLCache(const std::atomic_bool& stop_loading, const VideoCore::DiskResourceLoadCallback& callback); - bool AppendVSConfigProgram(CacheFile& file, const Pica::Shader::Generator::PicaVSConfig& config, + void AppendVSConfigProgram(CacheFile& file, const Pica::Shader::Generator::PicaVSConfig& config, const Pica::ShaderSetup& setup, u64 config_id, u64 program_id); - bool AppendVSProgram(CacheFile& file, const VSProgramEntry& entry, u64 program_id); - bool AppendVSConfig(CacheFile& file, const VSConfigEntry& entry, u64 config_id); - bool AppendVSSPIRV(CacheFile& file, std::span program, u64 program_id); + void AppendVSProgram(CacheFile& file, const VSProgramEntry& entry, u64 program_id); + void AppendVSConfig(CacheFile& file, const VSConfigEntry& entry, u64 config_id); + void AppendVSSPIRV(CacheFile& file, std::span program, u64 program_id); - bool AppendFSConfig(CacheFile& file, const FSConfigEntry& entry, u64 config_id); - bool AppendFSSPIRV(CacheFile& file, std::span program, u64 program_id); + void AppendFSConfig(CacheFile& file, const FSConfigEntry& entry, u64 config_id); + void AppendFSSPIRV(CacheFile& file, std::span program, u64 program_id); - bool AppendGSConfig(CacheFile& file, const GSConfigEntry& entry, u64 config_id); - bool AppendGSSPIRV(CacheFile& file, std::span program, u64 program_id); + void AppendGSConfig(CacheFile& file, const GSConfigEntry& entry, u64 config_id); + void AppendGSSPIRV(CacheFile& file, std::span program, u64 program_id); - bool AppendPLConfig(CacheFile& file, const PLConfigEntry& entry, u64 config_id); + void AppendPLConfig(CacheFile& file, const PLConfigEntry& entry, u64 config_id); CacheFile vs_cache; CacheFile fs_cache; From 5c6b23c64db10abcb39d057c0dc51d64dfe766d1 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Tue, 17 Feb 2026 13:32:01 +0000 Subject: [PATCH 06/94] tools: Added enter-docker-dev-container.sh script --- tools/enter-docker-dev-container.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100755 tools/enter-docker-dev-container.sh diff --git a/tools/enter-docker-dev-container.sh b/tools/enter-docker-dev-container.sh new file mode 100755 index 000000000..f03105be6 --- /dev/null +++ b/tools/enter-docker-dev-container.sh @@ -0,0 +1,5 @@ +#!/bin/bash -ex + +# This script assumes that Docker is installed + +docker run -it --rm -v $(pwd):/mnt opensauce04/azahar-build-environment From 354f5d698fa9ab0e0037d667459630e05f1aeac6 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Tue, 17 Feb 2026 13:36:36 +0000 Subject: [PATCH 07/94] Updated translations via Transifex --- dist/languages/ca_ES_valencia.ts | 501 ++++++++--------- dist/languages/da_DK.ts | 495 +++++++++-------- dist/languages/de.ts | 499 ++++++++--------- dist/languages/el.ts | 497 +++++++++-------- dist/languages/es_ES.ts | 513 +++++++++--------- dist/languages/fi.ts | 493 +++++++++-------- dist/languages/fr.ts | 503 ++++++++--------- dist/languages/hu_HU.ts | 495 +++++++++-------- dist/languages/id.ts | 495 +++++++++-------- dist/languages/it.ts | 503 ++++++++--------- dist/languages/ja_JP.ts | 499 ++++++++--------- dist/languages/ko_KR.ts | 497 +++++++++-------- dist/languages/lt_LT.ts | 493 +++++++++-------- dist/languages/nb.ts | 497 +++++++++-------- dist/languages/nl.ts | 497 +++++++++-------- dist/languages/pl_PL.ts | 503 ++++++++--------- dist/languages/pt_BR.ts | 503 ++++++++--------- dist/languages/ro_RO.ts | 497 +++++++++-------- dist/languages/ru_RU.ts | 499 ++++++++--------- dist/languages/sv.ts | 503 ++++++++--------- dist/languages/tr_TR.ts | 499 ++++++++--------- dist/languages/vi_VN.ts | 495 +++++++++-------- dist/languages/zh_CN.ts | 501 ++++++++--------- dist/languages/zh_TW.ts | 495 +++++++++-------- .../res/values-b+ca+ES+valencia/strings.xml | 2 - .../src/main/res/values-b+da+DK/strings.xml | 6 +- .../src/main/res/values-b+es+ES/strings.xml | 6 +- .../src/main/res/values-b+pl+PL/strings.xml | 2 +- .../src/main/res/values-b+pt+BR/strings.xml | 2 - .../src/main/res/values-b+ru+RU/strings.xml | 2 - .../src/main/res/values-b+tr+TR/strings.xml | 2 - .../src/main/res/values-b+zh+CN/strings.xml | 2 - .../app/src/main/res/values-de/strings.xml | 2 - .../app/src/main/res/values-fr/strings.xml | 2 - .../app/src/main/res/values-it/strings.xml | 6 +- .../app/src/main/res/values-sv/strings.xml | 2 - 36 files changed, 6178 insertions(+), 5830 deletions(-) diff --git a/dist/languages/ca_ES_valencia.ts b/dist/languages/ca_ES_valencia.ts index b3614b36b..9e0466c40 100644 --- a/dist/languages/ca_ES_valencia.ts +++ b/dist/languages/ca_ES_valencia.ts @@ -326,8 +326,8 @@ Aixó banejarà el seu nom d'usuari de fòrum i la seua adreça IP. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Este efecte de post-processat ajusta la velocitat de l'àudio per a igualar-la a la de l'emulador i ajuda a previndre aturades d'àudio, però augmenta la latència d'este. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -336,8 +336,8 @@ Aixó banejarà el seu nom d'usuari de fòrum i la seua adreça IP. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Ajusta la velocitat de reproducció d'àudio per a compensar les caigudes en la velocitat d'emulació. Això significa que l'àudio es reproduirà a velocitat completa fins i tot quan la velocitat de quadres del joc siga baixa. Pot causar problemes de desincronització d'àudio. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -476,8 +476,8 @@ Aixó banejarà el seu nom d'usuari de fòrum i la seua adreça IP. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Selecciona el lloc d'on prové la imatge de la càmera emulada. Pot ser una imatge o una càmera real. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1492,8 +1492,8 @@ Desitja ignorar l'error i continuar? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - La Sincronització Vertical impedix el tearing de la imatge, però algunes targetes gràfiques tenen pitjor rendiment quan este està activat. Mantingues-ho activat si no notes cap diferència en el rendiment. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1501,22 +1501,32 @@ Desitja ignorar l'error i continuar? Activar Sincronització Vertical - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global Usar global - + Use per-application Usar configuració de l'aplicació - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Demora el fil emulat de renderitzat del joc una determinada quantitat de mil·lisegons cada vegada que envie comandos de renderitzat a la GPU.</p><p>Ajusta esta característica en els (pocs) jocs amb FPS dinàmics per a arreglar problemes de rendiment.</p></body></html> @@ -2519,8 +2529,8 @@ Desitja ignorar l'error i continuar? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Comprimix el contingut de fitxers CIA quan són instal·lats a la SD emulada. Només afecta contingut CIA instal·lat amb esta opció activada. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + @@ -4118,19 +4128,19 @@ Per favor, comprove la instal·lació de FFmpeg usada per a la compilació. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. La velocitat d'emulació actual. Valors majors o menors de 100% indiquen que la velocitat d'emulació funciona més ràpida o lentament que en una 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Els fotogrames per segon que està mostrant el joc. Variaran d'aplicació en aplicació i d'escena a escena. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. El temps que porta emular un fotograma de 3DS, sense tindre en compte el limitador de fotogrames, ni la sincronització vertical. Per a una emulació òptima, este valor no ha de superar els 16.67 ms. @@ -4204,7 +4214,7 @@ Per favor, comprove la instal·lació de FFmpeg usada per a la compilació. - + Artic Server Artic Server @@ -4281,7 +4291,7 @@ Per favor, comprove la instal·lació de FFmpeg usada per a la compilació. - + Folder does not exist! La carpeta no existix! @@ -4296,317 +4306,317 @@ Per favor, comprove la instal·lació de FFmpeg usada per a la compilació.Reiniciar temps de joc? - - - - + + + + Create Shortcut Crear drecera - + Do you want to launch the application in fullscreen? Desitja llançar esta aplicació en pantalla completa? - + Successfully created a shortcut to %1 Drecera a %1 creat amb èxit - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Aixó crearà una drecera a la AppImage actual. Pot no funcionar bé si actualitzes. Continuar? - + Failed to create a shortcut to %1 Fallada en crear una drecera a %1 - + Create Icon Crear icona - + Cannot create icon file. Path "%1" does not exist and cannot be created. No es va poder crear un arxiu d'icona. La ruta "%1" no existix i no pot ser creada. - + Dumping... Bolcant... - - + + Cancel Cancel·lar - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. No es va poder bolcar el RomFS base. Comprove el registre per a més detalls. - + Error Opening %1 Error en obrir %1 - + Select Directory Seleccionar directori - + Properties Propietats - + The application properties could not be loaded. Les propietats de l'aplicació no han pogut ser carregades. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Executable 3DS(%1);;Tots els arxius(*.*) - + Load File Carregar Fitxer - - + + Set Up System Files Configurar Fitxers del Sistema - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar necessita fitxers d'una consola real per poder utilitzar algunes de les seues funcions.<br>Pots obtindre els fitxers amb la <a href=https://github.com/azahar-emu/ArticSetupTool>ferramenta de configuració Azahar</a><br>Notes:<ul><li><b>Aquesta operació instal·larà fitxers únics de la consola a Azahar, no compartisques les teues carpetes d'usuari o nand<br>després de completar el procés de configuració!</b></li><li>Després de la configuració, Azahar s'enllaçarà a la consola que ha executat la ferramenta de configuració. Pots desvincular la<br>consola més tard des de la pestanya "Fitxers de sistema" del menú d'opcions de l'emulador.</li><li>No et connectes en línia amb Azahar i la consola 3DS al mateix temps després de configurar els arxius del sistema,<br>ja que això podria causar problemes.</li><li>La configuració de Old 3DS és necessària perquè funcione la configuració de New 3DS (configurar els dos modes és recomanat).</li><li>Els dos modes de configuració funcionaran independentment del model de la consola que execute la ferramenta de configuració.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Introduïx la direcció de la ferramenta de configuració: - + <br>Choose setup mode: <br>Tria mode de configuració: - + (ℹ️) Old 3DS setup (ℹ️) Configuració Old 3DS - - + + Setup is possible. La configuració és possible. - + (⚠) New 3DS setup (⚠) Configuració New 3DS - + Old 3DS setup is required first. La configuració Old 3DS es neccessaria abans. - + (✅) Old 3DS setup (✅) Configuració Old 3DS - - + + Setup completed. Configuració completada. - + (ℹ️) New 3DS setup (ℹ️) Configuració New 3DS - + (✅) New 3DS setup (✅) Configuració New 3DS - + The system files for the selected mode are already set up. Reinstall the files anyway? Els fitxers de sistema per al mode seleccionat ja estan configurats. Vols reinstal·lar els arxius de totes maneres? - + Load Files Carregar Fitxers - + 3DS Installation File (*.cia *.zcia) Fitxers d'Instalació de 3DS (*.cia *.zcia) - - - + + + All Files (*.*) Tots els fitxers (*.*) - + Connect to Artic Base Connectar amb Artic Base - + Enter Artic Base server address: Introduïx la direcció del servidor Artic Base - + %1 has been installed successfully. %1 s'ha instal·lat amb èxit. - + Unable to open File No es va poder obrir el Fitxer - + Could not open %1 No es va poder obrir %1 - + Installation aborted Instal·lació interrompuda - + The installation of %1 was aborted. Please see the log for more details La instal·lació de %1 ha sigut avortada.\n Per favor, mira el log per a més informació. - + Invalid File Fitxer no vàlid - + %1 is not a valid CIA %1 no és un CIA vàlid. - + CIA Encrypted CIA encriptat - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> El teu fitxer CIA està encriptat. <br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Per favor visita el nostre blog per a més informació.</a> - + Unable to find File No es pot trobar el Fitxer - + Could not find %1 No es va poder trobar %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... Desinstal·lant '%1'... - + Failed to uninstall '%1'. Va fallar la desinstal·lació de '%1'. - + Successfully uninstalled '%1'. '%1' desinstal·lat amb èxit. - + File not found Fitxer no trobat - + File "%1" not found Fitxer "%1" no trobat - + Savestates Estats - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4615,86 +4625,86 @@ Use at your own risk! Usa'ls sota el teu propi risc! - - - + + + Error opening amiibo data file Error en obrir els fitxers de dades de l'Amiibo - + A tag is already in use. Ja està en ús una etiqueta. - + Application is not looking for amiibos. L'aplicació no està buscant amiibos. - + Amiibo File (%1);; All Files (*.*) Fitxer d'Amiibo (%1);; Tots els arxius (*.*) - + Load Amiibo Carregar Amiibo - + Unable to open amiibo file "%1" for reading. No es va poder obrir el fitxer amiibo "%1" per a la seua lectura. - + Record Movie Gravar Pel·lícula - + Movie recording cancelled. Gravació de pel·lícula cancel·lada. - - + + Movie Saved Pel·lícula Guardada - - + + The movie is successfully saved. Pel·lícula guardada amb èxit. - + Application will unpause L'aplicació es resumirà - + The application will be unpaused, and the next frame will be captured. Is this okay? L'aplicació es resumirà, i el següent fotograma serà capturat. Estàs d'acord? - + Invalid Screenshot Directory Directori de captures de pantalla no vàlid - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. No es pot crear el directori de captures de pantalla. La ruta de captures de pantalla torna al seu valor per omissió. - + Could not load video dumper No es va poder carregar el bolcador de vídeo - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4707,265 +4717,265 @@ Per a instal·lar FFmpeg en Azahar, polsa Obrir i tria el directori de FFmpeg. Per a veure una guia sobre com instal·lar FFmpeg, polsa Ajuda. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) Fitxer ROM 3DS comprimit (*.%1) - + Save 3DS Compressed ROM File Desar fitxer 3DS comprimit - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) Fitxer ROM 3DS comprimit (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) Fitxer ROM 3DS (*.%1) - + Save 3DS ROM File Desar fitxer ROM 3DS - + Select Output 3DS ROM Folder - + Select FFmpeg Directory Seleccionar Directori FFmpeg - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. Al directori de FFmpeg indicat li falta %1. Per favor, assegura't d'haver seleccionat el directori correcte. - + FFmpeg has been sucessfully installed. FFmpeg ha sigut instal·lat amb èxit. - + Installation of FFmpeg failed. Check the log file for details. La instal·lació de FFmpeg ha fallat. Comprova l'arxiu del registre per a més detalls. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. No es va poder començar a gravar vídeo.<br>Assegura't que el codificador de vídeo està configurat correctament.<br>Per a més detalls, observa el registre. - + Recording %1 Gravant %1 - + Playing %1 / %2 Reproduint %1 / %2 - + Movie Finished Pel·lícula acabada - + (Accessing SharedExtData) (Accedint al SharedExtData) - + (Accessing SystemSaveData) (Accedint al SystemSaveData) - + (Accessing BossExtData) (Accedint al BossExtData) - + (Accessing ExtData) (Accedint al ExtData) - + (Accessing SaveData) (Accedint al SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Tràfic Artic: %1 %2%3 - + Speed: %1% Velocitat: %1% - + Speed: %1% / %2% Velocitat: %1% / %2% - + App: %1 FPS App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Frame: %1 ms - + VOLUME: MUTE VOLUM: SILENCI - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUM: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. Falta %1 . Per favor,<a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>bolca els teus arxius de sistema</a>.<br/>Continuar l'emulació pot resultar en penges i errors. - + A system archive Un fitxer del sistema - + System Archive Not Found El fitxer del sistema no s'ha trobat - + System Archive Missing Falta un Fitxer de Sistema - + Save/load Error Error de guardat/càrrega - + Fatal Error Error Fatal - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Error fatal.<a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Mira el log</a>per a més detalls.<br/>Continuar l'emulació pot resultar en penges i errors. - + Fatal Error encountered Error Fatal trobat - + Continue Continuar - + Quit Application Tancar aplicació - + OK Aceptar - + Would you like to exit now? Vols eixir ara? - + The application is still running. Would you like to stop emulation? L'aplicació seguix en execució. Vols parar l'emulació? - + Playback Completed Reproducció Completada - + Movie playback completed. Reproducció de pel·lícula completada. - + Update Available Actualització disponible - + Update %1 for Azahar is available. Would you like to download it? L'actualització %1 d'Azahar ja està disponible. Vols descarregar-la? - + Primary Window Finestra Primària - + Secondary Window Finestra Secundària @@ -5028,42 +5038,42 @@ Vols descarregar-la? GRenderWindow - + OpenGL not available! OpenGL no disponible! - + OpenGL shared contexts are not supported. Els contextos compartits de OpenGL no estan suportats. - + Error while initializing OpenGL! Error en iniciar OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. El teu GPU, o no suporta OpenGL, o no tens els últims drivers de la targeta gràfica. - + Error while initializing OpenGL 4.3! Error en iniciar OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 El teu GPU, o no suporta OpenGL 4.3, o no tens els últims drivers de la targeta gràfica.<br><br>Renderitzador GL:<br>%1 - + Error while initializing OpenGL ES 3.2! Error en iniciar OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 El teu GPU, o no suporta OpenGL ES 3.2, o no tens els últims drivers de la targeta gràfica.<br><br>Renderitzador GL:<br>%1 @@ -5071,180 +5081,185 @@ Vols descarregar-la? GameList - - + + Compatibility Compatibilitad - - + + Region Regió - - + + File type Tipus de Fitxer - - + + Size Grandària - - + + Play time Temps de joc - + Favorite Favorit - + Eject Cartridge - + Insert Cartridge - + Open Obrir - + Application Location Localització d'aplicacions - + Save Data Location Localització de dades de guardat - + Extra Data Location Localització de Dades Extra - + Update Data Location Localització de dades d'actualització - + DLC Data Location Localització de dades de DLC - + Texture Dump Location Localització del bolcat de textures - + Custom Texture Location Localització de les textures personalitzades - + Mods Location Localització dels mods - + Dump RomFS Bolcar RomFS - + Disk Shader Cache Caché de ombrejador de disc - + Open Shader Cache Location Obrir ubicació de cache de ombrejador - + Delete OpenGL Shader Cache Eliminar cache d'ombreig de OpenGL - + + Delete Vulkan Shader Cache + + + + Uninstall Desinstal·lar - + Everything Tot - + Application Aplicació - + Update Actualitzar - + DLC DLC - + Remove Play Time Data Llevar Dades de Temps de Joc - + Create Shortcut Crear drecera - + Add to Desktop Afegir a l'escriptori - + Add to Applications Menu Afegir al Menú d'Aplicacions - + Stress Test: App Launch - + Properties Propietats - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5253,64 +5268,64 @@ This will delete the application if installed, as well as any installed updates Aixó eliminarà l'aplicació si està instal·lada, així com també les actualitzacions i DLC instal·lades. - - + + %1 (Update) %1 (Actualització) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Estàs segur de voler desinstal·lar '%1'? - + Are you sure you want to uninstall the update for '%1'? Estàs segur de voler desinstal·lar l'actualització de '%1'? - + Are you sure you want to uninstall all DLC for '%1'? Estàs segur de voler desinstal·lar tot el DLC de '%1'? - + Scan Subfolders Escanejar subdirectoris - + Remove Application Directory Eliminar directori d'aplicacions - + Move Up Moure a dalt - + Move Down Moure avall - + Open Directory Location Obrir ubicació del directori - + Clear Reiniciar - + Name Nom @@ -5396,7 +5411,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list Faça doble clic per a agregar una nova carpeta a la llista d'aplicacions @@ -5404,27 +5419,27 @@ Screen. GameListSearchField - + of de - + result resultat - + results resultats - + Filter: Filtre: - + Enter pattern to filter Introduïx un patró per a filtrar @@ -6057,24 +6072,24 @@ Missatge de depuració: Preparant ombrejadors %1 / %2 - - Loading Shaders %1 / %2 - Carregant ombrejadors %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Iniciant... - + Now Loading %1 Carregant %1 - + Estimated Time %1 Temps Estimat %1 diff --git a/dist/languages/da_DK.ts b/dist/languages/da_DK.ts index 965e89336..516f627b4 100644 --- a/dist/languages/da_DK.ts +++ b/dist/languages/da_DK.ts @@ -328,8 +328,8 @@ Dette vil udelukke både deres forum-brugernavn og IP-adresse. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Denne efterbehandling justerer lydens hastighed så den passer emuleringens og hjælper med at undgå hak i lyden. Denne effekt skaber større forsinkelse af lyden. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ Dette vil udelukke både deres forum-brugernavn og IP-adresse. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Skalerer lydafspilningshastigheden for at tage højde for fald i emuleringens billedhastighed. Dette betyder, at lyden afspilles med fuld hastighed, selvom spillets billedhastighed er lav. Kan give problemer med synkroniseringen af lyd. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ Dette vil udelukke både deres forum-brugernavn og IP-adresse. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Vælg hvor billedet til det emulerede kamera kommer fra. Det kan være et billede eller et rigtigt kamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1494,7 +1494,7 @@ Vil du ignorere fejlen og fortsætte? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> @@ -1503,22 +1503,32 @@ Vil du ignorere fejlen og fortsætte? - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2521,7 +2531,7 @@ Vil du ignorere fejlen og fortsætte? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4117,19 +4127,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Nuværende emuleringshastighed. Værdier højere eller lavere end 100% indikerer at emuleringen kører hurtigere eller langsommere end en 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Tid det tog at emulere en 3DS-skærmbillede, hastighedsbegrænsning og v-sync er tille talt med. For emulering med fuld hastighed skal dette højest være 16,67ms. @@ -4203,7 +4213,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4280,7 +4290,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Mappen findes ikke! @@ -4295,401 +4305,401 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... - - + + Cancel Annuller - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. - + Error Opening %1 Fejl ved åbning af %1 - + Select Directory Vælg mappe - + Properties - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS-program (%1);;Alle filer (*.*) - + Load File Indlæs fil - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Indlæs filer - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Alle filer (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 blev succesfuldt installeret. - + Unable to open File Kunne ikke åbne filen - + Could not open %1 Kunne ikke åbne %1 - + Installation aborted Installation afbrudt - + The installation of %1 was aborted. Please see the log for more details Installationen af %1 blev afbrudt. Se logfilen for flere detaljer. - + Invalid File Ugyldig fil - + %1 is not a valid CIA %1 er ikke en gyldig CIA - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found Filen blev ikke fundet - + File "%1" not found Filen "%1" blev ikke fundet - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo-fil (%1);;Alle filer (*.*) - + Load Amiibo Indlæs Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Optag film - + Movie recording cancelled. Filmoptagelse afbrudt - - + + Movie Saved Film gemt - - + + The movie is successfully saved. Filmen er succesfuldt blevet gemt. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4698,264 +4708,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Hastighed: %1% - + Speed: %1% / %2% Hastighed: %1%/%2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Billede: %1ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive - + System Archive Not Found Systemarkiver blev ikke fundet - + System Archive Missing - + Save/load Error - + Fatal Error Alvorlig fejl - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered - + Continue Fortsæt - + Quit Application - + OK OK - + Would you like to exit now? Vil du afslutte nu? - + The application is still running. Would you like to stop emulation? - + Playback Completed Afspilning færdig - + Movie playback completed. Afspilning af filmen er færdig. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5018,42 +5028,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5061,244 +5071,249 @@ Would you like to download it? GameList - - + + Compatibility Kompatibilitet - - + + Region Region - - + + File type Filtype - - + + Size Størrelse - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Skan undermapper - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Åbn mappens placering - + Clear Ryd - + Name Navn @@ -5384,7 +5399,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5392,27 +5407,27 @@ Screen. GameListSearchField - + of af - + result resultat - + results resultater - + Filter: Filter: - + Enter pattern to filter Indtast mønster til filtrering @@ -6044,23 +6059,23 @@ Debug Message: - - Loading Shaders %1 / %2 + + Loading %3 %1 / %2 - + Launching... - + Now Loading %1 - + Estimated Time %1 diff --git a/dist/languages/de.ts b/dist/languages/de.ts index 0f2a63ae4..d53dc207f 100644 --- a/dist/languages/de.ts +++ b/dist/languages/de.ts @@ -328,8 +328,8 @@ Dies bannt sowohl den Forum-Nutzernamen, als auch die IP-Adresse. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Dieser Nachbearbeitungseffekt passt die Audiogeschwindigkeit an die Emulationsgeschwindigkeit an und hilft, Audiostottern zu vermeiden. Dabei wird allerdings die Audiolatenz erhöht. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ Dies bannt sowohl den Forum-Nutzernamen, als auch die IP-Adresse. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Skaliert die Audiowiedergabegeschwindigkeit, um Einbrüche in der Emulations-Framerate auszugleichen. Dies bedeutet, dass Audio mit voller Geschwindigkeit wiedergegeben wird, auch wenn die Anwendungs-Framerate niedriger ist. Kann zu Audio-Fehlsynchronisierungsproblemen führen. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ Dies bannt sowohl den Forum-Nutzernamen, als auch die IP-Adresse. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Wähle eine Quelle für das emulierte Kamerabild. Dies kann ein Bild oder eine echte Kamera sein. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1494,8 +1494,8 @@ Möchtest du den Fehler ignorieren und fortfahren? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync verhindert Bildschirmzerrung, allerdings haben manche Grafikkarten eine schlechtere Leistung, wenn VSync aktiv ist. Lass es aktiviert, wenn du keinen Leistungsunterschied bemerkst. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1503,22 +1503,32 @@ Möchtest du den Fehler ignorieren und fortfahren? V-Sync aktivieren - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global Global nutzen - + Use per-application Anwendungsspezifisch benutzen - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Verzögert den emulierten Anwendungs-Render-Thread jedes Mal um die angegebene Anzahl von Millisekunden, wenn er Render-Befehle an die GPU sendet.</p><p> Passe diese Funktion in den (sehr wenigen) Anwendungen mit dynamischer Framerate an, um Leistungsprobleme zu beheben.</p></body></html> @@ -2521,7 +2531,7 @@ Möchtest du den Fehler ignorieren und fortfahren? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4120,19 +4130,19 @@ Bitte überprüfe deine FFmpeg-Installation, die für die Kompilierung verwendet - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Derzeitige Emulationsgeschwindigkeit. Werte höher oder niedriger als 100% zeigen, dass die Emulation schneller oder langsamer läuft als auf einem 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Wie viele Bilder pro Sekunde die App aktuell anzeigt. Dies ist von App zu App und von Szene zu Szene unterschiedlich. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Die benötigte Zeit um ein 3DS-Einzelbild zu emulieren (V-Sync oder Bildratenbegrenzung nicht mitgezählt). Bei Echtzeitemulation sollte dieser Wert höchstens 16,67ms betragen. @@ -4206,7 +4216,7 @@ Bitte überprüfe deine FFmpeg-Installation, die für die Kompilierung verwendet - + Artic Server Artic Server @@ -4283,7 +4293,7 @@ Bitte überprüfe deine FFmpeg-Installation, die für die Kompilierung verwendet - + Folder does not exist! Ordner existiert nicht! @@ -4298,317 +4308,317 @@ Bitte überprüfe deine FFmpeg-Installation, die für die Kompilierung verwendet Spielzeit zurücksetzen - - - - + + + + Create Shortcut Verknüpfung erstellen - + Do you want to launch the application in fullscreen? Möchtest du die Anwendung in Vollbild starten? - + Successfully created a shortcut to %1 Es wurde erfolgreich eine Verknüpfung für %1 erstellt - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Dadurch wird eine Verknüpfung zum aktuellen AppImage erstellt. Dies funktioniert möglicherweise nicht mehr richtig, wenn du aktualisierst. Möchtest du fortfahren? - + Failed to create a shortcut to %1 Es konnte keine Verknüpfung für %1 erstellt werden - + Create Icon Icon erstellen - + Cannot create icon file. Path "%1" does not exist and cannot be created. Es konnte kein Icon-Pfad erstellt werden. „%1“ existiert nicht, oder kann nicht erstellt werden. - + Dumping... Dumpvorgang... - - + + Cancel Abbrechen - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Konnte Base-RomFS nicht dumpen. Schau im Protokoll für weitere Informationen nach. - + Error Opening %1 Fehler beim Öffnen von %1 - + Select Directory Verzeichnis auswählen - + Properties Eigenschaften - + The application properties could not be loaded. Die Anwendungseigenschaften konnten nicht geladen werden. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS Programmdatei (%1);;Alle Dateien (*.*) - + Load File Datei laden - - + + Set Up System Files Systemdateien einrichten - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar benötigt Konsolendaten und Firmware-Dateien von einer echten Konsole, um einige Funktionen nutzen zu können. <br>Du kannst solche Dateien mit dem <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Einrichtungs-Tool</a> einrichten.<br>Hinweise:<ul><li><b>Bei diesem Vorgang werden konsolenspezifische Dateien in Azahar installiert. Gib deine Benutzer- oder NAND-Ordner nicht frei, <br>nachdem der Einrichtungsvorgang durchgeführt wurde!</b></li><li>Während des Einrichtungsvorgangs verknüpft Azahar deine Konsole mit dem Einrichtungstool. Du kannst die Verknüpfung <br>jederzeit im „Systemdateien“-Reiter in den Emulatoreinstellungen trennen.</li><li>Gehe nicht zeitgleich mit deinem eigenen 3DS und Azahar online, <br>da dies sonst zu Problemen führen könnte.</li><li>Damit die New 3DS-Einrichtung funktioniert, ist zuerst eine Old 3DS-Einrichtung erforderlich (Es wird empfohlen, beides einzurichten).</li><li>Beide Setup-Modi funktionieren unabhängig vom Modell der Konsole, auf dem das Setup-Tool ausgeführt wird.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Gib die Adresse des Azahar Artic Einrichtung-Tools ein: - + <br>Choose setup mode: <br>Wähle den Einrichtungsmodus: - + (ℹ️) Old 3DS setup (ℹ️) Old 3DS-Einrichtung - - + + Setup is possible. Einrichtung ist möglich. - + (⚠) New 3DS setup (⚠) New 3DS-Einrichtung - + Old 3DS setup is required first. Du musst zuerst die Old 3DS-Einrichtung abschließen. - + (✅) Old 3DS setup (✅) Old 3DS-Einrichtung - - + + Setup completed. Einrichtung abgeschlossen - + (ℹ️) New 3DS setup (ℹ️) New 3DS-Einrichtung - + (✅) New 3DS setup (✅) New 3DS-Einrichtung - + The system files for the selected mode are already set up. Reinstall the files anyway? Die Systemdateien für den ausgewählten Modus sind bereits eingerichtet. Die Dateien trotzdem neu installieren? - + Load Files Dateien laden - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Alle Dateien (*.*) - + Connect to Artic Base Verbinde dich mit Artic-Base - + Enter Artic Base server address: Gib die Artic-Base-Serveradresse ein - + %1 has been installed successfully. %1 wurde erfolgreich installiert. - + Unable to open File Datei konnte nicht geöffnet werden - + Could not open %1 Konnte %1 nicht öffnen - + Installation aborted Installation abgebrochen - + The installation of %1 was aborted. Please see the log for more details Die Installation von %1 wurde abgebrochen. Schaue im Protokoll für weitere Informationen nach - + Invalid File Ungültige Datei - + %1 is not a valid CIA %1 ist keine gültige CIA - + CIA Encrypted CIA verschlüsselt - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Deine CIA Datei ist verschlüsselt. <br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Bitte lese unseren Blog für mehr Info.</a> - + Unable to find File Datei konnte nicht gefunden werden - + Could not find %1 %1 konnte nicht gefunden werden - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... '%1' wird deinstalliert… - + Failed to uninstall '%1'. Deinstallation von '%1' fehlgeschlagen. - + Successfully uninstalled '%1'. '%1' erfolgreich deinstalliert. - + File not found Datei nicht gefunden - + File "%1" not found Datei "%1" nicht gefunden - + Savestates Speicherstände - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4617,86 +4627,86 @@ Use at your own risk! Nutzung auf eigene Gefahr! - - - + + + Error opening amiibo data file Fehler beim Öffnen der Amiibo-Datei - + A tag is already in use. Eine Markierung wird schon genutzt. - + Application is not looking for amiibos. Die Anwendung sucht keine Amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo-Datei (%1);; Alle Dateien (*.*) - + Load Amiibo Amiibo wird geladen - + Unable to open amiibo file "%1" for reading. Die Amiibo-Datei "%1" konnte nicht zum Lesen geöffnet werden. - + Record Movie Aufnahme starten - + Movie recording cancelled. Aufnahme abgebrochen. - - + + Movie Saved Aufnahme gespeichert - - + + The movie is successfully saved. Die Aufnahme wurde erfolgreich gespeichert. - + Application will unpause Die Anwendung wird fortgesetzt - + The application will be unpaused, and the next frame will be captured. Is this okay? Die Anwendung wird fortgesetzt und das nächste Bild wird aufgenommen. Ist das okay? - + Invalid Screenshot Directory Ungültiges Bildschirmfoto-Verzeichnis - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Das angegebene Bildschirmfoto-Verzeichnis kann nicht erstellt werden. Der Bildschirmfotopfad wurde auf die Voreinstellung zurückgesetzt. - + Could not load video dumper Konnte Video-Dumper nicht laden - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4709,265 +4719,265 @@ Um FFmpeg in Azahar zu installieren, klicke auf „Offnen“ und wähle dein FFm Um eine Anleitung zur Installation von FFmpeg anzuzeigen, klicke auf „Hilfe“. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory Wähle FFmpeg-Verzeichnis - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. Das angegebene FFmpeg-Verzeichnis fehlt %1. Bitte stelle sicher, dass du das richtige Verzeichnis ausgewählt hast. - + FFmpeg has been sucessfully installed. FFmpeg wurde erfolgreich installiert. - + Installation of FFmpeg failed. Check the log file for details. Installation von FFmpeg fehlgeschlagen. Prüfe die Protokolldatei für Details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. Video-Dump konnte nicht gestartet werden.<br>Bitte überprüfe, ob der Video-Encoder richtig eingestellt ist.<br>Schau im Protokoll für weitere Informationen nach. - + Recording %1 %1 wird aufgenommen - + Playing %1 / %2 %1 / %2 wird abgespielt - + Movie Finished Aufnahme beendet - + (Accessing SharedExtData) (Zugriff auf SharedExtData) - + (Accessing SystemSaveData) (Zugriff auf SystemSaveData) - + (Accessing BossExtData) (Zugriff auf BossExtData) - + (Accessing ExtData) (Zugriff auf ExtData) - + (Accessing SaveData) (Zugriff auf SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Artic Traffic: %1 %2%3 - + Speed: %1% Geschwindigkeit: %1% - + Speed: %1% / %2% Geschwindigkeit: %1% / %2% - + App: %1 FPS App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Einzelbild: %1 ms - + VOLUME: MUTE LAUTSTÄRKE: STUMM - + VOLUME: %1% Volume percentage (e.g. 50%) LAUTSTÄRKE: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Ein Systemarchiv - + System Archive Not Found Systemarchiv nicht gefunden - + System Archive Missing Systemarchiv fehlt - + Save/load Error Speichern/Laden Fehler - + Fatal Error Schwerwiegender Fehler - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Auf schwerwiegenden Fehler gestoßen - + Continue Fortsetzen - + Quit Application Beende die Anwendung - + OK O.K. - + Would you like to exit now? Möchtest du die Anwendung jetzt verlassen? - + The application is still running. Would you like to stop emulation? Die Anwendung läuft noch. Möchtest du die Emulation stoppen? - + Playback Completed Wiedergabe abgeschlossen - + Movie playback completed. Wiedergabe der Aufnahme abgeschlossen. - + Update Available Aktualisierung verfügbar - + Update %1 for Azahar is available. Would you like to download it? Für Azahar ist die Aktualisierung %1 verfügbar. Soll es heruntergeladen werden? - + Primary Window Hauptfenster - + Secondary Window Zweifenster @@ -5030,42 +5040,42 @@ Soll es heruntergeladen werden? GRenderWindow - + OpenGL not available! OpenGL nicht verfügbar! - + OpenGL shared contexts are not supported. OpenGL-Shared-Contexts sind nicht unterstützt. - + Error while initializing OpenGL! Fehler beim Initialisieren von OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Deine Grafikkarte unterstützt möglicherweise kein OpenGL, oder sie ist nicht auf dem neuesten Stand. - + Error while initializing OpenGL 4.3! Fehler beim Initialisieren von „OpenGL 4.3“! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Deine Grafikkarte unterstützt möglicherweise kein „OpenGL 4.3“, oder sie ist nicht auf dem neusten Stand.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! Fehler beim Initialisieren von „OpenGL ES 3.2“! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Deine Grafikkarte unterstützt möglicherweise kein „OpenGL ES 3.2“, oder sie ist nicht auf dem neusten Stand.<br><br>GL-Renderer:<br>%1 @@ -5073,180 +5083,185 @@ Soll es heruntergeladen werden? GameList - - + + Compatibility Kompatibilität - - + + Region Region - - + + File type Dateiart - - + + Size Größe - - + + Play time Spielzeit - + Favorite Favorit - + Eject Cartridge - + Insert Cartridge - + Open Öffnen - + Application Location Anwendungsspeicherort - + Save Data Location Speicherdatenstandort - + Extra Data Location Extradatenstandort - + Update Data Location Updatedaten-Verzeichnis - + DLC Data Location DLC-Verzeichnis - + Texture Dump Location Textur-Dump-Pfad - + Custom Texture Location Benutzerdefinierte-Texturen-Verzeichnis - + Mods Location Mod-Verzeichnis - + Dump RomFS RomFS dumpen - + Disk Shader Cache Shader-Cache - + Open Shader Cache Location Shader-Cache-Standort öffnen - + Delete OpenGL Shader Cache OpenGL-Shader-Cache löschen - + + Delete Vulkan Shader Cache + + + + Uninstall Deinstallieren - + Everything Alles - + Application Anwendung - + Update Update - + DLC Zusatzinhalte - + Remove Play Time Data Spielzeitdaten entfernen - + Create Shortcut Verknüpfung erstellen - + Add to Desktop Zum Desktop hinzufügen - + Add to Applications Menu Zum Anwendungsmenü hinzufügen - + Stress Test: App Launch - + Properties Eigenschaften - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5255,64 +5270,64 @@ This will delete the application if installed, as well as any installed updates Dadurch werden die Anwendung, sofern installiert, sowie alle installierten Updates oder DLCs gelöscht. - - + + %1 (Update) %1 (Update) - - + + %1 (DLC) %1 (Zusatzinhalt) - + Are you sure you want to uninstall '%1'? Bist du sicher, dass du '%1' deinstallieren möchtest? - + Are you sure you want to uninstall the update for '%1'? Bist du sicher, dass du das Update für '%1' deinstallieren möchtest? - + Are you sure you want to uninstall all DLC for '%1'? Bist du sicher, dass du die Zusatzinhalte für '%1' deinstallieren möchtest? - + Scan Subfolders Unterordner scannen - + Remove Application Directory Anwendungsverzeichnis entfernen - + Move Up Hoch bewegen - + Move Down Runter bewegen - + Open Directory Location Verzeichnispfad öffnen - + Clear Leeren - + Name Name @@ -5400,7 +5415,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list Klicke doppelt, um einen neuen Ordner zur Anwendungsliste hinzuzufügen @@ -5408,27 +5423,27 @@ Screen. GameListSearchField - + of von - + result Ergebnis - + results Ergebnisse - + Filter: Filter: - + Enter pattern to filter Gib Wörter zum Filtern ein @@ -6062,24 +6077,24 @@ Debug-Meldung: Bereite Shader vor %1 / %2 - - Loading Shaders %1 / %2 - Lade Shader %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Startvorgang... - + Now Loading %1 Lädt %1 - + Estimated Time %1 Geschätzte verbleibende Zeit %1 diff --git a/dist/languages/el.ts b/dist/languages/el.ts index 67f2980d3..c12ef9d04 100644 --- a/dist/languages/el.ts +++ b/dist/languages/el.ts @@ -322,8 +322,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Αυτό το μετεπεξεργαστικό εφέ προσαρμόζει την ταχύτητα του ήχου για να ταιριάξει με την ταχύτητα εξομοίωσης και βοηθά στην αποτροπή το «κόμπιασμα» του ήχου. Αυτό ωστόσο αυξάνει την καθυστέρηση του ήχου. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Επιλέξτε την προέλευση της εικόνας της εξομοιωμένης κάμερας. Μπορεί να είναι μια εικόνα ή μια πραγματική κάμερα. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1488,8 +1488,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - Το VSync αποτρέπει τα γραφικά από το να χαλάνε, αλλά κάποιες κάρτες γραφικών μπορεί να δίνουν χειρότερη απόδοση αν η ρύθμιση είναι ανοιχτή. Κρατήστε το VSync ανοιχτό μόνο εάν δεν δείτε κάποια αλλαγή στην απόδοση. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1497,22 +1497,32 @@ Would you like to ignore the error and continue? Ενεργοποίηση VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2515,7 +2525,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4112,19 +4122,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Η ταχύτητα της προσομοίωσης. Ταχύτητες μεγαλύτερες ή μικρότερες από 100% δείχνουν ότι η προσομοίωση λειτουργεί γρηγορότερα ή πιο αργά από ένα 3DS αντίστοιχα. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Ο χρόνος που χρειάζεται για την εξομοίωση ενός καρέ 3DS, χωρίς να υπολογίζεται ο περιορισμός καρέ ή το v-sync. Για εξομοίωση σε πλήρη ταχύτητα, αυτό θα πρέπει να είναι το πολύ 16.67 ms. @@ -4198,7 +4208,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4275,7 +4285,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Ο φάκελος δεν υπάρχει! @@ -4290,402 +4300,402 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon Δημηουργία Εικονιδίου - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Αποτύπωση... - - + + Cancel Ακύρωση - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. Δεν ήταν δυνατή η αποτύπωση του βασικού RomFS. Ανατρέξτε στο αρχείο καταγραφής για λεπτομέρειες. - + Error Opening %1 Σφάλμα ανοίγματος του «%1» - + Select Directory Επιλογή καταλόγου - + Properties Ιδιότητες - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Εκτελέσιμο 3DS (%1);;Όλα τα αρχεία (*.*) - + Load File Φόρτωση αρχείου - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Φόρτωση αρχείων - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Όλα τα αρχεία (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. Το «%1» εγκαταστάθηκε επιτυχώς. - + Unable to open File Δεν είναι δυνατό το άνοιγμα του αρχείου - + Could not open %1 Δεν ήταν δυνατό το άνοιγμα του «%1» - + Installation aborted Η εγκατάσταση ακυρώθηκε - + The installation of %1 was aborted. Please see the log for more details Η εγκατάσταση του «%1» ακυρώθηκε. Παρακαλώ δείτε το αρχείο καταγραφής για περισσότερες λεπτομέρειες - + Invalid File Μη έγκυρο αρχείο - + %1 is not a valid CIA Το «%1» δεν είναι έγκυρο CIA - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found Το αρχείο δεν βρέθηκε - + File "%1" not found Το αρχείο «%1» δεν βρέθηκε - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Αρχείο Amiibo (%1);; Όλα τα αρχεία (*.*) - + Load Amiibo Φόρτωση Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Εγγραφή βίντεο - + Movie recording cancelled. Η εγγραφή βίντεο ακυρώθηκε. - - + + Movie Saved Το βίντεο αποθηκεύτηκε - - + + The movie is successfully saved. Το βίντεο αποθηκεύτηκε επιτυχώς. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4694,264 +4704,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 Εγγραφή %1 - + Playing %1 / %2 Αναπαραγωγή %1 / %2 - + Movie Finished Το βίντεο τελείωσε - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Ταχύτητα: %1% - + Speed: %1% / %2% Ταχύτητα: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Καρέ: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Ένα αρχείο συστήματος - + System Archive Not Found Δεν βρέθηκε αρχείο συστήματος - + System Archive Missing Απουσία αρχείου συστήματος - + Save/load Error Σφάλμα αποθήκευσης/φόρτωσης - + Fatal Error Κρίσιμο σφάλμα - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Προέκυψε κρίσιμο σφάλμα - + Continue Συνέχεια - + Quit Application - + OK OK - + Would you like to exit now? Θέλετε να κλείσετε το πρόγραμμα τώρα; - + The application is still running. Would you like to stop emulation? - + Playback Completed Η αναπαραγωγή ολοκληρώθηκε - + Movie playback completed. Η αναπαραγωγή βίντεο ολοκληρώθηκε. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5014,42 +5024,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5057,244 +5067,249 @@ Would you like to download it? GameList - - + + Compatibility Συμβατότητα - - + + Region Περιοχή - - + + File type Τύπος αρχείου - - + + Size Μέγεθος - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open Άνοιγμα - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS Αποτύπωση RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties Ιδιότητες - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Σάρωση υποφακέλων - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Άνοιγμα τοποθεσίας καταλόγου - + Clear Απαλοιφή - + Name Όνομα @@ -5380,7 +5395,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5388,27 +5403,27 @@ Screen. GameListSearchField - + of από - + result αποτέλεσμα - + results αποτελέσματα - + Filter: Φίλτρο: - + Enter pattern to filter Εισαγάγετε μοτίβο για φιλτράρισμα @@ -6040,24 +6055,24 @@ Debug Message: Προετοιμασία shader %1 / %2 - - Loading Shaders %1 / %2 - Φόρτωση shader %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Εκκίνηση... - + Now Loading %1 Γίνεται φόρτωση %1 - + Estimated Time %1 Εκτιμώμενος χρόνος: %1 diff --git a/dist/languages/es_ES.ts b/dist/languages/es_ES.ts index 297a6e4f2..de87fa9c6 100644 --- a/dist/languages/es_ES.ts +++ b/dist/languages/es_ES.ts @@ -328,8 +328,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Este efecto de post-procesado ajusta la velocidad del audio para igualarla a la del emulador y ayuda a prevenir parones de audio, pero aumenta la latencia de éste. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + <html><head/><body><p>Este efecto de post-procesado ajusta la velocidad del audio para igualarla a la del emulador y ayuda a prevenir parones de audio, pero aumenta la latencia de éste.</p></body></html> @@ -338,8 +338,8 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Ajusta la velocidad de reproducción de audio para tener en cuenta las caidas de fotogramas. El audio se reproducirá a velocidad completa pese a que los fotogramas de la aplicación sean bajos. Puede causar problemas de desincronización. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + <html><head/><body><p>Ajusta la velocidad de reproducción de audio para tener en cuenta las caidas de fotogramas. El audio se reproducirá a velocidad completa pese a que los fotogramas de la aplicación sean bajos. Puede causar problemas de desincronización.</p></body></html> @@ -478,8 +478,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Selecciona el lugar de donde proviene la imagen de la cámara emulada. Puede ser una imagen o una cámara real. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + <html><head/><body><p>Selecciona el lugar de donde proviene la imagen de la cámara emulada. Puede ser una imagen o una cámara real.</p></body></html> @@ -1203,7 +1203,7 @@ Would you like to ignore the error and continue? Updates - + Actualizaciones @@ -1213,17 +1213,17 @@ Would you like to ignore the error and continue? Update Channel - + Actualización Canal Stable - + Estable Prerelease - + Prelanzamiento @@ -1445,7 +1445,7 @@ Would you like to ignore the error and continue? <html><head/><body><p>Perform presentation on separate threads. Improves performance when using Vulkan in most applications. Adds ~1 frame of input lag.</p></body></html> - + <html><head/><body><p>Presentar en hilos diferentes. Mejora el rendimiento cuando se usa Vulkan en muchos juegos. Agrega ~1 fotograma de retraso de lag.</p></body></html> @@ -1494,8 +1494,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - La Sincronización Vertical impide el tearing de la imagen, pero algunas tarjetas gráficas tienen peor rendimiento cuando éste está activado. Manténlo activado si no notas ninguna diferencia en el rendimiento. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + <html><head/><body><p>La Sincronización Vertical impide el tearing de la imagen, pero algunas tarjetas gráficas tienen peor rendimiento cuando éste está activado. Manténlo activado si no notas ninguna diferencia en el rendimiento.</p></body></html> @@ -1503,22 +1503,32 @@ Would you like to ignore the error and continue? Activar Sincronización Vertical - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global Usar global - + Use per-application Usar configuración de la aplicación - + Delay Application Render Thread Demorar el hilo de ejecución de renderizado: - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Demora el hilo emulado de renderizado del juego una determinada cantidad de milisegundos cada vez que envíe comandos de renderizado a la GPU.</p><p>Ajusta esta característica en los (pocos) juegos con FPS dinámicos para arreglar problemas de rendimiento.</p></body></html> @@ -2521,8 +2531,8 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Comprime el contenido de archivos CIA cuando son instalados a la SD emulada. Solo afecta contenido CIA instalado con esta opción activada. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + <html><head/><body><p>Comprime el contenido de archivos CIA cuando son instalados a la SD emulada. Solo afecta contenido CIA instalado con esta opción activada.</p></body></html> @@ -4120,19 +4130,19 @@ Por favor, compruebe la instalación de FFmpeg usada para la compilación. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. La velocidad de emulación actual. Valores mayores o menores de 100% indican que la velocidad de emulación funciona más rápida o lentamente que en una 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Los fotogramas por segundo que está mostrando el juego. Variarán de aplicación en aplicación y de escena a escena. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. El tiempo que lleva emular un fotograma de 3DS, sin tener en cuenta el limitador de fotogramas, ni la sincronización vertical. Para una emulación óptima, este valor no debe superar los 16.67 ms. @@ -4206,7 +4216,7 @@ Por favor, compruebe la instalación de FFmpeg usada para la compilación. - + Artic Server Servidor Artic @@ -4283,7 +4293,7 @@ Por favor, compruebe la instalación de FFmpeg usada para la compilación. - + Folder does not exist! ¡La carpeta no existe! @@ -4298,317 +4308,317 @@ Por favor, compruebe la instalación de FFmpeg usada para la compilación.¿Reiniciar tiempo de juego? - - - - + + + + Create Shortcut Crear atajo - + Do you want to launch the application in fullscreen? ¿Desea lanzar esta aplicación en pantalla completa? - + Successfully created a shortcut to %1 Atajo a %1 creado con éxito - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Ésto creará un atajo a la AppImage actual. Puede no funcionar bien si actualizas. ¿Continuar? - + Failed to create a shortcut to %1 Fallo al crear un atajo a %1 - + Create Icon Crear icono - + Cannot create icon file. Path "%1" does not exist and cannot be created. No se pudo crear un archivo de icono. La ruta "%1" no existe y no puede ser creada. - + Dumping... Volcando... - - + + Cancel Cancelar - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. No se pudo volcar el RomFS base. Compruebe el registro para más detalles. - + Error Opening %1 Error al abrir %1 - + Select Directory Seleccionar directorio - + Properties Propiedades - + The application properties could not be loaded. No se pudieron cargar las propiedades de la aplicación. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Ejecutable 3DS(%1);;Todos los archivos(*.*) - + Load File Cargar Archivo - - + + Set Up System Files Configurar Archivos de Sistema - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar necesita archivos de una consola real para poder utilizar algunas de sus funciones.<br>Puedes obtener los archivos con la <a href=https://github.com/azahar-emu/ArticSetupTool>herramienta de configuración Artic</a><br>Notas:<ul><li><b>Esta operación instalará archivos únicos de la consola en Azahar, ¡no compartas las carpetas de usuario ni nand<br>después de realizar el proceso de configuración!</b></li><li>Tras la configuración, Azahar se enlazará a la consola que ha ejecutado la herramienta de configuración. Puedes desvincular la<br>consola más tarde desde la pestaña "Archivos de sistema" del menú de opciones del emulador.</li><li>No te conectes en línea con Azahar y la consola 3DS al mismo tiempo después de configurar los archivos del sistema,<br>ya que esto podría causar problemas.</li><li>Se necesita la configuración de Old 3DS para que funcione la configuración de New 3DS (configurar ambos modos es recomendado).</li><li>Ambos modos de configuración funcionarán independientemente del modelo de la consola que ejecute la herramienta de configuración.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Introduce la dirección de la herramienta de configuración Artic - + <br>Choose setup mode: <br>Elige el modo de configuración: - + (ℹ️) Old 3DS setup (ℹ️) Configuración Old 3DS - - + + Setup is possible. La configuración es posible. - + (⚠) New 3DS setup (⚠) Configuración New 3DS - + Old 3DS setup is required first. La configuración Old 3DS es necesaria primero. - + (✅) Old 3DS setup (✅) Configuración Old 3DS - - + + Setup completed. Configuración completa. - + (ℹ️) New 3DS setup (ℹ️) Configuración New 3DS - + (✅) New 3DS setup (✅) Configuración New 3DS - + The system files for the selected mode are already set up. Reinstall the files anyway? Los archivos de sistema para el modo seleccionado ya están configurados. ¿Desea reinstalar los archivos de todas formas? - + Load Files Cargar archivos - + 3DS Installation File (*.cia *.zcia) Archivo de Instalación de 3DS (*.cia *.zcia) - - - + + + All Files (*.*) Todos los archivos (*.*) - + Connect to Artic Base Conectar a Artic Base - + Enter Artic Base server address: Introduce la dirección del servidor Artic Base - + %1 has been installed successfully. %1 ha sido instalado con éxito. - + Unable to open File No se pudo abrir el Archivo - + Could not open %1 No se pudo abrir %1 - + Installation aborted Instalación interrumpida - + The installation of %1 was aborted. Please see the log for more details La instalación de %1 ha sido cancelada. Por favor, consulte los registros para más información. - + Invalid File Archivo no válido - + %1 is not a valid CIA %1 no es un archivo CIA válido - + CIA Encrypted CIA encriptado - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Tu archivo CIA está encriptado. <br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Por favor visita nuestro blog para más información.</a> - + Unable to find File No puede encontrar el archivo - + Could not find %1 No se pudo encontrar %1 - - - - + + + + Z3DS Compression Compresión Z3DS - + Failed to compress some files, check log for details. No se pudieron comprimir algunos archivos, mira el registro para más detalles. - + Failed to decompress some files, check log for details. No se pudieron descomprimir algunos archivos, mira el registro para más detalles. - + All files have been compressed successfully. Todos los archivos ya comprimido con éxtio. - + All files have been decompressed successfully. Todos los archivos ya descompresión con éxtio. - + Uninstalling '%1'... Desinstalando '%1'... - + Failed to uninstall '%1'. Falló la desinstalación de '%1'. - + Successfully uninstalled '%1'. '%1' desinstalado con éxito. - + File not found Archivo no encontrado - + File "%1" not found Archivo "%1" no encontrado - + Savestates Estados - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4617,86 +4627,86 @@ Use at your own risk! ¡Úsalos bajo tu propio riesgo! - - - + + + Error opening amiibo data file Error al abrir los archivos de datos del Amiibo - + A tag is already in use. Ya está en uso una etiqueta. - + Application is not looking for amiibos. La aplicación no está buscando amiibos. - + Amiibo File (%1);; All Files (*.*) Archivo de Amiibo(%1);; Todos los archivos (*.*) - + Load Amiibo Cargar Amiibo - + Unable to open amiibo file "%1" for reading. No se pudo abrir el archivo del amiibo "%1" para su lectura. - + Record Movie Grabar Película - + Movie recording cancelled. Grabación de película cancelada. - - + + Movie Saved Película Guardada - - + + The movie is successfully saved. Película guardada con éxito. - + Application will unpause La aplicación se despausará - + The application will be unpaused, and the next frame will be captured. Is this okay? La aplicación se despausará, y el siguiente fotograma será capturado. ¿Estás de acuerdo? - + Invalid Screenshot Directory Directorio de capturas de pantalla no válido - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. No se puede crear el directorio de capturas de pantalla. La ruta de capturas de pantalla vuelve a su valor por defecto. - + Could not load video dumper No se pudo cargar el volcador de vídeo - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4709,265 +4719,265 @@ Para instalar FFmpeg en Azahar, pulsa Abrir y elige el directorio de FFmpeg. Para ver una guía sobre cómo instalar FFmpeg, pulsa Ayuda. - + Load 3DS ROM Files Cargar ROM de 3DS - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + Archivos ROM 3DS (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) Archivo ROM 3DS comprimido (*.%1) - + Save 3DS Compressed ROM File Guardar archivo 3DS comprimido - + Select Output 3DS Compressed ROM Folder Seleccione la carpeta de salida comprimida del ROM 3DS - + Load 3DS Compressed ROM Files Cargar archivo 3DS comprimido - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) Archivo ROM 3DS comprimido (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) Archivo ROM 3DS (*.%1) - + Save 3DS ROM File Guardar archivo ROM 3DS - + Select Output 3DS ROM Folder Seleccione la carpeta de salida del ROM 3DS - + Select FFmpeg Directory Seleccionar Directorio FFmpeg - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. Al directorio de FFmpeg indicado le falta %1. Por favor, asegúrese de haber seleccionado el directorio correcto. - + FFmpeg has been sucessfully installed. FFmpeg ha sido instalado con éxito. - + Installation of FFmpeg failed. Check the log file for details. La instalación de FFmpeg ha fallado. Compruebe el archivo del registro para más detalles. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. No se pudo empezar a grabar vídeo.<br>Asegúrese de que el encodeador de vídeo está configurado correctamente.<br>Para más detalles, observe el registro. - + Recording %1 Grabando %1 - + Playing %1 / %2 Reproduciendo %1 / %2 - + Movie Finished Película terminada - + (Accessing SharedExtData) (Accediendo al SharedExtData) - + (Accessing SystemSaveData) (Accediendo al SystemSaveData) - + (Accessing BossExtData) (Accediendo al BossExtData) - + (Accessing ExtData) (Accediendo al ExtData) - + (Accessing SaveData) (Accediendo al SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Tráfico Artic: %1 %2%3 - + Speed: %1% Velocidad: %1% - + Speed: %1% / %2% Velocidad: %1% / %2% - + App: %1 FPS App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Frame: %1 ms - + VOLUME: MUTE VOLUMEN: SILENCIO - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUMEN: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. Falta %1. Por favor,<a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>vuelca tus archivos de sistema</a>.<br/>Continuar la emulación puede resultar en cuelgues y errores. - + A system archive Un archivo de sistema - + System Archive Not Found Archivo de Sistema no encontrado - + System Archive Missing Falta un Archivo de Sistema - + Save/load Error Error de guardado/carga - + Fatal Error Error Fatal - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Ha ocurrido un error fatal.<a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Mira el log</a>para más detalles.<br/>Continuar la emulación puede resultar en cuelgues y errores. - + Fatal Error encountered Error Fatal encontrado - + Continue Continuar - + Quit Application Cerrar aplicación - + OK Aceptar - + Would you like to exit now? ¿Quiere salir ahora? - + The application is still running. Would you like to stop emulation? La aplicación sigue en ejecución. ¿Quiere parar la emulación? - + Playback Completed Reproducción Completada - + Movie playback completed. Reproducción de película completada. - + Update Available Actualización disponible - + Update %1 for Azahar is available. Would you like to download it? La actualización %1 de Azahar está disponible. ¿Quieres descargarla? - + Primary Window Ventana Primaria - + Secondary Window Ventana Secundaria @@ -5030,42 +5040,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! ¡OpenGL no disponible! - + OpenGL shared contexts are not supported. Los contextos compartidos de OpenGL no están soportados. - + Error while initializing OpenGL! ¡Error al iniciar OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Tu GPU, o no soporta OpenGL, o no tienes los últimos drivers de la tarjeta gráfica. - + Error while initializing OpenGL 4.3! ¡Error al iniciar OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Tu GPU, o no soporta OpenGL 4.3, o no tienes los últimos drivers de la tarjeta gráfica.<br><br>Renderizador GL:<br>%1 - + Error while initializing OpenGL ES 3.2! ¡Error al iniciar OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Tu GPU, o no soporta OpenGL ES 3.2, o no tienes los últimos drivers de la tarjeta gráfica.<br><br>Renderizador GL:<br>%1 @@ -5073,180 +5083,185 @@ Would you like to download it? GameList - - + + Compatibility Compatibilidad - - + + Region Región - - + + File type Tipo de Archivo - - + + Size Tamaño - - + + Play time Tiempo de juego - + Favorite Favorito - + Eject Cartridge Expulsar Cartucho - + Insert Cartridge Insertar Cartucho - + Open Abrir - + Application Location Localización de aplicaciones - + Save Data Location Localización de datos de guardado - + Extra Data Location Localización de Datos Extra - + Update Data Location Localización de datos de actualización - + DLC Data Location Localización de datos de DLC - + Texture Dump Location Localización del volcado de texturas - + Custom Texture Location Localización de las texturas personalizadas - + Mods Location Localización de los mods - + Dump RomFS Volcar RomFS - + Disk Shader Cache Caché de sombreador de disco - + Open Shader Cache Location Abrir ubicación de caché de sombreador - + Delete OpenGL Shader Cache Eliminar caché de sombreado de OpenGL - + + Delete Vulkan Shader Cache + Eliminar caché de sombreado de Vulkan + + + Uninstall Desinstalar - + Everything Todo - + Application Aplicación - + Update Actualización - + DLC DLC - + Remove Play Time Data Quitar Datos de Tiempo de Juego - + Create Shortcut Crear atajo - + Add to Desktop Añadir al Escritorio - + Add to Applications Menu Añadir al Menú Aplicación - + Stress Test: App Launch Prueba de estrés: lanzamiento de la aplicación - + Properties Propiedades - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5255,64 +5270,64 @@ This will delete the application if installed, as well as any installed updates Ésto eliminará la aplicación si está instalada, así como también las actualizaciones y DLC instaladas. - - + + %1 (Update) %1 (Actualización) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? ¿Estás seguro de querer desinstalar '%1'? - + Are you sure you want to uninstall the update for '%1'? ¿Estás seguro de querer desinstalar la actualización de '%1'? - + Are you sure you want to uninstall all DLC for '%1'? ¿Estás seguro de querer desinstalar todo el DLC de '%1'? - + Scan Subfolders Escanear subdirectorios - + Remove Application Directory Quitar carpeta de aplicaciones - + Move Up Mover arriba - + Move Down Mover abajo - + Open Directory Location Abrir ubicación del directorio - + Clear Reiniciar - + Name Nombre @@ -5398,7 +5413,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list Haz doble click para añadir una nueva carpeta a la lista de aplicaciones @@ -5406,27 +5421,27 @@ Screen. GameListSearchField - + of de - + result resultado - + results resultados - + Filter: Filtro: - + Enter pattern to filter Introduzca un patrón para filtrar @@ -6059,24 +6074,24 @@ Mensaje de depuración: Preparando Sombreadores %1 / %2 - - Loading Shaders %1 / %2 - Cargando Sombreadores %1 / %2 + + Loading %3 %1 / %2 + Cargando %3 %1 / %2 - + Launching... Iniciando... - + Now Loading %1 Cargando %1 - + Estimated Time %1 Tiempo Estimado %1 diff --git a/dist/languages/fi.ts b/dist/languages/fi.ts index dc002099d..2fda25233 100644 --- a/dist/languages/fi.ts +++ b/dist/languages/fi.ts @@ -322,8 +322,8 @@ Tämä antaa porttikiellon heidän käyttäjänimelleen ja IP-osoitteelleen. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Tämä prosessointiefekti säätää äänen nopeuden samaan nopeuteen emulaation kanssa, joka auttaa ääniongelmissa. Tämä kuitenkin lisää äänen viivettä. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ Tämä antaa porttikiellon heidän käyttäjänimelleen ja IP-osoitteelleen. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ Tämä antaa porttikiellon heidän käyttäjänimelleen ja IP-osoitteelleen. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Valitse, mistä emuloitu kamera tulee. Se voi olla kuva tai oikea kamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1487,7 +1487,7 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> @@ -1496,22 +1496,32 @@ Would you like to ignore the error and continue? Aktivoi V-Sync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2514,7 +2524,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4110,19 +4120,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Nykyinen emulaationopeus. Arvot yli tai ali 100% osoittavat, että emulaatio on nopeampi tai hitaampi kuin 3DS:än nopeus. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. @@ -4196,7 +4206,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4273,7 +4283,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Kansio ei ole olemassa! @@ -4288,401 +4298,401 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... - - + + Cancel Peruuta - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. - + Error Opening %1 Virhe avatessa %1 - + Select Directory Valitse hakemisto - + Properties - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. - + Load File Lataa tiedosto - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Lataa tiedostoja - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Kaikki tiedostot (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 asennettiin onnistuneesti. - + Unable to open File Tiedostoa ei voitu avata - + Could not open %1 Ei voitu avata %1 - + Installation aborted Asennus keskeytetty - + The installation of %1 was aborted. Please see the log for more details - + Invalid File Sopimaton Tiedosto - + %1 is not a valid CIA %1 ei ole sopiva CIA-tiedosto - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found Tiedostoa ei löytynyt - + File "%1" not found Tiedosto "%1" ei löytynyt. - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo tiedosto (%1);; Kaikki tiedostot (*.*) - + Load Amiibo Lataa Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Tallenna Video - + Movie recording cancelled. - - + + Movie Saved - - + + The movie is successfully saved. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4691,264 +4701,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Nopeus: %1% - + Speed: %1% / %2% Nopeus %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Kuvaruutu: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive - + System Archive Not Found - + System Archive Missing - + Save/load Error - + Fatal Error - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered - + Continue Jatka - + Quit Application - + OK OK - + Would you like to exit now? Haluatko poistua nyt? - + The application is still running. Would you like to stop emulation? - + Playback Completed - + Movie playback completed. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5011,42 +5021,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5054,244 +5064,249 @@ Would you like to download it? GameList - - + + Compatibility Yhteensopivuus - - + + Region Alue - - + + File type Tiedoston tyyppi - - + + Size Koko - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Avaa hakemiston sijainti - + Clear Tyhjennä - + Name Nimi @@ -5377,7 +5392,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5385,27 +5400,27 @@ Screen. GameListSearchField - + of - + result - + results - + Filter: Suodatin: - + Enter pattern to filter @@ -6027,23 +6042,23 @@ Debug Message: - - Loading Shaders %1 / %2 + + Loading %3 %1 / %2 - + Launching... - + Now Loading %1 - + Estimated Time %1 diff --git a/dist/languages/fr.ts b/dist/languages/fr.ts index 292708f03..d8a6179f8 100644 --- a/dist/languages/fr.ts +++ b/dist/languages/fr.ts @@ -328,8 +328,8 @@ Cela bannira à la fois son nom du forum et son adresse IP. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Cet effet de post-traitement ajuste la vitesse audio pour correspondre à la vitesse d'émulation et aide à prévenir les distorsions. Cela augmente cependant la latence du son. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ Cela bannira à la fois son nom du forum et son adresse IP. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Adapte la vitesse de lecture de l'audio pour tenir compte des baisses de fréquence d'images de l'émulation. Cela signifie que l'audio sera lu à pleine vitesse même si la fréquence d'images de l'application est faible. Peut entraîner des problèmes de désynchronisation de l'audio. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ Cela bannira à la fois son nom du forum et son adresse IP. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Choisissez la provenance de l'image de la caméra émulée. Elle peut être une image ou une vraie caméra. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1494,8 +1494,8 @@ Souhaitez vous ignorer l'erreur et poursuivre ? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync empêche les effets de déchirement de l'image, mais elle réduira la performance de certaines cartes graphiques. Laissez-la activée si vous ne constatez pas de différence. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1503,22 +1503,32 @@ Souhaitez vous ignorer l'erreur et poursuivre ? Activer VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global Utiliser la globale - + Use per-application Utilisation par application - + Delay Application Render Thread Retarder le thread de rendu de l'application : - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Délaie le thread de rendu de l'application émulée du nombre de millisecondes spécifié chaque fois qu'il soumet des commandes de rendu au GPU.</p><p> Ajustez cette fonction dans les (très rares) applications à fréquence d'images dynamique pour résoudre les problèmes de performance.</p></body></html> @@ -2521,8 +2531,8 @@ Souhaitez vous ignorer l'erreur et poursuivre ? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Compresse le contenu des fichiers CIA lorsqu'ils sont installés sur la carte SD émulée. N'affecte que le contenu CIA installé lorsque le paramètre est activé. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + @@ -4121,19 +4131,19 @@ Veuillez vérifier votre installation FFmpeg utilisée pour la compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Vitesse actuelle d'émulation. Les valeurs supérieures ou inférieures à 100% indiquent que l'émulation est plus rapide ou plus lente qu'une 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Nombre d'images par seconde affichées par l'application. Cela varie d'une application à l'autre et d'une scène à l'autre. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Temps nécessaire pour émuler une trame 3DS, sans compter la limitation de trame ou la synchronisation verticale V-Sync. Pour une émulation à pleine vitesse, cela ne devrait pas dépasser 16,67 ms. @@ -4207,7 +4217,7 @@ Veuillez vérifier votre installation FFmpeg utilisée pour la compilation. - + Artic Server Serveur Artic @@ -4284,7 +4294,7 @@ Veuillez vérifier votre installation FFmpeg utilisée pour la compilation. - + Folder does not exist! Le répertoire n'existe pas ! @@ -4299,317 +4309,317 @@ Veuillez vérifier votre installation FFmpeg utilisée pour la compilation.Réinitialiser le temps de jeu ? - - - - + + + + Create Shortcut Créer un raccourci - + Do you want to launch the application in fullscreen? Voulez-vous lancer l'application en plein écran ? - + Successfully created a shortcut to %1 Création réussie d'un raccourci vers %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Cela créera un raccourci vers l'AppImage actuelle. Il se peut que cela ne fonctionne pas bien si vous effectuez une mise à jour. Poursuivre ? - + Failed to create a shortcut to %1 Échec de la création d'un raccourci vers %1 - + Create Icon Créer icône - + Cannot create icon file. Path "%1" does not exist and cannot be created. Impossible de créer le fichier icône. Le chemin "%1" n'existe pas et ne peut pas être créé. - + Dumping... Extraction... - - + + Cancel Annuler - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Impossible d'extraire les RomFS de base. Référez-vous aux logs pour plus de détails. - + Error Opening %1 Erreur lors de l'ouverture de %1 - + Select Directory Sélectionner un répertoire - + Properties Propriétés - + The application properties could not be loaded. Les propriétés de l'application n'ont pas pu être chargées. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Exécutable 3DS (%1);;Tous les fichiers (*.*) - + Load File Charger un fichier - - + + Set Up System Files Configurer les fichiers système - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar a besoin des données et des fichiers de firmware propres à une console réelle pour pouvoir utiliser certaines de ses fonctionnalités.<br>Ces fichiers et données peuvent être configurés à l'aide de l'outil <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool.</a><br>Notes :<ul><li><b>Cette opération installera des données uniques à la console Azahar. Ne partagez pas vos dossiers utilisateur ou nand <br>après avoir effectué le processus d'installation !</b></li><li>Pendant le processus d'installation, Azahar se connectera à la console exécutant l'outil d'installation. Vous pourrez ensuite déconnecter la <br>console à partir de l'onglet Système dans le menu de configuration de l'émulateur.</li><li>Ne vous connectez pas à Internet avec Azahar et votre console 3DS en même temps après avoir configuré les fichiers système,<br> car cela pourrait causer des problèmes.</li><li>La configuration de l'ancienne 3DS est nécessaire pour que la configuration de la nouvelle 3DS fonctionne (il est recommandé d'effectuer les deux modes de configuration).</li><li>Les deux modes de configuration fonctionnent quel que soit le modèle de console sur lequel l'outil de configuration est exécuté.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Entrer l'adresse de l'outil de configuration Artic d'Azahar : - + <br>Choose setup mode: <br>Choisissez le mode de configuration : - + (ℹ️) Old 3DS setup (ℹ️) Configuration Old 3DS - - + + Setup is possible. La configuration est possible. - + (⚠) New 3DS setup (⚠) Configuration New 3DS - + Old 3DS setup is required first. La configuration Old 3DS est requise d'abord. - + (✅) Old 3DS setup (✅) Configuration Old 3DS - - + + Setup completed. Configuration terminée. - + (ℹ️) New 3DS setup (ℹ️) Configuration New 3DS - + (✅) New 3DS setup (✅) Configuration New 3DS - + The system files for the selected mode are already set up. Reinstall the files anyway? Les fichiers système pour le mode sélectionné sont déjà configurés. Voulez-vous les réinstaller quand même ? - + Load Files Charger des fichiers - + 3DS Installation File (*.cia *.zcia) Fichier d'installation 3DS (*.cia *.zcia) - - - + + + All Files (*.*) Tous les fichiers (*.*) - + Connect to Artic Base Se connecter à Artic Base - + Enter Artic Base server address: Entrez l'adresse du serveur Artic Base : - + %1 has been installed successfully. %1 a été installé avec succès. - + Unable to open File Impossible d'ouvrir le fichier - + Could not open %1 Impossible d'ouvrir %1 - + Installation aborted Installation annulée - + The installation of %1 was aborted. Please see the log for more details L'installation de %1 a été interrompue. Veuillez consulter les logs pour plus de détails. - + Invalid File Fichier invalide - + %1 is not a valid CIA %1 n'est pas un CIA valide - + CIA Encrypted CIA chiffré - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Votre fichier CIA est chiffré.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Consultez notre blog pour plus d'informations.</a> - + Unable to find File Impossible de trouver le fichier - + Could not find %1 Impossible de trouver %1 - - - - + + + + Z3DS Compression Compression Z3DS - + Failed to compress some files, check log for details. Échec de la compression de certains fichiers, vérifiez le log pour plus de détails. - + Failed to decompress some files, check log for details. Échec de la décompression de certains fichiers, vérifiez le log pour plus de détails. - + All files have been compressed successfully. Tous les fichiers ont été compréssé avec succès. - + All files have been decompressed successfully. Tous les fichiers ont été décompréssé avec succès. - + Uninstalling '%1'... Désinstallation de '%1'... - + Failed to uninstall '%1'. Échec de la désinstallation de '%1'. - + Successfully uninstalled '%1'. Désinstallation de '%1' réussie. - + File not found Fichier non trouvé - + File "%1" not found Le fichier "%1" n'a pas été trouvé - + Savestates Points de récupération - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4618,86 +4628,86 @@ Use at your own risk! À utiliser à vos risques et périls ! - - - + + + Error opening amiibo data file Erreur d'ouverture du fichier de données amiibo - + A tag is already in use. Un tag est déjà en cours d'utilisation. - + Application is not looking for amiibos. L'application ne recherche pas d'amiibos. - + Amiibo File (%1);; All Files (*.*) Fichier Amiibo (%1);; Tous les fichiers (*.*) - + Load Amiibo Charger un Amiibo - + Unable to open amiibo file "%1" for reading. Impossible d'ouvrir le fichier amiibo "%1" pour le lire. - + Record Movie Enregistrer une vidéo - + Movie recording cancelled. Enregistrement de la vidéo annulé. - - + + Movie Saved Vidéo enregistrée - - + + The movie is successfully saved. La vidéo a été enregistrée avec succès. - + Application will unpause L'application sera rétablie. - + The application will be unpaused, and the next frame will be captured. Is this okay? L'application sera rétablie et l'image suivante sera capturée. Cela vous convient-il ? - + Invalid Screenshot Directory Répertoire des captures d'écran invalide - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Création du répertoire des captures d'écran spécifié impossible. Le chemin d'accès vers les captures d'écran est réinitialisé à sa valeur par défaut. - + Could not load video dumper Impossible de charger le module de capture vidéo. - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4710,265 +4720,265 @@ Pour installer FFmpeg sur Azahar, appuyez sur Ouvrir et sélectionnez votre rép Pour afficher un guide sur l'installation de FFmpeg, appuyez sur Aide. - + Load 3DS ROM Files Charger les fichiers ROM 3DS - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + Fichiers ROM 3DS (*.cia *.cci *.3dsx *.cci *.3ds) - + 3DS Compressed ROM File (*.%1) Fichier ROM compressé 3DS (*.%1) - + Save 3DS Compressed ROM File Enregistrer le fichier ROM compressé 3DS - + Select Output 3DS Compressed ROM Folder Sélectionner le dossier de sortie des fichiers ROM 3DS compressés - + Load 3DS Compressed ROM Files Charger des fichiers ROM 3DS compréssés - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) Fichiers ROM compressés 3DS (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) Fichier ROM 3DS (*.%1) - + Save 3DS ROM File Enregistrer le fichier ROM 3DS - + Select Output 3DS ROM Folder Sélectionner le dossier de sortie des fichiers ROM 3DS - + Select FFmpeg Directory Sélectionnez le répertoire FFmpeg. - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. Le répertoire FFmpeg fourni manque %1. Assurez-vous d'avoir sélectionné le répertoire correct. - + FFmpeg has been sucessfully installed. FFmpeg a été installé avec succès. - + Installation of FFmpeg failed. Check the log file for details. L'installation de FFmpeg a échoué. Consultez le fichier journal pour plus de détails. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. Impossible de lancer le dump vidéo.<br>Veuillez vous assurer que l'encodeur vidéo est configuré correctement.<br>Reportez-vous au logs pour plus de détails. - + Recording %1 Enregistrement %1 - + Playing %1 / %2 Lecture de %1 / %2 - + Movie Finished Vidéo terminée - + (Accessing SharedExtData) (Accès à SharedExtData) - + (Accessing SystemSaveData) (Accès à SystemSaveData) - + (Accessing BossExtData) (Accès à BossExtData) - + (Accessing ExtData) (Accès à ExtData) - + (Accessing SaveData) (Accès à SaveData) - + MB/s Mo/s - + KB/s Ko/s - + Artic Traffic: %1 %2%3 Trafic Artic : %1 %2%3 - + Speed: %1% Vitesse : %1% - + Speed: %1% / %2% Vitesse : %1% / %2% - + App: %1 FPS App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Frame: %1 ms (CPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Trame : %1 ms - + VOLUME: MUTE VOLUME : MUET - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUME : %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. %1 est manquant. Veuillez <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>vider les archives de votre système</a>. <br/>La poursuite de l'émulation peut entraîner des plantages et des bogues. - + A system archive Une archive système - + System Archive Not Found Archive système non trouvée - + System Archive Missing Archive système absente - + Save/load Error Erreur de sauvegarde/chargement - + Fatal Error Erreur fatale - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Une erreur fatale s'est produite. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Consultez le journal</a> pour plus de détails.<br/> La poursuite de l'émulation peut entraîner des plantages et des bogues. - + Fatal Error encountered Une erreur fatale s'est produite - + Continue Continuer - + Quit Application Quitter l'application - + OK OK - + Would you like to exit now? Voulez-vous quitter maintenant ? - + The application is still running. Would you like to stop emulation? L'application est toujours en cours d'exécution. Voulez-vous arrêter l'émulation ? - + Playback Completed Lecture terminée - + Movie playback completed. Lecture de la vidéo terminée. - + Update Available Mise à jour disponible - + Update %1 for Azahar is available. Would you like to download it? La mise à jour %1 pour Azahar est disponible. Souhaitez-vous la télécharger ? - + Primary Window Fenêtre principale - + Secondary Window Fenêtre secondaire @@ -5031,42 +5041,42 @@ Souhaitez-vous la télécharger ? GRenderWindow - + OpenGL not available! OpenGL non disponible ! - + OpenGL shared contexts are not supported. Les contextes partagés OpenGL ne sont pas pris en charge. - + Error while initializing OpenGL! Erreur lors de l'initialisation d'OpenGL ! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Votre GPU peut ne pas prendre en charge OpenGL ou vous ne disposez pas des derniers pilotes graphiques. - + Error while initializing OpenGL 4.3! Erreur lors de l'initialisation d'OpenGL 4.3 ! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Votre GPU peut ne pas prendre en charge OpenGL 4.3 ou vous ne disposez pas des derniers pilotes graphiques.<br><br>Moteur de rendu GL :<br>%1 - + Error while initializing OpenGL ES 3.2! Erreur lors de l'initialisation d'OpenGL ES 3.2 ! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Votre GPU pourrait ne pas prendre en charge OpenGL ES 3.2, ou vous pourriez ne pas avoir le pilote graphique le plus récent.<br><br>Moteur de rendu GL :<br>%1 @@ -5074,180 +5084,185 @@ Souhaitez-vous la télécharger ? GameList - - + + Compatibility Compatibilité - - + + Region Région - - + + File type Type de fichier - - + + Size Taille - - + + Play time Temps de jeu - + Favorite Favori - + Eject Cartridge Éjecter Cartouche - + Insert Cartridge Insérer Cartouche - + Open Ouvrir - + Application Location Chemin de l'application - + Save Data Location Chemin des données de sauvegarde - + Extra Data Location Chemin des données additionnelles - + Update Data Location Chemin des données de mise à jour - + DLC Data Location Chemin des données de DLC - + Texture Dump Location Chemin d'extraction de textures - + Custom Texture Location Chemin des textures personnalisées - + Mods Location Emplacement des Mods - + Dump RomFS Extraire RomFS - + Disk Shader Cache Cache de shader de disque - + Open Shader Cache Location Ouvrir l'emplacement du cache de shader - + Delete OpenGL Shader Cache Supprimer le cache de shader OpenGL - + + Delete Vulkan Shader Cache + + + + Uninstall Désinstaller - + Everything Tout - + Application Application - + Update Mise à jour - + DLC DLC - + Remove Play Time Data Retirer les données de temps de jeu - + Create Shortcut Créer un raccourci - + Add to Desktop Ajouter au bureau - + Add to Applications Menu Ajouter au menu d'applications - + Stress Test: App Launch Stress Test : Lancement d'application - + Properties Propriétés - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5256,64 +5271,64 @@ This will delete the application if installed, as well as any installed updates Cela supprimera l'application si elle est installée, ainsi que toutes ses mises à jour et DLCs. - - + + %1 (Update) %1 (Mise à jour) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Êtes-vous sûr de vouloir désinstaller '%1' ? - + Are you sure you want to uninstall the update for '%1'? Êtes-vous sûr de vouloir désinstaller la mise à jour de '%1' ? - + Are you sure you want to uninstall all DLC for '%1'? Êtes-vous sûr de vouloir désinstaller le DLC de '%1' ? - + Scan Subfolders Scanner les sous-dossiers - + Remove Application Directory Supprimer le répertoire d'applications - + Move Up Déplacer en haut - + Move Down Déplacer en bas - + Open Directory Location Ouvrir l'emplacement de ce répertoire - + Clear Effacer - + Name Nom @@ -5404,7 +5419,7 @@ de démarrage. GameListPlaceholder - + Double-click to add a new folder to the application list Double-cliquez pour ajouter un nouveau dossier à la liste des applications. @@ -5412,27 +5427,27 @@ de démarrage. GameListSearchField - + of sur - + result résultat - + results résultats - + Filter: Filtre : - + Enter pattern to filter Entrer le motif de filtrage @@ -6065,24 +6080,24 @@ Message de débogage : Préparation des shaders %1 / %2 - - Loading Shaders %1 / %2 - Chargement des shaders %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Démarrage... - + Now Loading %1 Chargement en cours %1 - + Estimated Time %1 Durée estimée %1 diff --git a/dist/languages/hu_HU.ts b/dist/languages/hu_HU.ts index 1a522d17f..30a3af46c 100644 --- a/dist/languages/hu_HU.ts +++ b/dist/languages/hu_HU.ts @@ -320,8 +320,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Ez az utófeldolgozási hatás beállítja a hang sebességét, hogy az emuláció sebességével megegyezzen, és segít a hang-akadozás megakadályozásában. Azonban ez megnöveli a hang késleltetését. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -330,7 +330,7 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -470,8 +470,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Válaszd ki hogy honnan jön az emulált kamera képe. Lehet egy kép, vagy egy igazi kamera is. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1486,7 +1486,7 @@ Szeretnéd figyelmen kívül hagyni a hibát, és folytatod? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> @@ -1495,22 +1495,32 @@ Szeretnéd figyelmen kívül hagyni a hibát, és folytatod? VSync engedélyezése - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2513,7 +2523,7 @@ Szeretnéd figyelmen kívül hagyni a hibát, és folytatod? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4109,19 +4119,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Jelenlegi emulációs sebesség. A 100%-nál nagyobb vagy kisebb értékek azt mutatják, hogy az emuláció egy 3DS-nél gyorsabban vagy lassabban fut. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Mennyi idő szükséges egy 3DS képkocka emulálásához, képkocka-limit vagy V-Syncet leszámítva. Teljes sebességű emulációnál ez maximum 16.67 ms-nek kéne lennie. @@ -4195,7 +4205,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4272,7 +4282,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! A mappa nem létezik! @@ -4287,401 +4297,401 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Kimentés... - - + + Cancel Mégse - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. - + Error Opening %1 Hiba Indulás %1 - + Select Directory Könyvtár Kiválasztása - + Properties Tulajdonságok - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS állományok (%1);;Minden fájl (*.*) - + Load File Fájl Betöltése - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Fájlok Betöltése - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Minden fájl (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 sikeresen fel lett telepítve. - + Unable to open File A fájl megnyitása sikertelen - + Could not open %1 Nem lehet megnyitni: %1 - + Installation aborted Telepítés megszakítva - + The installation of %1 was aborted. Please see the log for more details %1 telepítése meg lett szakítva. Kérjük olvasd el a naplót több részletért. - + Invalid File Ismeretlen Fájl - + %1 is not a valid CIA %1 nem érvényes CIA - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File A fájl nem található - + Could not find %1 %1 nem található - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... '%1' eltávolítása... - + Failed to uninstall '%1'. '%1' eltávolítása sikertelen. - + Successfully uninstalled '%1'. '%1' sikeresen eltávolítva. - + File not found A fájl nem található - + File "%1" not found Fájl "%1" nem található - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo fájl (%1);; Minden fájl (*.*) - + Load Amiibo Amiibo betöltése - + Unable to open amiibo file "%1" for reading. - + Record Movie Film felvétele - + Movie recording cancelled. Filmfelvétel megszakítva. - - + + Movie Saved Film mentve - - + + The movie is successfully saved. A film sikeresen mentve. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4690,264 +4700,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory FFmpeg könyvtár kiválasztása - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. FFmpeg sikeresen telepítve. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 Felvétel %1 - + Playing %1 / %2 Lejátszás %1 / %2 - + Movie Finished Film befejezve - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Sebesség: %1% - + Speed: %1% / %2% Sebesség: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Képkocka: %1 ms - + VOLUME: MUTE HANGERŐ: NÉMÍTVA - + VOLUME: %1% Volume percentage (e.g. 50%) HANGERŐ: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Egy rendszerarchívum - + System Archive Not Found Rendszerarchívum Nem Található - + System Archive Missing - + Save/load Error Mentési/betöltési hiba - + Fatal Error Kritikus Hiba - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Végzetes hiba lépett fel - + Continue Folytatás - + Quit Application - + OK OK - + Would you like to exit now? Szeretnél most kilépni? - + The application is still running. Would you like to stop emulation? - + Playback Completed - + Movie playback completed. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window Elsődleges ablak - + Secondary Window Másodlagos ablak @@ -5010,42 +5020,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! OpenGL nem elérhető! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! Hiba történt az OpenGL inicializálásakor! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! Hiba történt az OpenGL 4.3 inicializálásakor! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! Hiba történt az OpenGL ES 3.2 inicializálásakor! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5053,244 +5063,249 @@ Would you like to download it? GameList - - + + Compatibility Kompatibilitás - - + + Region Régió - - + + File type Fájltípus - - + + Size Méret - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open Megnyitás - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS RomFS kimentése - + Disk Shader Cache Lemez árnyékoló-gyorsítótár - + Open Shader Cache Location - + Delete OpenGL Shader Cache OpenGL árnyékoló gyorsítótár törlése - + + Delete Vulkan Shader Cache + + + + Uninstall Eltávolítás - + Everything Minden - + Application - + Update Frissítés - + DLC DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties Tulajdonságok - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) %1 (frissítés) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Biztosan törölni szeretnéd: '%1'? - + Are you sure you want to uninstall the update for '%1'? Biztosan törölni szeretnéd a(z) '%1' frissítését? - + Are you sure you want to uninstall all DLC for '%1'? Biztosan törölni szeretnéd a(z) '%1' összes DLC-jét? - + Scan Subfolders Almappák szkennelése - + Remove Application Directory - + Move Up Feljebb mozgatás - + Move Down Lejjebb mozgatás - + Open Directory Location - + Clear Törlés - + Name Név @@ -5376,7 +5391,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5384,27 +5399,27 @@ Screen. GameListSearchField - + of - + result eredmény - + results eredmény - + Filter: Szürő: - + Enter pattern to filter Adj meg egy mintát a szűréshez @@ -6036,24 +6051,24 @@ Debug Message: Árnyékolók előkészítése %1 / %2 - - Loading Shaders %1 / %2 - Árnyékolók betöltése %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Indítás... - + Now Loading %1 Betöltés %1 - + Estimated Time %1 Hátralévő idő %1 diff --git a/dist/languages/id.ts b/dist/languages/id.ts index 5db5ff73e..c38a574bb 100644 --- a/dist/languages/id.ts +++ b/dist/languages/id.ts @@ -322,8 +322,8 @@ Ini akan mem-banned nama pengguna dan alamat IP mereka - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Efek pasca-pemrosesan ini dilakukan untuk menyesuaikan kecepatan audio agar sesuai dengan kecepatan emulasi dan mencegah terjadinya audio stutter. Namun proses ini meningkatkan latensi audio. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ Ini akan mem-banned nama pengguna dan alamat IP mereka - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ Ini akan mem-banned nama pengguna dan alamat IP mereka - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Pilih di mana gambar dari kamera yang di emulasi datang berasal. Bisa dari gambar atau kamera asli. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1488,8 +1488,8 @@ Apakah Anda ingin mengabaikan kesalahan dan melanjutkan? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync mencegah layar dari "robekan", tetapi beberapa kartu grafis mempunyai performa yang lebih rendah ketika VSync dinyalakan. Biarkan VSync menyala jika kamu tidak merasa adanya perbedaan pada performa. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1497,22 +1497,32 @@ Apakah Anda ingin mengabaikan kesalahan dan melanjutkan? Aktifkan VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2515,7 +2525,7 @@ Apakah Anda ingin mengabaikan kesalahan dan melanjutkan? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4111,19 +4121,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Kecepatan emulasi saat ini. Nilai yang lebih tinggi atau lebih rendah dari 100% menunjukan emulasi berjalan lebih cepat atau lebih lambat dari 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Waktu yang dibutuhkan untuk mengemulasi frame 3DS, tidak menghitung pembatasan frame atau v-sync. setidaknya emulasi yang tergolong kecepatan penuh harus berada setidaknya pada 16.67 ms. @@ -4197,7 +4207,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4274,7 +4284,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Folder tidak ada! @@ -4289,401 +4299,401 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... - - + + Cancel Batal - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. - + Error Opening %1 Kesalahan Dalam Membuka %1 - + Select Directory Pilih Direktori - + Properties Properti - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. - + Load File Muat File - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Muat berkas - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Semua File (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 telah terinstall. - + Unable to open File Tidak dapat membuka File - + Could not open %1 Tidak dapat membuka %1 - + Installation aborted Instalasi dibatalkan - + The installation of %1 was aborted. Please see the log for more details Instalasi %1 dibatalkan. Silahkan lihat file log untuk info lebih lanjut. - + Invalid File File yang tidak valid - + %1 is not a valid CIA %1 bukan CIA yang valid - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... Mencopot Pemasangan '%1'... - + Failed to uninstall '%1'. Gagal untuk mencopot pemasangan '%1%. - + Successfully uninstalled '%1'. - + File not found File tidak ditemukan - + File "%1" not found File "%1" tidak ditemukan - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) - + Load Amiibo Muat Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Rekam Video - + Movie recording cancelled. Perekaman Video Di Batalkan. - - + + Movie Saved Video Di Simpan - - + + The movie is successfully saved. Video telah berhasil di simpan. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4692,264 +4702,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Kecepatan: %1% - + Speed: %1% / %2% Kelajuan: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Frame: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Sebuah arsip sistem - + System Archive Not Found Arsip Sistem Tidak Ditemukan - + System Archive Missing Arsip sistem tidak ada - + Save/load Error - + Fatal Error Fatal Error - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Galat fatal terjadi - + Continue Lanjut - + Quit Application - + OK OK - + Would you like to exit now? Apakah anda ingin keluar sekarang? - + The application is still running. Would you like to stop emulation? - + Playback Completed Pemutaran Kembali Telah Selesai - + Movie playback completed. Pemutaran kembali video telah selesai. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5012,42 +5022,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5055,244 +5065,249 @@ Would you like to download it? GameList - - + + Compatibility - - + + Region - - + + File type - - + + Size - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open Buka - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties Properti - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Pindai Subfolder - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Buka Lokasi Penyimpanan - + Clear Bersihkan - + Name Nama @@ -5378,7 +5393,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5386,27 +5401,27 @@ Screen. GameListSearchField - + of dari - + result hasil - + results hasil - + Filter: Saringan: - + Enter pattern to filter Masukkan pola untuk menyaring @@ -6038,23 +6053,23 @@ Debug Message: - - Loading Shaders %1 / %2 + + Loading %3 %1 / %2 - + Launching... - + Now Loading %1 - + Estimated Time %1 diff --git a/dist/languages/it.ts b/dist/languages/it.ts index 39af2d1e5..0df8a2c12 100644 --- a/dist/languages/it.ts +++ b/dist/languages/it.ts @@ -328,8 +328,8 @@ Questo bannerà sia il suo nome utente del forum che il suo indirizzo IP. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Questo effetto di post-processing permette di far combaciare la velocità dell'emulazione con quella dell'audio per prevenire scatti. Tuttavia, ciò aumenta la latenza dell'audio. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + <html><head/><body><p>Questo effetto di post-processing regola la velocità dell’audio per adattarla alla velocità dell’emulazione e aiuta a prevenire le interruzioni dell’audio. Tuttavia aumenta la latenza audio.</p></body></html> @@ -338,8 +338,8 @@ Questo bannerà sia il suo nome utente del forum che il suo indirizzo IP. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Regola la velocità della riproduzione dell'audio per compensare i cali nel framerate dell'emulazione. Questo significa che l'audio verrà riprodotto a velocità normale anche mentre il framerate dell'applicazione è basso. Può causare problemi di desincronizzazione dell'audio. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + <html><head/><body><p>Adatta la velocità di riproduzione dell’audio per compensare i cali del framerate dell’emulazione. Questo significa che l’audio verrà riprodotto a velocità normale anche quando il framerate dell’applicazione è basso. Potrebbe causare problemi di desincronizzazione audio.</p></body></html> @@ -478,8 +478,8 @@ Questo bannerà sia il suo nome utente del forum che il suo indirizzo IP. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Seleziona la fonte dell'immagine della fotocamera emulata. Può essere un'immagine o una vera fotocamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + <html><head/><body><p>Seleziona la sorgente dell’immagine della fotocamera emulata. Può essere un’immagine oppure di una fotocamera reale.</p></body></html> @@ -1494,8 +1494,8 @@ Desideri ignorare l'errore e continuare? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - Il VSync evita il tearing dello schermo, ma alcune schede video hanno prestazioni peggiori quando il VSync è abilitato. Lascialo abilitato se non noti una differenza nelle prestazioni. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + <html><head/><body><p>Il VSync impedisce il tearing dello schermo, ma alcune schede video hanno prestazioni inferiori con il VSync attivato. Mantienilo attivo se non noti differenze di prestazioni.</p></body></html> @@ -1503,22 +1503,32 @@ Desideri ignorare l'errore e continuare? Abilita VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + <html><head/><body><p>Se abilitata, questa impostazione rileva quando la frequenza di aggiornamento dello schermo è inferiore a quella del 3DS e, quando lo è, disabilita automaticamente VSync per evitare che la velocità di emulazione venga forzata al di sotto del 100%.</p></body></html> + + + + Enable display refresh rate detection + Abilita il rilevamento della frequenza di aggiornamento del display + + + Use global Usa la configurazione globale - + Use per-application Usa la configurazione dell'applicazione - + Delay Application Render Thread Ritarda il thread di render dell'applicazione - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Ritarda il thread dell'applicazione emulata per il quantitativo specificato di millisecondi ogni volta che invia dei comandi di render alla GPU.</p><p>Configura questa feature nelle (poche) applicazioni con framerate variabile per risolvere problemi di prestazioni.</p></body></html> @@ -2521,8 +2531,8 @@ Desideri ignorare l'errore e continuare? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Comprime il contenuto dei file CIA quando installati sulla scheda SD emulata. Riguarda solo i contenuti CIA installati mentre l'impostazione è abilitata. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + <html><head/><body><p>Comprime il contenuto dei file CIA durante l’installazione sulla scheda SD emulata. L’impostazione influisce solo sui contenuti CIA installati mentre è attiva.</p></body></html> @@ -4119,19 +4129,19 @@ Verifica l'installazione di FFmpeg usata per la compilazione. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Velocità di emulazione corrente. Valori più alti o più bassi di 100% indicano che l'emulazione sta funzionando più velocemente o lentamente di un 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Quanti frame al secondo l'app sta attualmente mostrando. Varierà da app ad app e da scena a scena. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Tempo necessario per emulare un fotogramma del 3DS, senza tenere conto del limite al framerate o del V-Sync. Per un'emulazione alla massima velocità, il valore non dovrebbe essere superiore a 16.67 ms. @@ -4205,7 +4215,7 @@ Verifica l'installazione di FFmpeg usata per la compilazione. - + Artic Server Artic Server @@ -4282,7 +4292,7 @@ Verifica l'installazione di FFmpeg usata per la compilazione. - + Folder does not exist! La cartella non esiste! @@ -4297,317 +4307,317 @@ Verifica l'installazione di FFmpeg usata per la compilazione. Reimpostare il tempo di gioco? - - - - + + + + Create Shortcut Crea scorciatoia - + Do you want to launch the application in fullscreen? Vuoi avviare l'applicazione a schermo intero? - + Successfully created a shortcut to %1 Scorciatoia creata con successo in %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Verrà creata una scorciatoia all'attuale AppImage, che potrebbe non funzionare correttamente in caso di aggiornamento. Vuoi continuare? - + Failed to create a shortcut to %1 Impossibile creare scorciatoia in %1 - + Create Icon Crea icona - + Cannot create icon file. Path "%1" does not exist and cannot be created. Impossibile creare file icona. La cartella "%1" non esiste e non può essere creato. - + Dumping... Estrazione in corso... - - + + Cancel Annulla - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Impossibile estrarre la RomFS base. Consulta il log per i dettagli. - + Error Opening %1 Errore nell'apertura di %1 - + Select Directory Seleziona cartella - + Properties Proprietà - + The application properties could not be loaded. Impossibile caricare le proprietà dell'applicazione. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Eseguibile 3DS (%1);;Tutti i file (*.*) - + Load File Carica file - - + + Set Up System Files Configura i file di sistema - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar necessita di dati univoci della console e di file firmware da una console reale per poter utilizzare alcune delle sue funzionalità.<br>Tali file e dati possono essere configurati con <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Note:<ul><li><b>questa operazione installerà dati univoci della console su Azahar, non condividere le cartelle utente o nand<br>dopo aver eseguito il processo di configurazione!</b></li><li>Durante il processo di configurazione, Azahar si collegherà alla console che esegue lo strumento di configurazione. È possibile scollegare la console in un secondo momento dalla scheda Sistema nel menu di configurazione dell'emulatore.</li><li>Non andare online con Azahar e con la console 3DS contemporaneamente dopo aver configurato i file di sistema,<br>poiché potrebbe causare problemi.</li><li>La vecchia configurazione 3DS è necessaria affinché la nuova configurazione 3DS funzioni (si consiglia di eseguire entrambe le modalità di configurazione).</li><li>Entrambe le modalità di configurazione funzioneranno indipendentemente dal modello della console che esegue lo strumento di configurazione.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Inserisci l'indirizzo di Artic Setup Tool: - + <br>Choose setup mode: <br>Scegli la modalità di configurazione: - + (ℹ️) Old 3DS setup (ℹ️) Configurazione Old 3DS - - + + Setup is possible. La configurazione è possibile. - + (⚠) New 3DS setup (⚠) Configurazione New 3DS - + Old 3DS setup is required first. È necessario eseguire prima la configurazione Old 3DS. - + (✅) Old 3DS setup (✅) Configurazione Old 3DS - - + + Setup completed. Configurazione completata. - + (ℹ️) New 3DS setup (ℹ️) Configurazione New 3DS - + (✅) New 3DS setup (✅) Configurazione New 3DS - + The system files for the selected mode are already set up. Reinstall the files anyway? I file di sistema per la modalità selezionata sono già stati configurati. Vuoi comunque reinstallarli? - + Load Files Carica file - + 3DS Installation File (*.cia *.zcia) File di installazione 3DS (*.cia *.zcia) - - - + + + All Files (*.*) Tutti i file (*.*) - + Connect to Artic Base Connettiti ad Artic Base - + Enter Artic Base server address: Inserisci l'indirizzo del server Artic Base: - + %1 has been installed successfully. %1 è stato installato con successo. - + Unable to open File Impossibile aprire il file - + Could not open %1 Impossibile aprire %1 - + Installation aborted Installazione annullata - + The installation of %1 was aborted. Please see the log for more details L'installazione di %1 è stata annullata. Visualizza il log per maggiori dettagli. - + Invalid File File non valido - + %1 is not a valid CIA %1 non è un CIA valido - + CIA Encrypted File CIA criptato - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Il file CIA è criptato. <br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Consulta il nostro blog per maggiori informazioni.</a> - + Unable to find File Impossibile trovare il file - + Could not find %1 Impossibile trovare %1 - - - - + + + + Z3DS Compression Compressione Z3DS - + Failed to compress some files, check log for details. Impossibile comprimere alcuni file, controllare il log per i dettagli. - + Failed to decompress some files, check log for details. Impossibile decomprimere alcuni file, controllare il log per i dettagli. - + All files have been compressed successfully. Tutti i file sono stati compressi con successo. - + All files have been decompressed successfully. Tutti i file sono stati decompressi con successo. - + Uninstalling '%1'... Disinstallazione di "%1" in corso... - + Failed to uninstall '%1'. Errore nella disinstallazione di "%1". - + Successfully uninstalled '%1'. "%1" è stato disinstallato con successo. - + File not found File non trovato - + File "%1" not found File "%1" non trovato - + Savestates Stati salvati - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4616,86 +4626,86 @@ Use at your own risk! Usali a tuo rischio e pericolo. - - - + + + Error opening amiibo data file Errore durante l'apertura dell'amiibo. - + A tag is already in use. Un tag è già in uso. - + Application is not looking for amiibos. L'applicazione non sta cercando alcun amiibo. - + Amiibo File (%1);; All Files (*.*) File Amiibo (%1);; Tutti i file (*.*) - + Load Amiibo Carica Amiibo - + Unable to open amiibo file "%1" for reading. Impossibile leggere il file amiibo "%1". - + Record Movie Registra filmato - + Movie recording cancelled. Registrazione del filmato annullata. - - + + Movie Saved Filmato salvato - - + + The movie is successfully saved. Il filmato è stato salvato con successo. - + Application will unpause L'applicazione riprenderà - + The application will be unpaused, and the next frame will be captured. Is this okay? L'applicazione riprenderà, e il prossimo frame verrà catturato. Va bene? - + Invalid Screenshot Directory Cartella degli screenshot non valida - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Non è stato possibile creare la cartella degli screenshot specificata. Il percorso a tale cartella è stato ripristinato al suo valore predefinito. - + Could not load video dumper Impossibile caricare l'estrattore del video - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4708,265 +4718,265 @@ Per installare FFmpeg su Azahar, clicca su Apri e seleziona la cartella di FFmpe Per istruzioni su come installare FFmpeg, clicca su Aiuto. - + Load 3DS ROM Files Carica file ROM 3DS - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + File ROM 3DS (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) File ROM 3DS compresso (*.%1) - + Save 3DS Compressed ROM File Salva file ROM 3DS compresso - + Select Output 3DS Compressed ROM Folder Seleziona la cartella di destinazione delle ROM 3DS compresse - + Load 3DS Compressed ROM Files Carica file ROM 3DS compressi - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) File ROM 3DS compresso (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) File ROM 3DS (*.%1) - + Save 3DS ROM File Salva file ROM 3DS - + Select Output 3DS ROM Folder Seleziona la cartella di destinazione delle ROM 3DS - + Select FFmpeg Directory Seleziona la cartella di installazione di FFmpeg. - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. La cartella di FFmpeg selezionata non contiene %1. Assicurati di aver selezionato la cartella corretta. - + FFmpeg has been sucessfully installed. FFmpeg è stato installato con successo. - + Installation of FFmpeg failed. Check the log file for details. Installazione di FFmpeg fallita. Consulta i file di log per maggiori dettagli. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. Impossibile iniziare il dumping video.<br> Assicurati che l'encoder video sia configurato correttamente. <br>Controlla il log per ulteriori dettagli. - + Recording %1 Registrazione in corso (%1) - + Playing %1 / %2 Riproduzione in corso (%1 / %2) - + Movie Finished Filmato terminato - + (Accessing SharedExtData) (Accessing SharedExtData) - + (Accessing SystemSaveData) (Accessing SystemSaveData) - + (Accessing BossExtData) (Accessing BossExtData) - + (Accessing ExtData) (Accessing ExtData) - + (Accessing SaveData) (Accessing SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Traffico Artic: %1 %2%3 - + Speed: %1% Velocità: %1% - + Speed: %1% / %2% Velocità: %1% / %2% - + App: %1 FPS App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Frame: %1 ms - + VOLUME: MUTE VOLUME: MUTO - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUME: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. %1 non trovato. <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>Estrai i tuoi archivi di sistema</a>.<br/>Proseguendo l'emulazione si potrebbero verificare arresti anomali e bug. - + A system archive Un archivio di sistema - + System Archive Not Found Archivio di sistema non trovato - + System Archive Missing Archivio di sistema mancante - + Save/load Error Errore di salvataggio/caricamento - + Fatal Error Errore irreversibile - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Si è verificato un errore fatale. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Controlla il log</a> per i dettagli.<br/>Continuare l'emulazione potrebbe causare arresti anomali e bug. - + Fatal Error encountered Errore irreversibile riscontrato - + Continue Continua - + Quit Application Chiudi applicazione - + OK OK - + Would you like to exit now? Desideri uscire ora? - + The application is still running. Would you like to stop emulation? L'applicazione è ancora in esecuzione. Vuoi arrestare l'emulazione? - + Playback Completed Riproduzione completata - + Movie playback completed. Riproduzione del filmato completata. - + Update Available Aggiornamento disponibile - + Update %1 for Azahar is available. Would you like to download it? L'aggiornamento %1 di Azahar è disponibile. Vuoi installarlo? - + Primary Window Finestra principale - + Secondary Window Finestra secondaria @@ -5029,42 +5039,42 @@ Vuoi installarlo? GRenderWindow - + OpenGL not available! OpenGL non disponibile! - + OpenGL shared contexts are not supported. Gli shared context di OpenGL non sono supportati. - + Error while initializing OpenGL! Errore durante l'inizializzazione di OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. La tua GPU potrebbe non supportare OpenGL, o non hai installato l'ultima versione dei driver video. - + Error while initializing OpenGL 4.3! Errore durante l'inizializzazione di OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 La tua GPU potrebbe non supportare OpenGL 4.3, o non hai installato l'ultima versione dei driver video.<br><br>Renderer GL:<br>%1 - + Error while initializing OpenGL ES 3.2! Errore durante l'inizializzazione di OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 La tua GPU potrebbe non supportare OpenGL ES 3.2, oppure non hai i driver grafici più recenti. <br><br>GL Renderer: <br>%1 @@ -5072,180 +5082,185 @@ Vuoi installarlo? GameList - - + + Compatibility Compatibilità - - + + Region Regione - - + + File type Tipo di file - - + + Size Dimensione - - + + Play time Tempo di gioco - + Favorite Preferito - + Eject Cartridge Rimuovi schedina - + Insert Cartridge Inserisci schedina - + Open Apri - + Application Location Cartella dell'applicazione - + Save Data Location Cartella dei dati di salvataggio - + Extra Data Location Cartella dei dati aggiuntivi - + Update Data Location Cartella dei dati di aggiornamento - + DLC Data Location Cartella dei dati dei DLC - + Texture Dump Location Cartella di estrazione delle texture - + Custom Texture Location Cartella delle texture personalizzate - + Mods Location Cartella delle mod - + Dump RomFS Estrai la RomFS - + Disk Shader Cache Cache degli shader su disco - + Open Shader Cache Location Apri la cartella della cache degli shader - + Delete OpenGL Shader Cache Elimina la cache degli shader OpenGL - + + Delete Vulkan Shader Cache + + + + Uninstall Disinstalla - + Everything Tutto - + Application Applicazione - + Update Aggiornamento - + DLC DLC - + Remove Play Time Data Rimuovi dati sul tempo di gioco - + Create Shortcut Crea scorciatoia - + Add to Desktop Aggiungi al desktop - + Add to Applications Menu Aggiungi al menù delle applicazioni - + Stress Test: App Launch Stress Test: Avvio dell'app - + Properties Proprietà - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5254,64 +5269,64 @@ This will delete the application if installed, as well as any installed updates Se installata, l'applicazione verrà rimossa assieme ad eventuali aggiornamenti e DLC installati. - - + + %1 (Update) %1 (Aggiornamento) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Sei sicuro di voler disinstallare '%1'? - + Are you sure you want to uninstall the update for '%1'? Sei sicuro di voler disinstallare l'aggiornamento di '%1'? - + Are you sure you want to uninstall all DLC for '%1'? Sei sicuro di voler disinstallare tutti i DLC di '%1'? - + Scan Subfolders Scansiona le sottocartelle - + Remove Application Directory Rimuovi cartella delle applicazioni - + Move Up Sposta in alto - + Move Down Sposta in basso - + Open Directory Location Apri cartella - + Clear Ripristina - + Name Nome @@ -5402,7 +5417,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list Fai doppio clic per aggiungere una nuova cartella alla lista delle applicazioni @@ -5410,27 +5425,27 @@ Screen. GameListSearchField - + of di - + result risultato - + results risultati - + Filter: Filtro: - + Enter pattern to filter Inserisci pattern per filtrare @@ -6063,24 +6078,24 @@ Messaggio di debug: Preparazione shader %1 / %2 - - Loading Shaders %1 / %2 - Caricamento shader %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Avvio in corso... - + Now Loading %1 Caricamento %1 - + Estimated Time %1 Tempo stimato %1 diff --git a/dist/languages/ja_JP.ts b/dist/languages/ja_JP.ts index a85fe6a11..a1c1b2cc4 100644 --- a/dist/languages/ja_JP.ts +++ b/dist/languages/ja_JP.ts @@ -328,8 +328,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - この後処理エフェクトを有効にすると、エミュレーション速度に合わせて音声を伸長し、音声のカクつきを低減させます。ただし、レイテンシが増加します。 + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - アプリケーションのフレームレートの低下を考慮して、オーディオの再生速度を調整します。これにより、ゲームのフレームレートが低くてもオーディオは通常の速度で再生されますが、オーディオのズレが発生する可能性があります。 + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - カメラに表示する画像もしくは実際に接続されているカメラを選択 + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1493,8 +1493,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSyncはティアリングを防止しますが、グラフィックカードによってはパフォーマンスが低下します。パフォーマンスに影響がないようなら有効にしてください。 + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1502,22 +1502,32 @@ Would you like to ignore the error and continue? Vsync有効化 - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global グローバルを使用します - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2520,7 +2530,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4117,19 +4127,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. 現在のエミュレーション速度です。100%以外の値は、エミュレーションが3DS実機より高速または低速で行われていることを表します。 - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. 3DSフレームをエミュレートするのにかかった時間です。フレームリミットや垂直同期の有効時にはカウントしません。フルスピードで動作させるには16.67ms以下に保つ必要があります。 @@ -4203,7 +4213,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server Artic Base @@ -4280,7 +4290,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! フォルダが見つかりません! @@ -4295,402 +4305,402 @@ Please check your FFmpeg installation used for compilation. プレイ時間をリセットしますか? - - - - + + + + Create Shortcut ショートカットを作成する - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 %1にショートカットを作成しました。 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... ダンプ中... - - + + Cancel キャンセル - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. ベースRomFSをダンプできませんでした。 詳細はログを参照してください。 - + Error Opening %1 %1 を開く際のエラー - + Select Directory 3DSのROMがあるフォルダを選択 - + Properties プロパティ - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS実行ファイル (%1);;すべてのファイル (*.*) - + Load File ゲームファイルの読み込み - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files ファイルの読み込み - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) すべてのファイル (*.*) - + Connect to Artic Base Arctic Baseに接続 - + Enter Artic Base server address: - + %1 has been installed successfully. %1が正常にインストールされました - + Unable to open File ファイルを開けません - + Could not open %1 %1を開くことができませんでした - + Installation aborted インストール中止 - + The installation of %1 was aborted. Please see the log for more details %1のインストールは中断されました。詳細はログを参照してください - + Invalid File 無効なファイル - + %1 is not a valid CIA %1は有効なCIAではありません - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File ファイルが見つかりません - + Could not find %1 %1を見つけられませんでした - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... '%1'をアンインストールしています - + Failed to uninstall '%1'. '%1' をアンインストールできませんでした - + Successfully uninstalled '%1'. - + File not found ファイルなし - + File "%1" not found ファイル%1が見つかりませんでした - + Savestates ステートセーブ - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiiboファイル (%1);; すべてのファイル (*.*) - + Load Amiibo Amiiboを読込 - + Unable to open amiibo file "%1" for reading. - + Record Movie 操作を記録 - + Movie recording cancelled. 操作の記録がキャンセルされました - - + + Movie Saved 保存成功 - - + + The movie is successfully saved. 操作記録を保存しました - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4699,264 +4709,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% スピード:%1% - + Speed: %1% / %2% スピード:%1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms フレーム:%1 ms - + VOLUME: MUTE 音量: ミュート - + VOLUME: %1% Volume percentage (e.g. 50%) 音量: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive システムアーカイブ - + System Archive Not Found システムアーカイブなし - + System Archive Missing システムアーカイブが見つかりません - + Save/load Error セーブ/ロード エラー - + Fatal Error 致命的なエラー - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered 致命的なエラーが発生しました - + Continue 続行 - + Quit Application - + OK OK - + Would you like to exit now? 今すぐ終了しますか? - + The application is still running. Would you like to stop emulation? - + Playback Completed 再生完了 - + Movie playback completed. 操作記録の再生が完了しました - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5019,42 +5029,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! OpenGLを初期化中にエラーが発生しました! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. あなたのGPUはOpenGLをサポートしていないか、あなたのGPUドライバーが古い可能性があります。 - + Error while initializing OpenGL 4.3! OpenGL 4.3を初期化中にエラーが発生しました! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 あなたのGPUはOpenGL 4.3をサポートしていないか、あなたのGPUドライバーが古い可能性があります。<br><br>GLレンダラー:<br>%1 - + Error while initializing OpenGL ES 3.2! OpenGL ES 3.2を初期化中にエラーが発生しました! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 あなたのGPUはOpenGL ES 3.2をサポートしていないか、あなたのGPUドライバーが古い可能性があります。<br><br>GLレンダラー:<br>%1 @@ -5062,244 +5072,249 @@ Would you like to download it? GameList - - + + Compatibility 動作状況 - - + + Region 地域 - - + + File type ファイルの種類 - - + + Size サイズ - - + + Play time プレイ時間 - + Favorite お気に入り - + Eject Cartridge - + Insert Cartridge - + Open 開く - + Application Location アプリケーションの場所 - + Save Data Location セーブデータの場所 - + Extra Data Location 追加データの場所 - + Update Data Location アップデートデータの場所 - + DLC Data Location DLCデータの場所 - + Texture Dump Location テクスチャのダンプ場所 - + Custom Texture Location カスタムテクスチャの場所 - + Mods Location モッドの場所 - + Dump RomFS RomFSをダンプ - + Disk Shader Cache ディスクシェーダーキャッシュ - + Open Shader Cache Location シェーダーキャッシュの場所を開く - + Delete OpenGL Shader Cache OpenGLシェーダーキャッシュを削除 - + + Delete Vulkan Shader Cache + + + + Uninstall アンインストール - + Everything すべて - + Application - + Update アップデート - + DLC DLC - + Remove Play Time Data プレイ時間のデータを削除 - + Create Shortcut ショートカットを作成する - + Add to Desktop デスクトップ - + Add to Applications Menu スタートメニューに追加 - + Stress Test: App Launch - + Properties プロパティ - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? '%1'をアンインストールしますか? - + Are you sure you want to uninstall the update for '%1'? '%1'のアップデートをアンインストールしますか? - + Are you sure you want to uninstall all DLC for '%1'? '%1'のすべてのDLCを削除しますか? - + Scan Subfolders サブフォルダも検索 - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location フォルダの場所を開く - + Clear クリア - + Name タイトル @@ -5385,7 +5400,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5393,27 +5408,27 @@ Screen. GameListSearchField - + of 件ヒットしました - + result 件中 - + results 件中 - + Filter: タイトル名でフィルタ - + Enter pattern to filter ゲームタイトルを入力 @@ -6045,24 +6060,24 @@ Debug Message: シェーダーを準備中 %1 / %2 - - Loading Shaders %1 / %2 - シェーダーをロード中 %1 / %2 + + Loading %3 %1 / %2 + - + Launching... 起動中... - + Now Loading %1 ロード中 %1 - + Estimated Time %1 予想時間 %1 diff --git a/dist/languages/ko_KR.ts b/dist/languages/ko_KR.ts index a8997bdf9..fa53ac869 100644 --- a/dist/languages/ko_KR.ts +++ b/dist/languages/ko_KR.ts @@ -322,8 +322,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - 이 후처리 효과는 오디오 속도를 에뮬레이션 속도와 일치시키고 오디오 떨림을 방지하는 데 도움이 됩니다. 하지만 이렇게 되면 오디오 지연 시간이 늘어납니다. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - 에뮬레이션되는 카메라의 이미지를 가져올 위치를 선택하십시오. 이미지 또는 실제 카메라일 수 있습니다. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1488,8 +1488,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync는 티어링을 방지하지만 일부 그래픽 카드는 VSync를 활성화하면 성능이 저하됩니다. 성능 차이가 눈에 띄지 않으면 활성화하십시오. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1497,22 +1497,32 @@ Would you like to ignore the error and continue? VSync 활성화 - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2515,7 +2525,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4113,19 +4123,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. 현재 에뮬레이션 속도. 100%보다 높거나 낮은 값은 에뮬레이션이 3DS보다 빠르거나 느린 것을 나타냅니다. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. 3DS 프레임을 에뮬레이션 하는 데 걸린 시간, 프레임제한 또는 v-동기화를 카운트하지 않음. 최대 속도 에뮬레이션의 경우, 이는 최대 16.67 ms여야 합니다. @@ -4199,7 +4209,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4276,7 +4286,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! 폴더가 존재하지 않습니다! @@ -4291,402 +4301,402 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... 덤프중... - - + + Cancel 취소 - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. 베이스 RomFS를 덤프 할 수 없습니다. 자세한 내용은 로그를 참조하십시오. - + Error Opening %1 %1 열기 오류 - + Select Directory 디렉터리 선택하기 - + Properties 속성 - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS 실행파일 (%1);;모든파일 (*.*) - + Load File 파일 불러오기 - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files 파일 불러오기 - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) 모든파일 (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1가 성공적으로 설치되었습니다. - + Unable to open File 파일을 열 수 없음 - + Could not open %1 %1을(를) 열 수 없음 - + Installation aborted 설치 중단됨 - + The installation of %1 was aborted. Please see the log for more details %1의 설치가 중단되었습니다. 자세한 내용은 로그를 참조하십시오. - + Invalid File 올바르지 않은 파일 - + %1 is not a valid CIA %1은 올바른 CIA가 아닙니다 - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File 파일을 찾을 수 없음 - + Could not find %1 1을(를) 찾을 수 없습니다 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found 파일을 찾을 수 없음 - + File "%1" not found "%1" 파일을 찾을 수 없음 - + Savestates 상태저장(Savestates) - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file Amiibo 데이터 파일 열기 오류 - + A tag is already in use. 태그가 이미 사용중입니다. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo 파일 (%1);; 모든파일 (*.*) - + Load Amiibo Amiibo 불러오기 - + Unable to open amiibo file "%1" for reading. Amiibo 파일 "%1"을 읽을 수 없습니다. - + Record Movie 무비 녹화 - + Movie recording cancelled. 무비 레코딩이 취소되었습니다. - - + + Movie Saved 무비 저장됨 - - + + The movie is successfully saved. 무비가 성공적으로 저장되었습니다 - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory 올바르지 않은 스크린숏 디렉터리 - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. 지정된 스크린숏 디렉터리를 생성할 수 없습니다. 스크린숏 경로가 기본값으로 다시 설정됩니다. - + Could not load video dumper 비디오 덤퍼를 불러올 수 없습니다 - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4695,264 +4705,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory FFmpeg 디렉토리 선택 - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. 제공된 FFmpeg 디렉토리에 %1이 없습니다. 올바른 디렉토리가 선택되었는지 확인하십시오. - + FFmpeg has been sucessfully installed. FFmpeg가 성공적으로 설치되었습니다. - + Installation of FFmpeg failed. Check the log file for details. FFmpeg 설치에 실패했습니다. 자세한 내용은 로그 파일을 확인하십시오. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 %1 녹화 중 - + Playing %1 / %2 %1 / %2 재생 중 - + Movie Finished 무비 완료됨 - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% 속도: %1% - + Speed: %1% / %2% 속도: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms 프레임: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive 시스템 아카이브 - + System Archive Not Found 시스템 아카이브를 찾을수 없습니다 - + System Archive Missing 시스템 아카이브가 없습니다 - + Save/load Error 저장하기/불러오기 오류 - + Fatal Error 치명적인 오류 - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered 치명적인 오류가 발생했습니다 - + Continue 계속 - + Quit Application - + OK 확인 - + Would you like to exit now? 지금 종료하시겠습니까? - + The application is still running. Would you like to stop emulation? - + Playback Completed 재생 완료 - + Movie playback completed. 무비 재생 완료 - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window 첫번째 윈도우 - + Secondary Window 두번째 윈도우 @@ -5015,42 +5025,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! OpenGL을 사용할 수 없습니다! - + OpenGL shared contexts are not supported. OpenGL 공유 컨텍스트가 지원되지 않습니다. - + Error while initializing OpenGL! OpenGL을 초기화하는 동안 오류가 발생했습니다! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. GPU가 OpenGL을 지원하지 않거나 최신 그래픽 드라이버가 없을 수 있습니다. - + Error while initializing OpenGL 4.3! OpenGL 4.3을 초기화하는 동안 오류가 발생했습니다! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 GPU가 OpenGL 4.3을 지원하지 않거나 최신 그래픽 드라이버를 가지고 있지 않을 수 있습니다.<br><br>GL 렌더러:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5058,244 +5068,249 @@ Would you like to download it? GameList - - + + Compatibility 호환성 - - + + Region 지역 - - + + File type 파일 타입 - - + + Size 크기 - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open 열기 - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS RomFS 덤프 - + Disk Shader Cache 디스크 셰이더 캐시 - + Open Shader Cache Location 셰이더 캐시 위치 열기 - + Delete OpenGL Shader Cache OpenGL 셰이더 캐시 삭제하기 - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties 속성 - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders 서브 디렉토리 스캔 - + Remove Application Directory - + Move Up 위로 - + Move Down 아래로 - + Open Directory Location 디렉터리 위치 열기 - + Clear 지우기 - + Name 이름 @@ -5381,7 +5396,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5389,27 +5404,27 @@ Screen. GameListSearchField - + of 중의 - + result 결과 - + results 결과 - + Filter: 필터: - + Enter pattern to filter 검색 필터 입력 @@ -6040,24 +6055,24 @@ Debug Message: 셰이더 준비중 %1 / %2 - - Loading Shaders %1 / %2 - %1 / %2 셰이더 불러오는 중 + + Loading %3 %1 / %2 + - + Launching... 실행중... - + Now Loading %1 지금 불러오는 중 %1 - + Estimated Time %1 추정 시간 %1 diff --git a/dist/languages/lt_LT.ts b/dist/languages/lt_LT.ts index 5c8254113..6750d4e67 100644 --- a/dist/languages/lt_LT.ts +++ b/dist/languages/lt_LT.ts @@ -320,8 +320,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Šis efektas suderina garso greitį su emuliacijos greičiu ir padeda išvengti garso trūkinėjimų. Bet tai kartu pailgina garso latenciją. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -330,7 +330,7 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -470,8 +470,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Pasirinkite kameros įvestį. Tai gali būti paveikslėlis arba tikra kamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1485,7 +1485,7 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> @@ -1494,22 +1494,32 @@ Would you like to ignore the error and continue? - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2512,7 +2522,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4108,19 +4118,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Dabartinės emuliacijos greitis. Reikšmės žemiau ar aukščiau 100% parodo, kad emuliacija veikia greičiau ar lėčiau negu 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Laikas, kuris buvo sunaudotas atvaizduoti 1 3DS kadrą, neskaičiuojant FPS ribojimo ar V-Sync. Pilno greičio emuliacijai reikalinga daugiausia 16.67 ms reikšmė. @@ -4194,7 +4204,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4271,7 +4281,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Aplankas neegzistuoja! @@ -4286,401 +4296,401 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... - - + + Cancel Atšaukti - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. - + Error Opening %1 Klaida atidarant %1 - + Select Directory Pasirinkti katalogą - + Properties - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS programa (%1);;Visi failai (*.*) - + Load File Įkrauti failą - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Įkrauti failus - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Visi failai (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 buvo įdiegtas sėkmingai. - + Unable to open File Negalima atverti failo - + Could not open %1 Nepavyko atverti %1 - + Installation aborted Instaliacija nutraukta - + The installation of %1 was aborted. Please see the log for more details Failo %1 instaliacija buvo nutraukta. Pasižiūrėkite į žurnalą dėl daugiau informacijos - + Invalid File Klaidingas failas - + %1 is not a valid CIA %1 nėra tinkamas CIA - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found Failas nerastas - + File "%1" not found Failas "%1" nerastas - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) „Amiibo“ failas (%1);; Visi failai (*.*) - + Load Amiibo Įkrauti „Amiibo“ - + Unable to open amiibo file "%1" for reading. - + Record Movie Įrašyti įvesčių vaizdo įrašą - + Movie recording cancelled. Įrašo įrašymas nutrauktas. - - + + Movie Saved Įrašas išsaugotas - - + + The movie is successfully saved. Filmas sėkmingai išsaugotas. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4689,264 +4699,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Greitis: %1% - + Speed: %1% / %2% Greitis: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Kadras: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive - + System Archive Not Found Sisteminis archyvas nerastas - + System Archive Missing - + Save/load Error - + Fatal Error Nepataisoma klaida - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered - + Continue Tęsti - + Quit Application - + OK Gerai - + Would you like to exit now? Ar norite išeiti? - + The application is still running. Would you like to stop emulation? - + Playback Completed Atkūrimas užbaigtas - + Movie playback completed. Įrašo atkūrimas užbaigtas. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5009,42 +5019,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5052,244 +5062,249 @@ Would you like to download it? GameList - - + + Compatibility Suderinamumas - - + + Region Regionas - - + + File type Failo tipas - - + + Size Dydis - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Ieškoti poaplankius - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Atidaryti katalogo vietą - + Clear Išvalyti - + Name Pavadinimas @@ -5375,7 +5390,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5383,27 +5398,27 @@ Screen. GameListSearchField - + of - + result rezultatų - + results rezultatai - + Filter: Filtras: - + Enter pattern to filter Įveskite raktinius žodžius filtravimui @@ -6035,23 +6050,23 @@ Debug Message: - - Loading Shaders %1 / %2 + + Loading %3 %1 / %2 - + Launching... - + Now Loading %1 - + Estimated Time %1 diff --git a/dist/languages/nb.ts b/dist/languages/nb.ts index adacee4d3..833d312c3 100644 --- a/dist/languages/nb.ts +++ b/dist/languages/nb.ts @@ -322,8 +322,8 @@ Dette ville forby både deres brukernavn og IP-adressen. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Denne etterbehandlingeffekten justerer lydhastigheten for å samsvare med emuleringshastigheten og bidrar til å forhindre lyd-hakking. Dette øker imidlertid lyd forsinkelsen. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ Dette ville forby både deres brukernavn og IP-adressen. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ Dette ville forby både deres brukernavn og IP-adressen. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Velg hvor bildet fra det emulerte kameraet kommer fra. Det kan være et bilde eller et ekte kamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1487,8 +1487,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync forhindrer screen tearing, men noen grafikkort har lavere ytelse når VSync er aktivert. Hold den aktivert hvis du ikke merker en ytelsesforskjell. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1496,22 +1496,32 @@ Would you like to ignore the error and continue? Aktiver VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2514,7 +2524,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4111,19 +4121,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Nåværende emuleringhastighet. Verdier høyere eller lavere enn 100% indikerer at emuleringen kjører raskere eller langsommere enn en 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Tid tatt for å emulere et 3DS bilde, gjelder ikke bildebegrensning eller V-Sync. For raskest emulering bør dette være høyst 16,67 ms. @@ -4197,7 +4207,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4274,7 +4284,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Mappen eksistere ikke! @@ -4289,402 +4299,402 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Dumper... - - + + Cancel Kanseller - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. Kunne ikke dumpe basen RomFS. Se loggen for detaljer. - + Error Opening %1 Feil ved åpning av %1 - + Select Directory Velg Mappe - + Properties - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS Executable (%1);;All Files (*.*) - + Load File Last Fil - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Last Filer - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Alle Filer (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 Ble installert vellykket. - + Unable to open File Kan ikke åpne Fil - + Could not open %1 Kunne ikke åpne %1 - + Installation aborted Installasjon avbrutt - + The installation of %1 was aborted. Please see the log for more details Installeringen av %1 ble avbrutt. Vennligst se logg for detaljer - + Invalid File Ugyldig Fil - + %1 is not a valid CIA %1 er ikke en gyldig CIA - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found Fil ikke funnet - + File "%1" not found Fil "%1" ble ikke funnet - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo File (%1);; All Files (*.*) - + Load Amiibo Last inn Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Ta Opp Video - + Movie recording cancelled. Filmopptak avbrutt. - - + + Movie Saved Film Lagret - - + + The movie is successfully saved. Filmen ble lagret vellykket. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4693,264 +4703,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Fart: %1% - + Speed: %1% / %2% Fart: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Bilde: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Et System Arkiv - + System Archive Not Found System Arkiv ikke funnet - + System Archive Missing System Arkiv Mangler - + Save/load Error Lagre/laste inn Feil - + Fatal Error Fatal Feil - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Fatal Feil Oppstått - + Continue Fortsett - + Quit Application - + OK OK - + Would you like to exit now? Vil du avslutte nå? - + The application is still running. Would you like to stop emulation? - + Playback Completed Avspilling Fullført - + Movie playback completed. Filmavspilling fullført. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5013,42 +5023,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5056,244 +5066,249 @@ Would you like to download it? GameList - - + + Compatibility Kompatibilitet - - + + Region Region - - + + File type Filtype - - + + Size Størrelse - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS Dump RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Skann Undermapper - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Fjern Mappe Plassering - + Clear - + Name Navn @@ -5379,7 +5394,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5387,27 +5402,27 @@ Screen. GameListSearchField - + of av - + result Resultat - + results Resultater - + Filter: Filter: - + Enter pattern to filter Skriv inn mønster for å filtrere @@ -6039,24 +6054,24 @@ Debug Message: Forbereder Shaders %1 / %2 - - Loading Shaders %1 / %2 - Laster Shaders %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Starter... - + Now Loading %1 Laster %1 - + Estimated Time %1 Estimert Tid %1 diff --git a/dist/languages/nl.ts b/dist/languages/nl.ts index 72adb8c5f..d9fba478a 100644 --- a/dist/languages/nl.ts +++ b/dist/languages/nl.ts @@ -322,8 +322,8 @@ Dit zal hun Forum gebruikersnaam en IP adres verbannen. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Dit post-processing effect past de geluidssnelheid aan om gelijk te zijn aan de emulatiesnelheid en helpt audiostotter te voorkomen. Dit zorgt wel voor meer audiovertraging. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ Dit zal hun Forum gebruikersnaam en IP adres verbannen. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ Dit zal hun Forum gebruikersnaam en IP adres verbannen. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Selecteer waar de afbeelding van de ge-emuleerde camera vandaan komt. Het mag een afbeelding zijn of een echte camera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1488,8 +1488,8 @@ Wilt u de fout negeren en doorgaan? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync voorkomt dat het scherm scheurt, maar sommige grafische kaarten presteren minder goed als VSync is ingeschakeld. Laat het ingeschakeld als u geen prestatieverschil merkt. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1497,22 +1497,32 @@ Wilt u de fout negeren en doorgaan? Activeer VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2515,7 +2525,7 @@ Wilt u de fout negeren en doorgaan? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4113,19 +4123,19 @@ Controleer de FFmpeg-installatie die wordt gebruikt voor de compilatie. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Huidige emulatiesnelheid. Waardes hoger of lager dan 100% geven aan dat de emulatie sneller of langzamer gaat dan een 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Tijd verstrekt om één 3DS frame te emuleren, zonder framelimitatie of V-Sync te tellen. Voor volledige snelheid emulatie zal dit maximaal 16.67 ms moeten zijn. @@ -4199,7 +4209,7 @@ Controleer de FFmpeg-installatie die wordt gebruikt voor de compilatie. - + Artic Server @@ -4276,7 +4286,7 @@ Controleer de FFmpeg-installatie die wordt gebruikt voor de compilatie. - + Folder does not exist! Map bestaat niet! @@ -4291,402 +4301,402 @@ Controleer de FFmpeg-installatie die wordt gebruikt voor de compilatie.Stel speeltijd opnieuw in? - - - - + + + + Create Shortcut Snelkoppeling maken - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 Het maken van een snelkoppeling naar %1 was succesvol - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Dit zal een snelkoppeling naar het huidige AppImage aanmaken. Dit zal mogelijk niet meer werken als u deze software bijwerkt. Wilt u doorgaan? - + Failed to create a shortcut to %1 Kon geen snelkoppeling naar %1 aanmaken - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Dumping... - - + + Cancel Annuleren - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. Kon basis RomFS niet dumpen. Raadpleeg het log voor meer informatie. - + Error Opening %1 Fout bij het openen van %1 - + Select Directory Selecteer Folder - + Properties Eigenschappen - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS Executable (%1);;Alle bestanden (*.*) - + Load File Laad bestand - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Laad Bestanden - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Alle bestanden (*.*) - + Connect to Artic Base Verbind met Artic Base - + Enter Artic Base server address: Voer Artic Base server adres in: - + %1 has been installed successfully. %1 is succesvol geïnstalleerd. - + Unable to open File Kan bestand niet openen - + Could not open %1 Kan %1 niet openen - + Installation aborted Installatie onderbroken - + The installation of %1 was aborted. Please see the log for more details De installatie van %1 is afgebroken. Zie het logboek voor meer details - + Invalid File Ongeldig bestand - + %1 is not a valid CIA %1 is geen geldige CIA - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File Bestand niet gevonden - + Could not find %1 Kon %1 niet vinden - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... '%1' aan het verwijderen... - + Failed to uninstall '%1'. Kon niet '%1' verwijderen. - + Successfully uninstalled '%1'. '%1' succesvol verwijderd. - + File not found Bestand niet gevonden - + File "%1" not found Bestand "%1" niet gevonden - + Savestates Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file Fout bij het openen van het amiibo databestand - + A tag is already in use. Er is al een tag in gebruik. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo Bestand (%1);; Alle Bestanden (*.*) - + Load Amiibo Laad Amiibo - + Unable to open amiibo file "%1" for reading. Kan amiibo-bestand "%1" niet openen om te worden gelezen. - + Record Movie Film opnemen - + Movie recording cancelled. Filmopname geannuleerd. - - + + Movie Saved Film Opgeslagen - - + + The movie is successfully saved. De film is met succes opgeslagen. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory Ongeldige schermafbeeldmap - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Kan de opgegeven map voor schermafbeeldingen niet maken. Het pad voor schermafbeeldingen wordt teruggezet naar de standaardwaarde. - + Could not load video dumper Kan videodumper niet laden - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4695,264 +4705,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory Selecteer FFmpeg map - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. De opgegeven FFmpeg directory ontbreekt %1. Controleer of de juiste map is geselecteerd. - + FFmpeg has been sucessfully installed. FFmpeg is met succes geïnstalleerd. - + Installation of FFmpeg failed. Check the log file for details. Installatie van FFmpeg is mislukt. Controleer het logbestand voor meer informatie. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 Opname %1 - + Playing %1 / %2 Afspelen %1 / %2 - + Movie Finished Film Voltooid - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Snelheid: %1% - + Speed: %1% / %2% Snelheid: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Frame: %1 ms - + VOLUME: MUTE VOLUME: STIL - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUME: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Een systeemarchief - + System Archive Not Found Systeem archief niet gevonden - + System Archive Missing Systeemarchief ontbreekt - + Save/load Error Opslaan/Laad fout - + Fatal Error Fatale Fout - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Fatale fout opgetreden - + Continue Doorgaan - + Quit Application - + OK OK - + Would you like to exit now? Wilt u nu afsluiten? - + The application is still running. Would you like to stop emulation? - + Playback Completed Afspelen voltooid - + Movie playback completed. Film afspelen voltooid. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window Primaire venster - + Secondary Window Secundair venster @@ -5015,42 +5025,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! OpenGL niet beschikbaar! - + OpenGL shared contexts are not supported. OpenGL gedeelde contexten zijn niet ondersteund. - + Error while initializing OpenGL! Fout bij het initialiseren van OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Het kan zijn dat uw GPU OpenGL niet ondersteunt of dat u niet de nieuwste grafische stuurprogramma geïnstalleert hebt. - + Error while initializing OpenGL 4.3! Fout tijdens het initialiseren van OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Uw GPU ondersteunt mogelijk OpenGL 4.3 niet of u hebt niet het meest recente grafische stuurprogramma.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! Fout tijdens het initialiseren van OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Uw GPU ondersteunt mogelijk OpenGL ES 3.2 niet of u hebt niet het meest recente grafische stuurprogramma.<br><br>GL Renderer:<br>%1 @@ -5058,244 +5068,249 @@ Would you like to download it? GameList - - + + Compatibility Compatibiliteit - - + + Region Regio - - + + File type Bestandstype - - + + Size Grootte - - + + Play time Gespeelde tijd - + Favorite Favoriet - + Eject Cartridge - + Insert Cartridge - + Open Open - + Application Location Applicatie Locatie - + Save Data Location Opgeslagen Gegevens Locatie - + Extra Data Location Extra Gegevens Locatie - + Update Data Location Updategegevens Locatie - + DLC Data Location DLC Gegevens Locatie - + Texture Dump Location Textures Dump Locatie - + Custom Texture Location Aangepaste Textures Locatie - + Mods Location Mods Locatie - + Dump RomFS Dump RomFS - + Disk Shader Cache Schijf Shader-cache - + Open Shader Cache Location Shader-cache locatie openen - + Delete OpenGL Shader Cache OpenGL Shader-cache verwijderen - + + Delete Vulkan Shader Cache + + + + Uninstall Verwijder - + Everything Alles - + Application - + Update Update - + DLC DLC - + Remove Play Time Data Verwijder Speeltijd Gegevens - + Create Shortcut Maak snelkoppeling - + Add to Desktop Voeg toe aan Bureaublad - + Add to Applications Menu Voeg to aan Applicatie Menu - + Stress Test: App Launch - + Properties Eigenschappen - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) %1 (Update) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Weet u zeker dat u '%1' wilt verwijderen? - + Are you sure you want to uninstall the update for '%1'? Weet u zeker dat u de update voor '%1' wilt verwijderen? - + Are you sure you want to uninstall all DLC for '%1'? Weet u zeker dat u alle DLC voor '%1' wilt verwijderen? - + Scan Subfolders Scan Submappen - + Remove Application Directory - + Move Up Omhoog - + Move Down Omlaag - + Open Directory Location Open map Locatie - + Clear Wissen - + Name Naam @@ -5381,7 +5396,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5389,27 +5404,27 @@ Screen. GameListSearchField - + of van de - + result resultaat - + results resultaten - + Filter: Filter: - + Enter pattern to filter Patroon invoeren om te filteren @@ -6041,24 +6056,24 @@ Debug Message: Shaders voorbereiden %1 / %2 - - Loading Shaders %1 / %2 - Shaders laden %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Starten... - + Now Loading %1 Nu aan het laden %1 - + Estimated Time %1 Geschatte tijd %1 diff --git a/dist/languages/pl_PL.ts b/dist/languages/pl_PL.ts index be208b3ff..e367da963 100644 --- a/dist/languages/pl_PL.ts +++ b/dist/languages/pl_PL.ts @@ -328,8 +328,8 @@ Spowodowałoby to zablokowanie zarówno nazwy użytkownika forum, jak i adresu I - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Efekt ten dostosowuje prędkość dźwięku do prędkości emulacji w celu zapobiegnięcia tzw. "stutteringu" dźwięku. Opcja ta zwiększa opóźnienie dźwięku. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + <html><head/><body><p>Efekt ten dostosowuje prędkość dźwięku do prędkości emulacji w celu zapobiegnięcia tzw. "stutteringu" dźwięku. Opcja ta zwiększa opóźnienie dźwięku.</p></body></html> @@ -338,8 +338,8 @@ Spowodowałoby to zablokowanie zarówno nazwy użytkownika forum, jak i adresu I - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Skaluje prędkość odtwarzania dźwięku, aby uwzględnić spadki liczby klatek na sekundę emulacji. Oznacza to, że dźwięk będzie odtwarzany z pełną prędkością nawet przy niskim framerate aplikacji. Może powodować problemy z desynchronizacją dźwięku. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + <html><head/><body><p>Skaluje prędkość odtwarzania dźwięku, aby uwzględnić spadki liczby klatek na sekundę emulacji. Oznacza to, że dźwięk będzie odtwarzany z pełną prędkością nawet przy niskim framerate aplikacji. Może powodować problemy z desynchronizacją dźwięku.</p></body></html> @@ -478,8 +478,8 @@ Spowodowałoby to zablokowanie zarówno nazwy użytkownika forum, jak i adresu I - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Wybierz obraz, z którego ma być emulowana kamera. Może to być plik lub prawdziwa kamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + <html><head/><body><p>Wybierz obraz, z którego ma być emulowana kamera. Może to być plik lub prawdziwa kamera.</p></body></html> @@ -1494,8 +1494,8 @@ Czy chcesz zignorować błąd i kontynuować? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync zapobiega efektowi rozdarcia ekranu, ale niektóre karty graficzne mają niższą wydajność z włączoną funkcją VSync. Pozostaw ją włączoną, jeśli nie zauważysz różnicy w wydajności. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + <html><head/><body><p>VSync zapobiega efektowi rozdarcia ekranu, ale niektóre karty graficzne mają niższą wydajność z włączoną funkcją VSync. Pozostaw ją włączoną, jeśli nie zauważysz różnicy w wydajności.</p></body></html> @@ -1503,22 +1503,32 @@ Czy chcesz zignorować błąd i kontynuować? Włącz V-Sync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + <html><head/><body><p>Po włączeniu tej opcji, wykrywa ona, kiedy częstotliwość odświeżania ekranu jest niższa niż w 3DS, i w takim przypadku, automatycznie wyłącza VSync, żeby uniknąć zmniejszenia prędkości emulacji poniżej 100%.</p></body></html> + + + + Enable display refresh rate detection + Włącz wykrywanie częstotliwości odświeżania wyświetlacza + + + Use global Użyj ustawień ogólnych - + Use per-application Użyj dla każdej aplikacji - + Delay Application Render Thread Opóźnij renderowanie wątku aplikacji: - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Opóźnia emulowany wątek renderowania aplikacji o określoną liczbę milisekund za każdym razem, gdy wysyła polecenia renderowania do GPU.</p><p>Dostosuj tę funkcję w (bardzo niewielu) aplikacjach z dynamiczną liczbą klatek na sekundę, aby naprawić problemy z wydajnością.</p></body></html> @@ -2521,8 +2531,8 @@ Czy chcesz zignorować błąd i kontynuować? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Kompresuje zawartość plików CIA zainstalowanych na emulowanej karcie SD. Ma wpływ tylko na zawartość CIA, która jest instalowana, gdy ustawienie jest włączone. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + <html><head/><body><p>Kompresuje zawartość plików CIA zainstalowanych na emulowanej karcie SD. Ma wpływ tylko na zawartość CIA, która jest instalowana, gdy ustawienie jest włączone.</p></body></html> @@ -4121,19 +4131,19 @@ Sprawdź instalację FFmpeg używaną do kompilacji. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Obecna szybkość emulacji. Wartości większe lub mniejsze niż 100 % oznaczają, że emulacja jest szybsza lub wolniejsza niż 3DS - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Jak wiele klatek na sekundę aplikacja wyświetla w tej chwili. Ta wartość będzie się różniła między aplikacji, jak również między scenami w aplikacji. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Czas potrzebny do emulacji klatki 3DS, nie zawiera limitowania klatek oraz v-sync. Dla pełnej prędkości emulacji, wartość nie powinna przekraczać 16.67 ms. @@ -4207,7 +4217,7 @@ Sprawdź instalację FFmpeg używaną do kompilacji. - + Artic Server Serwer Artic @@ -4284,7 +4294,7 @@ Sprawdź instalację FFmpeg używaną do kompilacji. - + Folder does not exist! Folder nie istnieje! @@ -4299,317 +4309,317 @@ Sprawdź instalację FFmpeg używaną do kompilacji. Zresetować czas gry? - - - - + + + + Create Shortcut Utwórz skrót - + Do you want to launch the application in fullscreen? Czy chcesz uruchomić aplikacje na pełnym ekranie? - + Successfully created a shortcut to %1 Pomyślnie utworzono skrót do %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Spowoduje to utworzenie skrótu do bieżącego obrazu aplikacji. Może to nie działać dobrze po aktualizacji. Kontynuować? - + Failed to create a shortcut to %1 Nie udało się utworzyć skrótu do %1 - + Create Icon Stwórz ikonę - + Cannot create icon file. Path "%1" does not exist and cannot be created. Nie można utworzyć pliku ikony. Ścieżka "%1" nie istnieje i nie można jej utworzyć. - + Dumping... Zrzucanie... - - + + Cancel Anuluj - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Nie można zrzucić podstawowego RomFS. Szczegółowe informacje można znaleźć w logu. - + Error Opening %1 Błąd podczas otwierania %1 - + Select Directory Wybierz Folder - + Properties Właściwości - + The application properties could not be loaded. Nie można wczytać właściwości aplikacji. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Pliki wykonywalne 3DS (%1);;Wszystkie pliki (*.*) - + Load File Załaduj Plik - - + + Set Up System Files Konfiguracja plików systemowych - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar potrzebuje plików z rzeczywistej konsoli, aby móc korzystać z niektórych jej funkcji.<br>Możesz uzyskać te pliki za pomocą <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Uwagi:<ul><li><b>Ta operacja zainstaluje unikalne pliki konsoli do Azahar, nie udostępniaj swoich folderów użytkownika lub nand<br>po wykonaniu procesu konfiguracji!</b></li><li>Podczas procesu konfiguracji Azahar połączy się z konsolą, na której uruchomione jest narzędzie instalacyjne.<br>Konsolę można później odłączyć w zakładce System w menu konfiguracji emulatora.</li><li>Nie korzystaj jednocześnie z Azahar i konsoli 3DS po skonfigurowaniu plików systemowych,<br>bo może to spowodować błędy. </li><li>Old 3DS jest wymagany do działania konfiguracji New 3DS (zalecane jest wykonanie obu tych konfiguracji).</li><li>Oba tryby konfiguracji będą działać niezależnie od modelu konsoli, na której uruchomiono narzędzie konfiguracyjne.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Wprowadź adres narzędzia konfiguracyjnego Azahar Artic: - + <br>Choose setup mode: <br>Wybierz tryb konfiguracji: - + (ℹ️) Old 3DS setup (ℹ️) Konfiguracja Old 3DS - - + + Setup is possible. Konfiguracja jest możliwa. - + (⚠) New 3DS setup (⚠) Konfiguracja New 3DS - + Old 3DS setup is required first. Najpierw wymagana jest konfiguracja Old 3DS. - + (✅) Old 3DS setup (✅) Konfiguracja Old 3DS - - + + Setup completed. Konfiguracja została zakończona. - + (ℹ️) New 3DS setup (ℹ️) Konfiguracja New 3DS - + (✅) New 3DS setup (✅) Konfiguracja New 3DS - + The system files for the selected mode are already set up. Reinstall the files anyway? Pliki systemowe dla wybranego trybu są już skonfigurowane. Czy mimo to chcesz ponownie zainstalować pliki? - + Load Files Załaduj Pliki - + 3DS Installation File (*.cia *.zcia) Plik Instalacyjny 3DS (*.cia *.zcia) - - - + + + All Files (*.*) Wszystkie Pliki (*.*) - + Connect to Artic Base Połącz z Artic Base - + Enter Artic Base server address: Wprowadź adres serwera Artic Base: - + %1 has been installed successfully. %1 został poprawnie zainstalowany. - + Unable to open File Nie można otworzyć Pliku - + Could not open %1 Nie można otworzyć %1 - + Installation aborted Instalacja przerwana - + The installation of %1 was aborted. Please see the log for more details Instalacja %1 została przerwana. Sprawdź logi, aby uzyskać więcej informacji. - + Invalid File Niepoprawny Plik - + %1 is not a valid CIA %1 nie jest prawidłowym plikiem CIA - + CIA Encrypted Plik CIA jest zaszyfrowany - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Twój plik CIA jest zaszyfrowany.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Więcej informacji można znaleźć na naszym blogu.</a> - + Unable to find File Nie można odnaleźć pliku - + Could not find %1 Nie można odnaleźć %1 - - - - + + + + Z3DS Compression Kompresuj pliki Z3DS - + Failed to compress some files, check log for details. Nie udało się skompresować niektórych plików, sprawdź log, aby uzyskać szczegółowe informacje. - + Failed to decompress some files, check log for details. Nie udało się rozpakować niektórych plików. Szczegółowe informacje można znaleźć w logu. - + All files have been compressed successfully. Wszystkie pliki zostały pomyślnie skompresowane. - + All files have been decompressed successfully. Wszystkie pliki zostały pomyślnie zdekompresowane. - + Uninstalling '%1'... Odinstalowywanie '%1'... - + Failed to uninstall '%1'. Nie udało się odinstalować '%1'. - + Successfully uninstalled '%1'. Pomyślnie odinstalowano '%1'. - + File not found Nie znaleziono pliku - + File "%1" not found Nie znaleziono pliku "%1" - + Savestates Savestate.y - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4618,86 +4628,86 @@ Use at your own risk! Używaj na własne ryzyko! - - - + + + Error opening amiibo data file Błąd podczas otwierania pliku danych amiibo - + A tag is already in use. Tag jest już używany. - + Application is not looking for amiibos. Aplikacja nie szuka amiibo. - + Amiibo File (%1);; All Files (*.*) Plik Amiibo (%1);; Wszystkie pliki (*.*) - + Load Amiibo Załaduj Amiibo - + Unable to open amiibo file "%1" for reading. Nie można otworzyć pliku amiibo "%1" do odczytu. - + Record Movie Nagraj Film - + Movie recording cancelled. Nagrywanie zostało przerwane. - - + + Movie Saved Zapisano Film - - + + The movie is successfully saved. Film został poprawnie zapisany. - + Application will unpause Aplikacja zostanie wstrzymana - + The application will be unpaused, and the next frame will be captured. Is this okay? Aplikacja zostanie zatrzymana, a następna klatka zostanie przechwycona. Czy jest to w porządku? - + Invalid Screenshot Directory Nieprawidłowy katalog zrzutów ekranu - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Nie można utworzyć określonego katalogu zrzutów ekranu. Ścieżka zrzutu ekranu zostanie przywrócona do wartości domyślnej. - + Could not load video dumper Nie można załadować zrzutu filmu - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4710,265 +4720,265 @@ Aby zainstalować FFmpeg do Azahar, naciśnij Otwórz i wybierz katalog FFmpeg. Aby wyświetlić poradnik dotyczący instalacji FFmpeg, naciśnij Pomoc. - + Load 3DS ROM Files Załaduj pliki ROMów 3DS - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + Pliki ROMów 3DS (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) Skompresowany plik ROMu 3DS (*.%1) - + Save 3DS Compressed ROM File Zapisz skompresowany plik ROMu 3DS - + Select Output 3DS Compressed ROM Folder Wybierz folder wyjściowy skompresowanych plików ROMów 3DS - + Load 3DS Compressed ROM Files Wczytaj skompresowane pliki ROMów 3DS - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) Skompresowane pliki ROMów 3DS (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) Plik ROMu 3DS (*.%1) - + Save 3DS ROM File Zapisz plik ROMu 3DS - + Select Output 3DS ROM Folder Wybierz folder wyjściowy ROMów 3DS - + Select FFmpeg Directory Wybierz katalog FFmpeg - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. W podanym katalogu FFmpeg brakuje %1. Upewnij się, że wybrany został poprawny katalog. - + FFmpeg has been sucessfully installed. FFmpeg został pomyślnie zainstalowany. - + Installation of FFmpeg failed. Check the log file for details. Instalacja FFmpeg nie powiodła się. Sprawdź plik dziennika, aby uzyskać szczegółowe informacje. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. Nie można uruchomić zrzutu filmu.<br>Upewnij się, że koder filmu jest poprawnie skonfigurowany.<br>Szczegółowe informacje można znaleźć w logu. - + Recording %1 Nagrywanie %1 - + Playing %1 / %2 Odtwarzanie %1 / %2 - + Movie Finished Film ukończony - + (Accessing SharedExtData) (Uzyskiwanie dostępu do SharedExtData) - + (Accessing SystemSaveData) (Uzyskiwanie dostępu do SystemSaveData) - + (Accessing BossExtData) (Uzyskiwanie dostępu do BossExtData) - + (Accessing ExtData) (Uzyskiwanie dostępu do ExtData) - + (Accessing SaveData) (Uzyskiwanie dostępu do SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Ruch Artic: %1 %2%3 - + Speed: %1% Prędkość: %1% - + Speed: %1% / %2% Prędkość: %1% / %2% - + App: %1 FPS Aplikacja: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Klatka: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Klatka: %1 ms - + VOLUME: MUTE GŁOŚNOŚĆ: WYCISZONA - + VOLUME: %1% Volume percentage (e.g. 50%) GŁOŚNOŚĆ: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. Brakuje %1. Prosimy o <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>zrzucenie archiwów systemowych</a>.<br/>Dalsze korzystanie z emulacji może spowodować awarie i błędy. - + A system archive Archiwum systemu - + System Archive Not Found Archiwum Systemowe nie zostało odnalezione - + System Archive Missing Brak archiwum systemu - + Save/load Error Błąd zapisywania/wczytywania - + Fatal Error Krytyczny Błąd - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Wystąpił krytyczny błąd. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Sprawdź szczegóły w logu</a>.<br/>Dalsze korzystanie z emulacji może spowodować awarie i błędy. - + Fatal Error encountered Wystąpił błąd krytyczny - + Continue Kontynuuj - + Quit Application Wyjdź z aplikacji - + OK OK - + Would you like to exit now? Czy chcesz teraz wyjść? - + The application is still running. Would you like to stop emulation? Aplikacja jest nadal uruchomiona. Czy chcesz przerwać emulację? - + Playback Completed Odtwarzanie Zakończone - + Movie playback completed. Odtwarzanie filmu zostało zakończone. - + Update Available Dostępna jest aktualizacja - + Update %1 for Azahar is available. Would you like to download it? Aktualizacja %1 dla Azahar jest dostępna. Czy chcesz ją pobrać? - + Primary Window Główne okno - + Secondary Window Dodatkowe okno @@ -5031,42 +5041,42 @@ Czy chcesz ją pobrać? GRenderWindow - + OpenGL not available! OpenGL jest niedostępny! - + OpenGL shared contexts are not supported. Współdzielone konteksty OpenGL nie są obsługiwane. - + Error while initializing OpenGL! Błąd podczas uruchamiania OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Twój procesor graficzny może nie obsługiwać OpenGL lub nie masz najnowszego sterownika graficznego. - + Error while initializing OpenGL 4.3! Błąd podczas uruchamiania OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Twój procesor graficzny może nie obsługiwać OpenGL 4.3 lub nie masz najnowszego sterownika graficznego.<br><br>Render GL:<br>%1 - + Error while initializing OpenGL ES 3.2! Błąd podczas uruchamiania OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Twój procesor graficzny może nie obsługiwać OpenGL ES 3.2 lub nie masz najnowszego sterownika graficznego.<br><br>Render GL:<br>%1 @@ -5074,180 +5084,185 @@ Czy chcesz ją pobrać? GameList - - + + Compatibility Kompatybilność - - + + Region Region - - + + File type Typ pliku - - + + Size Rozmiar - - + + Play time Czas gry - + Favorite Ulubione - + Eject Cartridge Wyjmij Kartridż - + Insert Cartridge Włóż Kartridż - + Open Otwórz - + Application Location Lokalizacja aplikacji - + Save Data Location Lokalizacja zapisywanych danych - + Extra Data Location Lokalizacja dodatkowych danych - + Update Data Location Zaktualizuj lokalizację danych - + DLC Data Location Lokalizacja danych DLC - + Texture Dump Location Lokalizacja zrzutu tekstur - + Custom Texture Location Lokalizacja niestandardowych tekstur - + Mods Location Lokalizacja modów - + Dump RomFS Zrzuć RomFS - + Disk Shader Cache Pamięć podręczna shaderów na dysku - + Open Shader Cache Location Otwórz lokalizację pamięci podręcznej shaderów - + Delete OpenGL Shader Cache Usuń pamięć podręczną shaderów OpenGL - + + Delete Vulkan Shader Cache + Usuń pamięć podręczną shaderów Vulkan + + + Uninstall Odinstaluj - + Everything Wszystko - + Application Aplikacja - + Update Aktualizacje - + DLC DLC - + Remove Play Time Data Usuń dane czasu gry - + Create Shortcut Utwórz skrót - + Add to Desktop Dodaj do pulpitu - + Add to Applications Menu Dodaj do menu aplikacji - + Stress Test: App Launch Analiza wydajnościowa: uruchomienie aplikacji - + Properties Właściwości - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5256,64 +5271,64 @@ This will delete the application if installed, as well as any installed updates Spowoduje to usunięcie aplikacji, jeśli jest zainstalowana, a także wszelkich zainstalowanych aktualizacji lub DLC. - - + + %1 (Update) %1 (Aktualizacja) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Czy na pewno chcesz odinstalować '%1'? - + Are you sure you want to uninstall the update for '%1'? Czy na pewno chcesz odinstalować aktualizacje '%1'? - + Are you sure you want to uninstall all DLC for '%1'? Czy na pewno chcesz odinstalować DLC '%1'? - + Scan Subfolders Przeszukaj Podkatalogi - + Remove Application Directory Usuń Katalog Aplikacji - + Move Up Przesuń w górę - + Move Down Przesuń w dół - + Open Directory Location Otwórz lokalizację katalogu - + Clear Wyczyść - + Name Nazwa @@ -5404,7 +5419,7 @@ Działa jedynie ekran startowy. GameListPlaceholder - + Double-click to add a new folder to the application list Kliknij dwukrotnie, aby dodać nowy folder do listy aplikacji. @@ -5412,27 +5427,27 @@ Działa jedynie ekran startowy. GameListSearchField - + of z - + result wynik - + results wyniki - + Filter: Filtr: - + Enter pattern to filter Wprowadź wzór filtra @@ -6065,24 +6080,24 @@ Komunikat debugowania: Przygotowanie shaderów %1 / %2 - - Loading Shaders %1 / %2 - Ładowanie shaderów %1 / %2 + + Loading %3 %1 / %2 + Ładowanie %3 %1 / %2 - + Launching... Uruchamianie... - + Now Loading %1 Aktualnie Ładowanie %1 - + Estimated Time %1 Oszacowany Czas %1 diff --git a/dist/languages/pt_BR.ts b/dist/languages/pt_BR.ts index c81f215a6..8c78ab5d9 100644 --- a/dist/languages/pt_BR.ts +++ b/dist/languages/pt_BR.ts @@ -328,8 +328,8 @@ Esta ação banirá tanto o seu nome de usuário do fórum como o seu endereço - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Este efeito de pós-processamento ajusta a velocidade do áudio para acompanhar a velocidade de emulação e ajuda a evitar cortes no áudio. No entanto, isto aumenta a latência do áudio. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + <html><head/><body><p>Este efeito de pós-processamento ajusta a velocidade do áudio para corresponder à velocidade de emulação e ajuda a evitar engasgos no áudio. Isso, no entanto, aumenta a latência de áudio.</p></body></html> @@ -338,8 +338,8 @@ Esta ação banirá tanto o seu nome de usuário do fórum como o seu endereço - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Dimensiona a velocidade de reprodução de áudio para compensar quedas na taxa de quadros da emulação. Isso significa que o áudio será reproduzido em velocidade máxima mesmo quando a taxa de quadros do aplicativo estiver baixa. Pode causar problemas de dessincronização de áudio. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + <html><head/><body><p>Escala a velocidade de reprodução do áudio para compensar quedas na taxa de quadros da emulação. Isso significa que o áudio será reproduzido em velocidade total mesmo quando a taxa de quadros do aplicativo estiver baixa. Pode causar problemas de dessincronização de áudio.</p></body></html> @@ -478,8 +478,8 @@ Esta ação banirá tanto o seu nome de usuário do fórum como o seu endereço - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Selecione de onde vem a imagem da câmera emulada. Pode ser um arquivo de imagem ou uma câmera real. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + <html><head/><body><p>Selecione de onde vem a imagem da câmera emulada. Pode ser uma imagem ou uma câmera real.</p></body></html> @@ -1494,8 +1494,8 @@ Gostaria de ignorar o erro e continuar? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - A sincronização vertical evita que as imagens do jogo pareçam cortadas, porém algumas placas gráficas apresentam redução de desempenho quando esta está ativada. Deixe-a ativada se você não reparar alguma diferença de desempenho. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + <html><head/><body><p>O VSync evita que a tela sofra rasgos (tearing), mas algumas placas de vídeo têm menor desempenho com o VSync ativado. Mantenha-o ativado se não notar diferença de desempenho.</p></body></html> @@ -1503,22 +1503,32 @@ Gostaria de ignorar o erro e continuar? Ativar sincronização vertical - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + <html><head/><body><p>Quando ativada, esta configuração detecta quando a taxa de atualização da tela está abaixo da do 3DS e, quando isso ocorre, desativa o VSync automaticamente para evitar que a velocidade de emulação seja forçada abaixo de 100%.</p></body></html> + + + + Enable display refresh rate detection + Ativar detecção da taxa de atualização da tela + + + Use global Usar global - + Use per-application Usar por aplicativo - + Delay Application Render Thread Atrasar Thread de Renderização da Aplicação - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Atrasa a thread de renderização emulada por uma quantidade específica de milissegundos toda vez que forem enviados comandos para a GPU.</p><p>Ajuste esta funcionalidade em (bem poucos) aplicativos com taxa de quadros dinâmica para corrigir problemas de desempenho.</p></body></html> @@ -2521,8 +2531,8 @@ Gostaria de ignorar o erro e continuar? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Comprime o conteúdo dos arquivos CIA ao serem instalados no cartão SD emulado. Afeta apenas o conteúdo CIA instalado enquanto a opção estiver ativada. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + <html><head/><body><p>Compacta o conteúdo de arquivos CIA quando instalados no cartão SD emulado. Afeta apenas o conteúdo CIA instalado enquanto a configuração estiver ativada.</p></body></html> @@ -4120,19 +4130,19 @@ Por favor, verifique a instalação do FFmpeg usada para compilação. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Velocidade atual de emulação. Valores maiores ou menores que 100% indicam que a emulação está funcionando mais rápida ou lentamente que num 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Quantos quadros por segundo que o app está mostrando atualmente. Pode variar de app para app e cena para cena. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Tempo levado para emular um quadro do 3DS, sem considerar o limitador de taxa de quadros ou a sincronização vertical. Valores menores ou iguais a 16,67 ms indicam que a emulação está em velocidade plena. @@ -4206,7 +4216,7 @@ Por favor, verifique a instalação do FFmpeg usada para compilação. - + Artic Server Servidor Artic @@ -4283,7 +4293,7 @@ Por favor, verifique a instalação do FFmpeg usada para compilação. - + Folder does not exist! A pasta não existe! @@ -4298,317 +4308,317 @@ Por favor, verifique a instalação do FFmpeg usada para compilação.Redefinir tempo de jogo? - - - - + + + + Create Shortcut Criar Atalho - + Do you want to launch the application in fullscreen? Você gostaria de iniciar o aplicativo em tela cheia? - + Successfully created a shortcut to %1 Atalho para %1 criado com sucesso - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Isso criará um atalho para o AppImage atual. Isso pode não funcionar bem se você atualizar. Deseja continuar? - + Failed to create a shortcut to %1 Não foi possível criar um atalho para %1 - + Create Icon Criar Ícone - + Cannot create icon file. Path "%1" does not exist and cannot be created. Não foi possível criar o arquivo de ícone. O caminho "%1" não existe e não pode ser criado. - + Dumping... Extraindo... - - + + Cancel Cancelar - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Não foi possível extrair o RomFS base. Consulte o registro para ver os detalhes. - + Error Opening %1 Erro ao abrir %1 - + Select Directory Selecionar pasta - + Properties Propriedades - + The application properties could not be loaded. Não foi possível carregar as propriedades do aplicativo. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Executável do 3DS (%1);;Todos os arquivos (*.*) - + Load File Carregar arquivo - - + + Set Up System Files Configurar arquivos do sistema - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>O Azahar precisa dos dados exclusivos do console e arquivos de firmware de um console real para que seja possível usar alguns de seus recursos.<br>Tais arquivos e dados podem ser configurados através da <a href=https://github.com/azahar-emu/ArticSetupTool>Ferramenta de Configuração Artic do Azahar</a><br>Notas:<ul><li><b>Esta operação instalará os dados exclusivos do console para o Azahar, não compartilhe sua pasta de usuário ou nand<br>depois de realizar este processo!</b></li><li>Enquanto estiver realizando o processo de configuração, o Azahar irá vincular-se ao console executando a ferramenta de configuração. Você poderá desvincular<br>o console mais tarde na aba Sistema no menu de configuração do emulador.</li><li>Não entre no online com ambos Azahar e seu console 3DS ao mesmo tempo depois de configurar os arquivos de sistema,<br>já que isso poderá causar problemas.</li><li>A configuração do Antigo 3DS é necessária para a configuração do Novo 3DS funcionar (realizar ambas as configurações é recomendado).</li><li>Ambos os modos de configuração irão funcionar independente do modelo do console executando a ferramenta de configuração.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Digite o endereço da Ferramenta de Configuração Artic do Azahar: - + <br>Choose setup mode: <br>Escolha o modo de configuração: - + (ℹ️) Old 3DS setup (ℹ️) Configuração do Antigo 3DS - - + + Setup is possible. É possível configurar. - + (⚠) New 3DS setup (⚠) Configuração do Novo 3DS - + Old 3DS setup is required first. A configuração do Antigo 3DS é requisitada primeiro. - + (✅) Old 3DS setup (✅) Configuração do Antigo 3DS - - + + Setup completed. Configuração concluída. - + (ℹ️) New 3DS setup (ℹ️) Configuração do Novo 3DS - + (✅) New 3DS setup (✅) Configuração do Novo 3DS - + The system files for the selected mode are already set up. Reinstall the files anyway? Os arquivos do sistema para o modo selecionado já foram configurados. Reinstalar os arquivos mesmo assim? - + Load Files Carregar arquivos - + 3DS Installation File (*.cia *.zcia) Arquivo de Instalação 3DS (.cia .zcia) - - - + + + All Files (*.*) Todos os arquivos (*.*) - + Connect to Artic Base Conectar-se ao Artic Base - + Enter Artic Base server address: Digite o endereço do servidor Artic Base: - + %1 has been installed successfully. %1 foi instalado. - + Unable to open File Não foi possível abrir o arquivo - + Could not open %1 Não foi possível abrir %1 - + Installation aborted Instalação cancelada - + The installation of %1 was aborted. Please see the log for more details A instalação de %1 foi interrompida. Consulte o registro para mais detalhes - + Invalid File Arquivo inválido - + %1 is not a valid CIA %1 não é um CIA válido - + CIA Encrypted CIA criptografado - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Seu arquivo CIA está criptografado.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Confira nosso blog para mais informações.</a> - + Unable to find File Não foi possível localizar o arquivo - + Could not find %1 Não foi possível localizar %1 - - - - + + + + Z3DS Compression Compressão Z3DS - + Failed to compress some files, check log for details. Falha ao comprimir alguns arquivos. Verifique o log para mais detalhes. - + Failed to decompress some files, check log for details. Falha ao descomprimir alguns arquivos. Verifique o log para mais detalhes. - + All files have been compressed successfully. Todos os arquivos foram comprimidos com sucesso. - + All files have been decompressed successfully. Todos os arquivos foram descomprimidos com sucesso. - + Uninstalling '%1'... Desinstalando '%1'... - + Failed to uninstall '%1'. Erro ao desinstalar '%1'. - + Successfully uninstalled '%1'. '%1' desinstalado com sucesso. - + File not found Arquivo não encontrado - + File "%1" not found Arquivo "%1" não encontrado - + Savestates Estados salvos - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4617,86 +4627,86 @@ Use at your own risk! Use por sua conta e risco! - - - + + + Error opening amiibo data file Erro ao abrir arquivo de dados do amiibo - + A tag is already in use. Uma tag já está em uso. - + Application is not looking for amiibos. O aplicativo não está procurando por amiibos. - + Amiibo File (%1);; All Files (*.*) Arquivo do Amiibo (%1);; Todos os arquivos (*.*) - + Load Amiibo Carregar Amiibo - + Unable to open amiibo file "%1" for reading. Não foi possível abrir o arquivo amiibo "%1" para realizar a leitura. - + Record Movie Gravar passos - + Movie recording cancelled. Gravação cancelada. - - + + Movie Saved Gravação salva - - + + The movie is successfully saved. A gravação foi salva. - + Application will unpause O aplicativo será retomado - + The application will be unpaused, and the next frame will be captured. Is this okay? O aplicativo será retomado, e o próximo quadro será capturado. Tudo bem? - + Invalid Screenshot Directory Pasta inválida - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Não é possível criar a pasta especificada. O caminho original foi restabelecido. - + Could not load video dumper Não foi possível carregar o gravador de vídeo - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4709,265 +4719,265 @@ Para instalar o FFmpeg no Azahar, pressione Abrir e selecione seu diretório FFm Para ver um guia sobre como instalar o FFmpeg, pressione Ajuda. - + Load 3DS ROM Files Carregar arquivos de ROM do 3DS - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + **Arquivos de ROM do 3DS (.cia *.cci *.3dsx .cxi .3ds) - + 3DS Compressed ROM File (*.%1) Arquivo de ROM 3DS Comprimido (*.%1) - + Save 3DS Compressed ROM File Salvar Arquivo de ROM 3DS Comprimido - + Select Output 3DS Compressed ROM Folder Selecionar Pasta de Saída das ROMs 3DS Comprimidas - + Load 3DS Compressed ROM Files Carregar Arquivos de ROM 3DS Comprimidos - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) Arquivos de ROM 3DS Comprimidos (*.zcia *.zcci .z3dsx .zcxi) - + 3DS ROM File (*.%1) Arquivo de ROM 3DS (*.%1) - + Save 3DS ROM File Salvar Arquivo de ROM 3DS - + Select Output 3DS ROM Folder Selecionar Pasta de Saída das ROMs 3DS - + Select FFmpeg Directory Selecione o Diretório FFmpeg - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. O diretório FFmpeg fornecido não foi encontrado %1. Por favor, certifique-se de que o diretório correto foi selecionado. - + FFmpeg has been sucessfully installed. O FFmpeg foi instalado com sucesso. - + Installation of FFmpeg failed. Check the log file for details. A instalação do FFmpeg falhou. Verifique o arquivo de log para obter detalhes. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. Não foi possível começar a gravação do vídeo.<br>Assegure-se que o codificador de vídeo está configurado corretamente.<br>Refira-se ao log para mais detalhes. - + Recording %1 Gravando %1 - + Playing %1 / %2 Reproduzindo %1 / %2 - + Movie Finished Reprodução concluída - + (Accessing SharedExtData) (Acessando SharedExtData) - + (Accessing SystemSaveData) (Acessando SystemSaveData) - + (Accessing BossExtData) (Accessando BossExtData) - + (Accessing ExtData) (Acessando ExtData) - + (Accessing SaveData) (Acessando SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Tráfego do Artic: %1 %2%3 - + Speed: %1% Velocidade: %1% - + Speed: %1% / %2% Velocidade: %1% / %2% - + App: %1 FPS App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Quadro: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rest: %6 ms) - + Frame: %1 ms Quadro: %1 ms - + VOLUME: MUTE VOLUME: SILENCIADO - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUME: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. %1 está ausente. Por favor,<a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>faça a extração dos arquivos do sistema</a>. <br/>Continuar a emulação pode causar falhas e bugs. - + A system archive Um arquivo do sistema - + System Archive Not Found Arquivo de sistema não encontrado - + System Archive Missing Arquivo de sistema em falta - + Save/load Error Erro ao salvar/carregar - + Fatal Error Erro fatal - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Ocorreu um erro fatal. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Verifique o log</a> para mais detalhes.<br/>Continuar a emulação pode causar falhas e bugs. - + Fatal Error encountered Erro fatal encontrado - + Continue Continuar - + Quit Application Sair do Aplicativo - + OK OK - + Would you like to exit now? Deseja sair agora? - + The application is still running. Would you like to stop emulation? O aplicativo ainda está em execução. Deseja parar a emulação? - + Playback Completed Reprodução concluída - + Movie playback completed. Reprodução dos passos concluída. - + Update Available Atualização disponível - + Update %1 for Azahar is available. Would you like to download it? A atualização %1 para o Azahar está disponível. Você gostaria de baixá-la? - + Primary Window Janela Principal - + Secondary Window Janela Secundária @@ -5030,42 +5040,42 @@ Você gostaria de baixá-la? GRenderWindow - + OpenGL not available! OpenGL indisponível! - + OpenGL shared contexts are not supported. Shared contexts do OpenGL não são suportados. - + Error while initializing OpenGL! Erro ao inicializar o OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Sua GPU pode não suportar OpenGL, ou você não possui o driver gráfico mais recente. - + Error while initializing OpenGL 4.3! Erro ao inicializar OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Sua GPU pode não ser compatível com o OpenGL 4.3 ou você não possui o driver mais recente.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! Erro ao inicializar o OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Sua GPU pode não suportar o OpenGL ES 3.2 ou você não possui o driver gráfico mais recente.<br><br>Renderizador GL:<br>%1 @@ -5073,180 +5083,185 @@ Você gostaria de baixá-la? GameList - - + + Compatibility Compatibilidade - - + + Region Região - - + + File type Tipo de arquivo - - + + Size Tamanho - - + + Play time Tempo de jogo - + Favorite Favorito - + Eject Cartridge Ejetar Cartucho - + Insert Cartridge Inserir Cartucho - + Open Abrir - + Application Location Local do Aplicativo - + Save Data Location Local de Dados Salvos - + Extra Data Location Local de Dados Extras - + Update Data Location Atualizar Local dos Dados - + DLC Data Location Local de Dados de DLC  - + Texture Dump Location Local de Dump de Texturas - + Custom Texture Location Local de Texturas Personalizadas - + Mods Location Diretório de Mods - + Dump RomFS Extrair RomFS - + Disk Shader Cache Cache de shaders em disco - + Open Shader Cache Location Abrir local do cache dos shaders - + Delete OpenGL Shader Cache Apagar cache de shaders do OpenGL - + + Delete Vulkan Shader Cache + + + + Uninstall Desinstalar - + Everything Tudo - + Application Aplicativo - + Update Atualização - + DLC DLC - + Remove Play Time Data Remover Dados de Tempo de Jogo - + Create Shortcut Criar Atalho - + Add to Desktop Adicionar à Área de Trabalho - + Add to Applications Menu Adicionar ao Menu Iniciar - + Stress Test: App Launch Teste de Estresse: Inicialização de Aplicativos - + Properties Propriedades - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5255,64 +5270,64 @@ This will delete the application if installed, as well as any installed updates Isso irá deletar o aplicativo caso ele esteja instalado, assim como qualquer atualização ou DLC instalada. - - + + %1 (Update) %1 (Atualização) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Tem certeza que quer desinstalar '%1'? - + Are you sure you want to uninstall the update for '%1'? Tem certeza que deseja desinstalar a atualização para '%1'? - + Are you sure you want to uninstall all DLC for '%1'? Tem certeza de que deseja desinstalar todas as DLCs de '%1'? - + Scan Subfolders Examinar Subpastas - + Remove Application Directory Remover Diretório de Aplicativos - + Move Up Mover para cima - + Move Down Mover para baixo - + Open Directory Location Abrir local da pasta - + Clear Limpar - + Name Nome @@ -5403,7 +5418,7 @@ Tela Inicial. GameListPlaceholder - + Double-click to add a new folder to the application list Clique duas vezes para adicionar uma pasta à lista de aplicativos @@ -5411,27 +5426,27 @@ Tela Inicial. GameListSearchField - + of de - + result resultado - + results resultados - + Filter: Filtro: - + Enter pattern to filter Insira o padrão para filtrar @@ -6064,24 +6079,24 @@ Mensagem de depuração: Preparando shaders %1 / %2 - - Loading Shaders %1 / %2 - Carregando shaders %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Iniciando... - + Now Loading %1 Carregando %1 - + Estimated Time %1 Tempo estimado %1 diff --git a/dist/languages/ro_RO.ts b/dist/languages/ro_RO.ts index 21c83a65e..ab550862c 100644 --- a/dist/languages/ro_RO.ts +++ b/dist/languages/ro_RO.ts @@ -322,8 +322,8 @@ Astfel vor fi banați din forum numele lor de utilizator și adresa IP. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Acest efect de post-procesare ajustează viteza audio pentru a se potrivi la viteza emulatorului și ajută la împiedicarea bâlbâielilor audio. Acest lucru, totuși, mărește latența audio. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ Astfel vor fi banați din forum numele lor de utilizator și adresa IP. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ Astfel vor fi banați din forum numele lor de utilizator și adresa IP. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Selectați de unde provine imaginea camerei emulate. Poate fi o imagine sau o cameră reală. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1488,8 +1488,8 @@ Doriți să ignorați eroarea și să continuați? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync previne screen tearing, dar unele plăci grafice au performanțe mai scăzute cu VSync activat. Păstrați-l activat dacă nu observați o diferență de performanță. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1497,22 +1497,32 @@ Doriți să ignorați eroarea și să continuați? Permite VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global Folosește global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2515,7 +2525,7 @@ Doriți să ignorați eroarea și să continuați? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4113,19 +4123,19 @@ Vă rugăm să verificați instalarea FFmpeg utilizată pentru compilare. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Viteza actuală de emulare. Valori mai mari sau mai mici de 100% indică cum emularea rulează mai repede sau mai încet decât un 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Timp luat pentru a emula un cadru 3DS, fără a pune în calcul limitarea de cadre sau v-sync. Pentru emulare la viteza maximă, această valoare ar trebui să fie maxim 16.67 ms. @@ -4199,7 +4209,7 @@ Vă rugăm să verificați instalarea FFmpeg utilizată pentru compilare. - + Artic Server @@ -4276,7 +4286,7 @@ Vă rugăm să verificați instalarea FFmpeg utilizată pentru compilare. - + Folder does not exist! Folderul nu există! @@ -4291,402 +4301,402 @@ Vă rugăm să verificați instalarea FFmpeg utilizată pentru compilare.Resetați play time? - - - - + + + + Create Shortcut Creează un Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 Shortcut-ul către %1 a fost creat cu succes - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Asta va crea un shortcut la AppImage-ul curent. Este posibilitate că nu va lucra normal dacă veți actualiza. Continuă? - + Failed to create a shortcut to %1 Nu s-a putut crea o comandă rapidă către %1 - + Create Icon Creează un Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. Nu se poate crea fișierul de icon. Calea "%1" nu există și nu poate fi creat. - + Dumping... Dumping... - - + + Cancel Anulare - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Nu s-a putut să facă dump-ul bazei RomFS. Consultați log-urile pentru detalii. - + Error Opening %1 Eroare Deschizând %1 - + Select Directory Selectează Directorul - + Properties Proprietăți - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Executabilă 3DS (%1);;Toate Fișierele (*.*) - + Load File Încarcă Fișier - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Încarcă Fișiere - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Toate Fișierele (*.*) - + Connect to Artic Base Conectează Arctic Base - + Enter Artic Base server address: Introduceți adresa serverului Arctic Base: - + %1 has been installed successfully. %1 a fost instalat cu succes. - + Unable to open File Nu s-a putut deschide Fișierul - + Could not open %1 Nu s-a putut deschide %1 - + Installation aborted Instalare anulată - + The installation of %1 was aborted. Please see the log for more details Instalarea lui %1 a fost anulată. Vă rugăm să vedeți log-ul pentru mai multe detalii. - + Invalid File Fișier Invalid - + %1 is not a valid CIA %1 nu este un CIA valid - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File Nu se poate găsi Fișierul - + Could not find %1 %1 n-a fost găsit - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... Dezinstalarea '%1'... - + Failed to uninstall '%1'. Dezinstalarea '%1' a eșuat. - + Successfully uninstalled '%1'. '%1' era dezinstalat cu succes. - + File not found Fișier negăsit - + File "%1" not found Fișierul "%1" nu a fost găsit - + Savestates Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file Eroare la deschiderea fișierului de date amiibo - + A tag is already in use. Un tag deja este in folosire. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Fișier Amiibo (%1);; Toate Fișierele (*.*) - + Load Amiibo Încarcă Amiibo - + Unable to open amiibo file "%1" for reading. Nu se poate deschide fișierul amiibo "%1" pentru citire. - + Record Movie Înregistrează Film - + Movie recording cancelled. Înregistrarea filmului a fost anulată. - - + + Movie Saved Film Salvat - - + + The movie is successfully saved. Filmul a fost salvat cu succes. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory Directoria Capturii de Ecran este Invalidă - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Nu se poate crea directoria specificată a capturii de ecran. Calea capturii de ecran a fost setat implicit - + Could not load video dumper Dumperul video nu a putut fi încărcat - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4695,264 +4705,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory Selectați Directoria FFmpeg - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. Directoria FFmpeg furnizată nu este prezentă %1. Vă rugăm să vă asigurați că ați selectat directoria corectă. - + FFmpeg has been sucessfully installed. FFmpeg era instalat cu succes. - + Installation of FFmpeg failed. Check the log file for details. Instalația FFmpeg a eșuat. Verificați log-urile pentru detalii. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 Înregistrarea %1 - + Playing %1 / %2 Playing %1 / %2 - + Movie Finished Filmul finisat - + (Accessing SharedExtData) (Se accesează SharedExtData) - + (Accessing SystemSaveData) (Se accesează SystemSaveData) - + (Accessing BossExtData) (Se accesează BossExtData) - + (Accessing ExtData) (Se accesează ExtData) - + (Accessing SaveData) (Se accesează SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Viteză: %1% - + Speed: %1% / %2% Viteză: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Cadru: %1 ms - + VOLUME: MUTE VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) VOLUME: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Arhivul sistemului - + System Archive Not Found Fișier de Sistem Negăsit - + System Archive Missing Arhivul Sistemului nu este Prezent - + Save/load Error Eroare la salvare/încărcare - + Fatal Error Eroare Fatală - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered S-a Produs o Eroare Fatală - + Continue Continuă - + Quit Application - + OK OK - + Would you like to exit now? Doriți să ieșiți acum? - + The application is still running. Would you like to stop emulation? - + Playback Completed Redare Finalizată - + Movie playback completed. Redarea filmului a fost finalizată. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window Fereastră Primară - + Secondary Window Fereastră Secundară @@ -5015,42 +5025,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! OpenGL nu este disponibil! - + OpenGL shared contexts are not supported. OpenGL shared contexts nu sunt suportate. - + Error while initializing OpenGL! S-a produs o eroare inițializând OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Este posibil ca GPU-ul dvs. să nu accepte OpenGL, este posibil să nu aveți cel mai recent driver grafic - + Error while initializing OpenGL 4.3! Eroare la inițializarea OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Este posibil ca GPU-ul dvs. să nu accepte OpenGL 4.3, sau dvs. nu aveți cel mai recent driver grafic. <br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! Eroare la inițializare OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Este posibil ca GPU-ul dvs. să nu accepte OpenGL ES 3.2, sau dvs. nu aveți cel mai recent driver grafic. <br><br>GL Renderer:<br>%1 @@ -5058,244 +5068,249 @@ Would you like to download it? GameList - - + + Compatibility Compatibilitate - - + + Region Regiune - - + + File type Tip de Fișier - - + + Size Mărime - - + + Play time Timp de joacă - + Favorite Favorit - + Eject Cartridge - + Insert Cartridge - + Open Deschide - + Application Location Locația Aplicației - + Save Data Location Locația Datelor Salvării - + Extra Data Location Locația Datelor Extra - + Update Data Location Locația Datelor de Actualizare - + DLC Data Location Locația Datelor DLC - + Texture Dump Location Locația Dump-ului de Texturi - + Custom Texture Location Locația Custom a Texturilor - + Mods Location Locația Modurilor - + Dump RomFS Dump RomFS - + Disk Shader Cache Disk Shader Cache - + Open Shader Cache Location Locația Cache-ului de Open Shader - + Delete OpenGL Shader Cache Șterge OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall Dezinstalează - + Everything Totul - + Application - + Update Actualizare - + DLC DLC - + Remove Play Time Data Șterge Datele Timpului de Joacă - + Create Shortcut Creează un Shortcut - + Add to Desktop Adaugă pe desktop - + Add to Applications Menu Adaugă în Meniul de Aplicații - + Stress Test: App Launch - + Properties Proprietăți - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) %1 (Actualizare) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Sunteți siguri că doriți să dezinstalați '%1'? - + Are you sure you want to uninstall the update for '%1'? Sunteți siguri că doriți să dezinstalați actualizarea pentru '%1'? - + Are you sure you want to uninstall all DLC for '%1'? Sunteți siguri că doriți să dezinstalați toate DLC-urile pentru '%1'? - + Scan Subfolders Scanează Subfolderele - + Remove Application Directory - + Move Up Mută în Sus - + Move Down Mută în Jos - + Open Directory Location Deschide Locația Directorului - + Clear Șterge - + Name Nume @@ -5381,7 +5396,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5389,27 +5404,27 @@ Screen. GameListSearchField - + of de - + result rezultat - + results rezultate - + Filter: Filtru: - + Enter pattern to filter Introduceți un tipar de filtrare @@ -6041,24 +6056,24 @@ Debug Message: Se Pregătesc Texturele %1 / %2 - - Loading Shaders %1 / %2 - Se încarc Texturele %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Se Lansează... - + Now Loading %1 Se Încarcă %1 - + Estimated Time %1 Timpul Estimat %1 diff --git a/dist/languages/ru_RU.ts b/dist/languages/ru_RU.ts index c92bbf62d..d1ebe1e48 100644 --- a/dist/languages/ru_RU.ts +++ b/dist/languages/ru_RU.ts @@ -328,8 +328,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Этот эффект постобработки предотвращает появление прерывистости звука и корректирует скорость воспроизведения так, чтобы она совпадала со скоростью эмуляции. Однако всё это увеличивает звуковую задержку. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Выравнивает скорость воспроизведения звука с учётом пропуска кадров в эмуляции. Это означает, что звук будет воспроизводиться с полной скоростью даже при низкой частосте кадров в приложении. Может вызывать рассинхронизацию звука. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Выберите источник отображения для эмулируемой камеры. Это может быть как картинка, так и настоящая камера. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1494,8 +1494,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync предотвращает появление разрывов экрана, но у некоторых графических карт включение опции VSync ухудшает производительность. Если не будет заметного ухудшения производительности, можно оставить её включённой. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1503,22 +1503,32 @@ Would you like to ignore the error and continue? Включить VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global Использовать глобально - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Задерживает поток отрисовки эмулирумого приложения на указанное количество миллисекунд при каждой отправке им команд отрисовки на графический процессор.</p><p>Регулируйте эту настройку в (очень немногих) приложениях с динамической частотой кадров, чтобы исправить проблемы с производительностью.</p></body></html> @@ -2521,7 +2531,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4120,19 +4130,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Текущая скорость эмуляции. Значения выше или ниже 100% указывают на то, что эмуляция работает быстрее или медленнее, чем в 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Время, затрачиваемое на эмуляцию кадра 3DS, без учёта ограничения кадров или вертикальной синхронизации. Для полноскоростной эмуляции это значение должно быть не более 16,67 мс. @@ -4206,7 +4216,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4283,7 +4293,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Папка не существует! @@ -4298,402 +4308,402 @@ Please check your FFmpeg installation used for compilation. Сбросить игровое время? - - - - + + + + Create Shortcut Создать ярлык - + Do you want to launch the application in fullscreen? Запускать приложение в полном экране? - + Successfully created a shortcut to %1 Успешно создан ярлык для %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 Не удалось создать ярлык для %1 - + Create Icon Создать иконку - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Создание дампа... - - + + Cancel Отмена - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Не удалось создать дамп base RomFS. Подробную информацию см. в журнале. - + Error Opening %1 Ошибка при открытии %1 - + Select Directory Выбрать каталог - + Properties Свойства - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Исполняемый файл 3DS (%1);;Все файлы (*.*) - + Load File Загрузка файла - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: Выберите режим установки: - + (ℹ️) Old 3DS setup (ℹ️) Обычная 3DS - - + + Setup is possible. - + (⚠) New 3DS setup (⚠) New 3DS - + Old 3DS setup is required first. Сначала требуется установка обычной 3DS. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Загрузка файлов - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Все файлы (*.*) - + Connect to Artic Base Подключиться к Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 был успешно установлен. - + Unable to open File Не удалось открыть файл - + Could not open %1 Не удалось открыть %1 - + Installation aborted Установка прервана - + The installation of %1 was aborted. Please see the log for more details Установка %1 была прервана. Более подробную информацию см. в журнале. - + Invalid File Недопустимый файл - + %1 is not a valid CIA %1 — недопустимый CIA-файл - + CIA Encrypted CIA файл зашифрован - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File Не удалось найти файл - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... Удаление '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. Успешно удалён '%1'. - + File not found Файл не найден - + File "%1" not found Файл «%1» не найден - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Файл Amiibo (%1);; Все файлы (*.*) - + Load Amiibo Загрузка Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Запись видеоролика - + Movie recording cancelled. Запись видеоролика отменена. - - + + Movie Saved Сохранение видеоролика - - + + The movie is successfully saved. Видеоролик сохранён успешно. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4702,264 +4712,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory Выберите каталог FFmpeg - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Скорость: %1% - + Speed: %1% / %2% Скорость: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Кадр: %1 мс - + VOLUME: MUTE ГРОМКОСТЬ: ЗАГЛУШЕНО - + VOLUME: %1% Volume percentage (e.g. 50%) ГРОМКОСТЬ: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Системный архив - + System Archive Not Found Системный архив не найден - + System Archive Missing Не удалось найти системный архив - + Save/load Error Ошибка сохранения/загрузки - + Fatal Error Неустранимая ошибка - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Произошла неустранимая ошибка - + Continue Продолжить - + Quit Application Закрыть Приложение - + OK OK - + Would you like to exit now? Выйти сейчас? - + The application is still running. Would you like to stop emulation? - + Playback Completed Воспроизведение завершено - + Movie playback completed. Воспроизведение видеоролика завершено. - + Update Available Доступно обновление - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window Основной Экран - + Secondary Window Дополнительный Экран @@ -5022,42 +5032,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! OpenGL недоступен! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! Ошибка при инициализации OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! Ошибка при инициализации OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5065,244 +5075,249 @@ Would you like to download it? GameList - - + + Compatibility Совместимость - - + + Region Регион - - + + File type Тип файла - - + + Size Размер - - + + Play time Игровое время - + Favorite Избранное - + Eject Cartridge - + Insert Cartridge - + Open Открыть - + Application Location Путь к приложению - + Save Data Location Путь к файлам сохранений - + Extra Data Location - + Update Data Location Путь к файлам обновлений - + DLC Data Location Путь к файлам DLC - + Texture Dump Location Путь к файлам дампа текстур - + Custom Texture Location Путь к пользовательским текстурам - + Mods Location Путь к модификациям - + Dump RomFS Создать дамп RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall Удалить - + Everything Всё - + Application Приложение - + Update Обновления - + DLC - + Remove Play Time Data - + Create Shortcut Создать ярлык - + Add to Desktop Добавить на рабочий стол - + Add to Applications Menu - + Stress Test: App Launch - + Properties Свойства - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Сканировать подпапки - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Открыть расположение каталога - + Clear Очистить - + Name Имя @@ -5388,7 +5403,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5396,27 +5411,27 @@ Screen. GameListSearchField - + of из - + result результат - + results результатов - + Filter: Фильтр: - + Enter pattern to filter Введите шаблон для фильтрации @@ -6048,24 +6063,24 @@ Debug Message: Подготовка шейдеров %1 / %2 - - Loading Shaders %1 / %2 - Загрузка шейдеров %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Запуск... - + Now Loading %1 Выполняется загрузка %1 - + Estimated Time %1 Оставшееся время %1 diff --git a/dist/languages/sv.ts b/dist/languages/sv.ts index 17dd7f579..e1f412b0f 100644 --- a/dist/languages/sv.ts +++ b/dist/languages/sv.ts @@ -328,8 +328,8 @@ Detta skulle bannlysa både deras forumanvändarnamn och deras IP-adress. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Denna efterbehandlingseffekt justerar ljudhastigheten för att matcha emuleringshastigheten och hjälper till att förhindra ljudstörningar. Detta kan dock öka ljudlatensen. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + <html><head/><body><p>Denna efterbearbetningseffekt justerar ljudhastigheten så att den matchar emuleringshastigheten och hjälper till att förhindra ljudhack. Detta ökar dock ljudfördröjningen.</p></body></html> @@ -338,8 +338,8 @@ Detta skulle bannlysa både deras forumanvändarnamn och deras IP-adress. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Skalar uppspelningshastigheten för ljud för att kompensera för minskad bildfrekvens i emuleringen. Detta innebär att ljudet spelas upp med full hastighet även om programmets bildfrekvens är låg. Kan orsaka problem med felsynkronisering av ljud. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + <html><head/><body><p>Anpassar ljuduppspelningshastigheten för att kompensera för minskad bildfrekvens i emuleringen. Detta innebär att ljudet spelas upp i full hastighet även när applikationens bildfrekvens är låg. Kan orsaka problem med ljudsynkronisering.</p></body></html> @@ -478,8 +478,8 @@ Detta skulle bannlysa både deras forumanvändarnamn och deras IP-adress. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Välj varifrån bilden av den emulerade kameran kommer. Det kan vara en bild eller en riktig kamera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + <html><head/><body><p>Välj var bilden från den emulerade kameran kommer ifrån. Det kan vara en bild eller en riktig kamera.</p></body></html> @@ -1494,8 +1494,8 @@ Vill du ignorera felet och fortsätta? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync förhindrar grafikproblem, men vissa grafikkort har lägre prestanda med VSync aktiverat. Låt det vara aktiverat om du inte märker någon prestandaskillnad. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + <html><head/><body><p>VSync förhindrar att skärmen flimrar, men vissa grafikkort har sämre prestanda när VSync är aktiverat. Låt det vara aktiverat om du inte märker någon skillnad i prestanda.</p></body></html> @@ -1503,22 +1503,32 @@ Vill du ignorera felet och fortsätta? Aktivera VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + <html><head/><body><p>När denna inställning är aktiverad upptäcker den när skärmens uppdateringsfrekvens är lägre än för 3DS, och när så är fallet inaktiveras VSync automatiskt för att undvika att emuleringshastigheten tvingas ned under 100%.</p></body></html> + + + + Enable display refresh rate detection + Aktivera detektering av skärmens uppdateringsfrekvens + + + Use global Använd globalt - + Use per-application Använd per-applikation - + Delay Application Render Thread Fördröj applikationsrenderingstråd - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>Fördröjer det emulerade programmets renderingstråd med det angivna antalet millisekunder varje gång den skickar renderingskommandon till GPU:n.</p><p>Justera den här funktionen i de (mycket få) dynamiska bildfrekvensapplikationerna för att åtgärda prestandaproblem.</p></body></html> @@ -2521,8 +2531,8 @@ Vill du ignorera felet och fortsätta? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - Komprimerar innehållet för CIA-filer vid installation till det emulerade SD-kortet. Påverkar endast CIA-innehåll som installeras när inställningen är aktiverad. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + <html><head/><body><p>Komprimerar innehållet i CIA-filer när det installeras på det emulerade SD-kortet. Påverkar endast CIA-innehåll som installeras medan inställningen är aktiverad.</p></body></html> @@ -4121,19 +4131,19 @@ Kontrollera din FFmpeg-installation som användes för kompilering. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Aktuell emuleringshastighet. Värden som är högre eller lägre än 100% indikerar att emuleringen körs snabbare eller långsammare än 3DS. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. Hur många bilder per sekund som appen visar för närvarande. Detta varierar från app till app och från scen till scen. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Tidsåtgång för att emulera en 3DS-bildruta, utan att räkna med framelimiting eller v-sync. För emulering med full hastighet bör detta vara högst 16,67 ms. @@ -4207,7 +4217,7 @@ Kontrollera din FFmpeg-installation som användes för kompilering. - + Artic Server Artic-server @@ -4284,7 +4294,7 @@ Kontrollera din FFmpeg-installation som användes för kompilering. - + Folder does not exist! Mappen finns inte! @@ -4299,317 +4309,317 @@ Kontrollera din FFmpeg-installation som användes för kompilering.Återställ speltid? - - - - + + + + Create Shortcut Skapa genväg - + Do you want to launch the application in fullscreen? Vill du starta applikationen i helskärm? - + Successfully created a shortcut to %1 Skapade framgångsrikt en genväg till %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? Detta kommer att skapa en genväg till den aktuella AppImage. Detta kanske inte fungerar så bra om du uppdaterar. Fortsätta? - + Failed to create a shortcut to %1 Misslyckades med att skapa en genväg till %1 - + Create Icon Skapa ikon - + Cannot create icon file. Path "%1" does not exist and cannot be created. Det går inte att skapa en ikonfil. Sökvägen "%1" finns inte och kan inte skapas. - + Dumping... Dumpar... - - + + Cancel Avbryt - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Kunde inte dumpa RomFS-basen. Se loggen för mer information. - + Error Opening %1 Fel vid öppning av %1 - + Select Directory Välj katalog - + Properties Egenskaper - + The application properties could not be loaded. Applikationsegenskaperna kunde inte läsas in. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. Körbar 3DS-fil (%1);;Alla filer (*.*) - + Load File Läs in fil - - + + Set Up System Files Konfigurera systemfiler - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar behöver konsolunika data och firmware-filer från en riktig konsol för att kunna använda vissa av dess funktioner. <br>Sådana filer och data kan konfigureras med <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool </a><br>Observera:<ul><li><b> Den här åtgärden installerar konsolunika data till Azahar, dela inte dina användar- eller nand-mappar<br> efter att du har utfört installationsprocessen!</b></li><li> Under installationsprocessen kommer Azahar att länkas till den konsol som kör installationsverktyget. Du kan koppla bort <br>konsolen senare från fliken System i emulatorns konfigurationsmeny. </li><li>Gå inte online med både Azahar och din 3DS-konsol samtidigt efter att du har konfigurerat systemfiler, <br>eftersom det kan orsaka problem.</li><li> En installation av den gamla 3DS:en behövs för att installationen av den nya 3DS:en ska fungera (vi rekommenderar att du gör båda installationslägena).</li><li> Båda installationslägena fungerar oavsett vilken konsolmodell som kör installationsverktyget.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: Ange adressen till Azahar Artic Setup Tool: - + <br>Choose setup mode: <br>Välj konfigurationsläge: - + (ℹ️) Old 3DS setup (ℹ️) Gammal 3DS-konfiguration - - + + Setup is possible. Konfiguration är möjlig. - + (⚠) New 3DS setup (⚠) Ny 3DS-konfiguration - + Old 3DS setup is required first. Gammal 3DS-konfiguration krävs först. - + (✅) Old 3DS setup (✅) Gammal 3DS-konfiguration - - + + Setup completed. Konfigurationen är färdig. - + (ℹ️) New 3DS setup (ℹ️) Ny 3DS-konfiguration - + (✅) New 3DS setup (✅) Ny 3DS-konfiguration - + The system files for the selected mode are already set up. Reinstall the files anyway? Systemfilerna för det valda läget är redan konfigurerade. Installera om filerna i alla fall? - + Load Files Läs in filer - + 3DS Installation File (*.cia *.zcia) 3DS-installationsfil (*.cia *.zcia) - - - + + + All Files (*.*) Alla filer (*.*) - + Connect to Artic Base Anslut till Artic Base - + Enter Artic Base server address: Ange Artic Base-serveradress: - + %1 has been installed successfully. %1 har installerats. - + Unable to open File Kunde inte öppna filen - + Could not open %1 Kunde inte öppna %1 - + Installation aborted Installationen avbröts - + The installation of %1 was aborted. Please see the log for more details Installationen av %1 avbröts. Se loggen för mer information - + Invalid File Ogiltig fil - + %1 is not a valid CIA %1 är inte en giltig CIA - + CIA Encrypted CIA-krypterad - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> Din CIA-fil är krypterad.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Kolla vår blogg för mer info</a> - + Unable to find File Det går inte att hitta filen - + Could not find %1 Kunde inte hitta %1 - - - - + + + + Z3DS Compression Z3DS-komprimering - + Failed to compress some files, check log for details. Misslyckades med att komprimera några filer. Kontrollera loggen. - + Failed to decompress some files, check log for details. Misslyckades med att packa upp några filer. Kontrollera loggen. - + All files have been compressed successfully. Alla filer har komprimerats utan problem. - + All files have been decompressed successfully. Alla filer har packats upp utan problem. - + Uninstalling '%1'... Avinstallation av "%1"... - + Failed to uninstall '%1'. Misslyckades med att avinstallera "%1". - + Successfully uninstalled '%1'. Avinstallationen av "%1" har lyckats. - + File not found Filen hittades inte - + File "%1" not found Filen "%1" hittades inte - + Savestates Sparade tillstånd - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4618,86 +4628,86 @@ Use at your own risk! Använd på egen risk! - - - + + + Error opening amiibo data file Fel vid öppning av amiibo datafil - + A tag is already in use. En tagg är redan i bruk. - + Application is not looking for amiibos. Applikationen letar inte efter amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo-fil (%1);; Alla filer (*.*) - + Load Amiibo Läs in Amiibo - + Unable to open amiibo file "%1" for reading. Det gick inte att öppna amiibo-filen "%1" för läsning. - + Record Movie Spela in film - + Movie recording cancelled. Filminspelning avbruten. - - + + Movie Saved Filmen sparades - - + + The movie is successfully saved. Filmen sparades. - + Application will unpause Applikationen kommer att återupptas - + The application will be unpaused, and the next frame will be captured. Is this okay? Applikationen kommer att återupptas och nästa bildruta kommer att fångas. Är det här okej? - + Invalid Screenshot Directory Ogiltig katalog för skärmbilder - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. Det går inte att skapa angiven skärmbildskatalog. Sökvägen för skärmbilder återställs till sitt standardvärde. - + Could not load video dumper Kunde inte läsa in videodumpern - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4710,265 +4720,265 @@ För att installera FFmpeg till Azahar, tryck på Öppna och välj din FFmpeg-ka Om du vill visa en guide om hur du installerar FFmpeg trycker du på Hjälp. - + Load 3DS ROM Files Läs in 3DS ROM-filer - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS ROM-filer (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) Komprimerad 3DS ROM-fil (*.%1) - + Save 3DS Compressed ROM File Spara komprimerad 3DS ROM-fil - + Select Output 3DS Compressed ROM Folder Välj utdatamapp för 3DS-komprimerad ROM - + Load 3DS Compressed ROM Files Läs in 3DS-komprimerade ROM-filer - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) Komprimerade 3DS ROM-filer (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) 3DS ROM-fil (*.%1) - + Save 3DS ROM File Spara 3DS ROM-fil - + Select Output 3DS ROM Folder Välj utdatamapp för 3DS ROM - + Select FFmpeg Directory Välj FFmpeg-katalog - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. Den angivna FFmpeg-katalogen saknar %1. Kontrollera att rätt katalog har valts. - + FFmpeg has been sucessfully installed. FFmpeg har installerats. - + Installation of FFmpeg failed. Check the log file for details. Installationen av FFmpeg misslyckades. Kontrollera loggfilen för mer information. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. Det gick inte att starta videodumpningen.<br>Kontrollera att videokodaren är korrekt konfigurerad.<br>Se loggen för mer information. - + Recording %1 Spelar in %1 - + Playing %1 / %2 Spelar %1 / %2 - + Movie Finished Filmen är färdig - + (Accessing SharedExtData) (Åtkomst till SharedExtData) - + (Accessing SystemSaveData) (Åtkomst till SystemSaveData) - + (Accessing BossExtData) (Åtkomst till BossExtData) - + (Accessing ExtData) (Åtkomst till ExtData) - + (Accessing SaveData) (Åtkomst till SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Artic-trafik: %1 %2%3 - + Speed: %1% Hastighet: %1% - + Speed: %1% / %2% Hastighet: %1% / %2% - + App: %1 FPS App: %1 bilder/s - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) Bildruta: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Bildruta: %1 ms - + VOLUME: MUTE VOLYM: TYST - + VOLUME: %1% Volume percentage (e.g. 50%) VOLYM: %1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. %1 saknas. <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>Dumpa dina systemarkiv</a>.<br/>Fortsatt emulering kan resultera i krascher och buggar. - + A system archive Ett systemarkiv - + System Archive Not Found Systemarkiv hittades inte - + System Archive Missing Systemarkiv saknas - + Save/load Error Fel vid spara/läs in - + Fatal Error Ödesdigert fel - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. Ett ödesdigert fel inträffade. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Kontrollera loggen</a> för detaljer.<br/>Fortsatt emulering kan resultera i krascher och buggar. - + Fatal Error encountered Allvarligt fel uppstod - + Continue Fortsätt - + Quit Application Avsluta applikation - + OK Ok - + Would you like to exit now? Vill du avsluta nu? - + The application is still running. Would you like to stop emulation? Applikationen körs fortfarande. Vill du stoppa emuleringen? - + Playback Completed Uppspelningen är färdig - + Movie playback completed. Uppspelning av film slutförd. - + Update Available Uppdatering tillgänglig - + Update %1 for Azahar is available. Would you like to download it? Uppdatering %1 för Azahar finns tillgänglig. Vill du hämta ner den? - + Primary Window Primärt fönster - + Secondary Window Sekundärt fönster @@ -5031,42 +5041,42 @@ Vill du hämta ner den? GRenderWindow - + OpenGL not available! OpenGL är inte tillgängligt! - + OpenGL shared contexts are not supported. Delade OpenGL-kontexter stöds inte. - + Error while initializing OpenGL! Fel vid initiering av OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. Din GPU kanske inte stöder OpenGL, eller så har du inte den senaste grafikdrivrutinen. - + Error while initializing OpenGL 4.3! Fel vid initiering av OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Din GPU kanske inte stöder OpenGL 4.3, eller så har du inte den senaste grafikdrivrutinen.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! Fel vid initiering av OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 Din GPU kanske inte stöder OpenGL ES 3.2, eller så har du inte den senaste grafikdrivrutinen.<br><br>GL Renderer:<br>%1 @@ -5074,180 +5084,185 @@ Vill du hämta ner den? GameList - - + + Compatibility Kompatibilitet - - + + Region Region - - + + File type Filtyp - - + + Size Storlek - - + + Play time Speltid - + Favorite Favorit - + Eject Cartridge Mata ut cartridge - + Insert Cartridge Mata in cartridge - + Open Öppna - + Application Location Programplats - + Save Data Location Plats för sparat data - + Extra Data Location Plats för extradata - + Update Data Location Plats för uppdateringsdata - + DLC Data Location Plats för DLC-data - + Texture Dump Location Plats för texturdumpar - + Custom Texture Location Plats för anpassade texturer - + Mods Location Plats för mods - + Dump RomFS Dumpa RomFS - + Disk Shader Cache Disk shadercache - + Open Shader Cache Location Öppna plats för shadercache - + Delete OpenGL Shader Cache Ta bort OpenGL-shadercache - + + Delete Vulkan Shader Cache + + + + Uninstall Avinstallera - + Everything Allting - + Application Applikation - + Update Uppdatering - + DLC DLC - + Remove Play Time Data Ta bort data för speltid - + Create Shortcut Skapa genväg - + Add to Desktop Lägg till på skrivbordet - + Add to Applications Menu Lägg till i programmenyn - + Stress Test: App Launch Stresstest: Appstart - + Properties Egenskaper - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5256,64 +5271,64 @@ This will delete the application if installed, as well as any installed updates Detta kommer att radera programmet om det är installerat, samt alla installerade uppdateringar eller DLC. - - + + %1 (Update) %1 (Uppdatering) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? Är du säker att du vill avinstallera "%1"? - + Are you sure you want to uninstall the update for '%1'? Är du säker på att du vill avinstallera uppdateringen för "%1"? - + Are you sure you want to uninstall all DLC for '%1'? Är du säker på att du vill avinstallera alla DLC för "%1"? - + Scan Subfolders Sök igenom undermappar - + Remove Application Directory Ta bort applikationskatalog - + Move Up Flytta upp - + Move Down Flytta ner - + Open Directory Location Öppna katalogplats - + Clear Töm - + Name Namn @@ -5404,7 +5419,7 @@ startskärmen. GameListPlaceholder - + Double-click to add a new folder to the application list Dubbelklicka för att lägga till en ny mapp i applikationslistan @@ -5412,27 +5427,27 @@ startskärmen. GameListSearchField - + of av - + result resultat - + results resultat - + Filter: Filtrera: - + Enter pattern to filter Ange mönster att filtrera @@ -6065,24 +6080,24 @@ Felsökningsmeddelande: Förbereder shaders %1 / %2 - - Loading Shaders %1 / %2 - Läser in shaders %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Startar... - + Now Loading %1 Läser nu in %1 - + Estimated Time %1 Beräknad tid %1 diff --git a/dist/languages/tr_TR.ts b/dist/languages/tr_TR.ts index cd88e3c7b..5efdc8fd6 100644 --- a/dist/languages/tr_TR.ts +++ b/dist/languages/tr_TR.ts @@ -328,8 +328,8 @@ Bu onun hem forum kullanıcı adını hemde IP adresini yasaklar. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Bu post-processing efekti ses hızını emülasyon hızına eşleşmesi için ayarlar ve ses takılmasını önlemeye yardımcı olur. Ancak bu ses gecikmesini arttırır. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ Bu onun hem forum kullanıcı adını hemde IP adresini yasaklar. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - Emülasyon kare hızındaki düşüşleri hesaba katmak için ses çalma hızını ölçeklendirir. Bu, uygulama kare hızı düşük olsa bile sesin tam hızda çalınacağı anlamına gelir. Ses senkronizasyon sorunlarına neden olabilir. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ Bu onun hem forum kullanıcı adını hemde IP adresini yasaklar. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Emülasyon yapılmış kameranın görüntüsünün nereden geldiğini seçin. Bir resim veya gerçek bir kamera olabilir. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1494,8 +1494,8 @@ Hataya aldırmayıp devam etmek ister misiniz? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync ekran yırtılmasını engeller, fakat bazı görüntü kartları VSync etkinken daha az performans sergileyebilir. Eğer performans değişikliği hissetmiyorsanız açık bırakın. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1503,22 +1503,32 @@ Hataya aldırmayıp devam etmek ister misiniz? VSync Etkin - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2521,7 +2531,7 @@ Hataya aldırmayıp devam etmek ister misiniz? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4117,19 +4127,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Geçerli emülasyon hızı. 100%'den az veya çok olan değerler emülasyonun bir 3DS'den daha yavaş veya daha hızlı çalıştığını gösterir. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Bir 3DS karesini emüle etmekte geçen zaman, karelimitleme ve v-sync hariç. Tam hız emülasyon için bu en çok 16,67 ms. olmalı. @@ -4203,7 +4213,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server Artic Sunucusu @@ -4280,7 +4290,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Klasör mevcut değil! @@ -4295,315 +4305,315 @@ Please check your FFmpeg installation used for compilation. Oynama süresi sıfırlansın mı? - - - - + + + + Create Shortcut Kısayol Oluştur - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon Simge Oluştur - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Dump ediliyor... - - + + Cancel İptal et - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. Temel RomFS dump edilemedi. Detaylar için kütük dosyasına bakınız. - + Error Opening %1 %1 Açılırken Hata Oluştu - + Select Directory Dizin Seç - + Properties Özellikler - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS Çalıştırılabiliri (%1);; Bütün Dosyalar (*.*) - + Load File Dosya Yükle - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. Kurulum tamamlandı. - + (ℹ️) New 3DS setup (ℹ️) Yeni 3DS kurulumu - + (✅) New 3DS setup (✅) Yeni 3DS kurulumu - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Dosyaları Yükle - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Tüm Dosyalar (*.*) - + Connect to Artic Base Artic Base'e Bağla - + Enter Artic Base server address: - + %1 has been installed successfully. %1 başarıyla yüklendi. - + Unable to open File Dosya açılamıyor - + Could not open %1 %1 açılamıyor - + Installation aborted Yükleme iptal edildi - + The installation of %1 was aborted. Please see the log for more details %1'in yüklemesi iptal edildi. Daha fazla detay için lütfen kütüğe bakınız. - + Invalid File Geçersiz Dosya - + %1 is not a valid CIA %1 geçerli bir CIA dosyası değil - + CIA Encrypted CİA Şifreli - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File Dosya bulunamadı - + Could not find %1 %1 bulunamadı - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... '%1' siliniyor... - + Failed to uninstall '%1'. '%1' silinemedi. - + Successfully uninstalled '%1'. '%1' başarıyla silindi. - + File not found Dosya bulunamadı - + File "%1" not found "%1" Dosyası bulunamadı - + Savestates Kayıt Durumları - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4612,86 +4622,86 @@ Use at your own risk! Kullanım riski size aittir! - - - + + + Error opening amiibo data file Amiibo veri dosyasını açarken bir hata oldu - + A tag is already in use. Bir etiket zaten kullanılıyor. - + Application is not looking for amiibos. Uygulama amiibo aramıyor. - + Amiibo File (%1);; All Files (*.*) Amiibo Dosyası (%1);; Tüm Dosyalar (*.*) - + Load Amiibo Amiibo Yükle - + Unable to open amiibo file "%1" for reading. - + Record Movie Klip Kaydet - + Movie recording cancelled. Klip kaydı iptal edildi. - - + + Movie Saved Klip Kaydedildi - - + + The movie is successfully saved. Klip başarıyla kayıt edildi. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory Geçersiz Ekran Görüntüsü Dizini - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4700,264 +4710,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory FFmpeg Dizini Seç - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. FFmpeg başarıyla yüklendi. - + Installation of FFmpeg failed. Check the log file for details. FFmpeg yüklemesi başarısız oldu. Detaylar için log dosyasına bakınız. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 Ekran Kaydediliyor %1 - + Playing %1 / %2 Oynatılıyor %1 / %2 - + Movie Finished Film Bitti - + (Accessing SharedExtData) (SharedExtData'ya Erişiliyor) - + (Accessing SystemSaveData) (SystemSaveData'ya Erişiliyor) - + (Accessing BossExtData) (BossExtData'ya Erişiliyor) - + (Accessing ExtData) (ExtData'ya Erişiliyor) - + (Accessing SaveData) (SaveData'ya Erişiliyor) - + MB/s MB/sn - + KB/s KB/sn - + Artic Traffic: %1 %2%3 - + Speed: %1% Hız: %1% - + Speed: %1% / %2% Hız: %1% / %2% - + App: %1 FPS Uygulama: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Kare: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Bir sistem arşivi - + System Archive Not Found Sistem Arşivi Bulunamadı - + System Archive Missing Sistem Arşivi Eksik - + Save/load Error Kaydetme/yükleme Hatası - + Fatal Error Önemli Hata - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered Kritik hatayla karşılaşıldı - + Continue Devam - + Quit Application Uygulamadan Çık - + OK Tamam - + Would you like to exit now? Çıkmak istediğinize emin misiniz? - + The application is still running. Would you like to stop emulation? Uygulama hala çalışıyor. Emülasyonu durdurmak ister misiniz? - + Playback Completed Oynatma Tamamlandı - + Movie playback completed. Klip oynatması tamamlandı. - + Update Available Güncelleme Mevcut - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window Birincil Pencere - + Secondary Window İkincil Pencere @@ -5020,42 +5030,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! OpenGL başlatılırken bir hata oluştu! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. GPU'nuz OpenGL'i desteklemiyor veya grafik sürücünüz eski olabilir. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5063,244 +5073,249 @@ Would you like to download it? GameList - - + + Compatibility Uyumluluk - - + + Region Bölge - - + + File type Dosya türü - - + + Size Boyut - - + + Play time Oyun süresi - + Favorite Favori - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location Uygulama Konumu - + Save Data Location Kayıt Verileri Konumu - + Extra Data Location Ekstra Veri Konumu - + Update Data Location - + DLC Data Location DLC Veri Konumu - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS RomFS Dump - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall Sil - + Everything Her şey - + Application Uygulama - + Update Güncelle - + DLC DLC - + Remove Play Time Data - + Create Shortcut Kısayol Oluştur - + Add to Desktop Masaüstüne Ekle - + Add to Applications Menu Uygulamalar Menüsüne Ekle - + Stress Test: App Launch - + Properties Özellikler - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) %1 (Güncelleme) - - + + %1 (DLC) %1 (DLC) - + Are you sure you want to uninstall '%1'? '%1'i silmek istediğinizden emin misiniz? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Alt Dizinleri Tara - + Remove Application Directory Uygulama Dizinini Kaldır - + Move Up Yukarı Taşı - + Move Down Aşağı Taşı - + Open Directory Location Dizinin Bulunduğu Yeri Aç - + Clear Temizle - + Name İsim @@ -5386,7 +5401,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list Uygulama listesine yeni bir klasör eklemek için çift tıklayın @@ -5394,27 +5409,27 @@ Screen. GameListSearchField - + of 'nun - + result sonuç - + results sonuçlar - + Filter: Filtre: - + Enter pattern to filter Filtrelenecek düzeni girin @@ -6046,23 +6061,23 @@ Debug Message: Shader'lar Hazırlanıyor %1 / %2 - - Loading Shaders %1 / %2 - Shader'lar Yükleniyor %1 / %2 + + Loading %3 %1 / %2 + - + Launching... Başlatılıyor... - + Now Loading %1 Şimdi Yükleniyor %1 - + Estimated Time %1 Tahmini Süre %1 diff --git a/dist/languages/vi_VN.ts b/dist/languages/vi_VN.ts index 6471271f9..db6f779ef 100644 --- a/dist/languages/vi_VN.ts +++ b/dist/languages/vi_VN.ts @@ -322,8 +322,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - Hiệu ứng hậu xử lý giúp tăng tốc âm giúp phù hợp với tốc độ giả lập và cải thiện âm thanh, giúp hạn chế âm rè. Song điều này có thể tăng độ trễ âm. + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - Chọn nguồn ảnh mà giả lập sẽ nhận từ máy ảnh. Nó có thể là tệp tin hoặc một đầu ra Camera. + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1487,8 +1487,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - VSync giúp giảm thiểu hiện tượng tải chia cắt hình ảnh hiển thị trên màn hình, một số các phần cứng độ họa sẽ chạy hiệu suất thấp khi bật VSync. Bật VSync nếu bạn không thấy ảnh hưởng gì. + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1496,22 +1496,32 @@ Would you like to ignore the error and continue? Bật VSync - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2514,7 +2524,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4110,19 +4120,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. Tốc độ giả lập hiện tại. Giá trị cao hoặc thấp hơn 100% thể hiện giả lập đang chạy nhanh hay chậm hơn một chiếc máy 3DS thực sự. - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. Thời gian để giả lập một khung hình của máy 3DS, không gồm giới hạn khung hay v-sync Một giả lập tốt nhất sẽ tiệm cận 16.67 ms. @@ -4196,7 +4206,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4273,7 +4283,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! Thư mục này không tồn tại! @@ -4288,402 +4298,402 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... Đang trích xuất... - - + + Cancel Hủy bỏ - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. Không thể trích xuất base RomFS. Kiểm tra log để biết thêm chi tiết. - + Error Opening %1 Lỗi khi mở %1 - + Select Directory Chọn thư mục - + Properties - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. - + Load File Mở tệp tin - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files Mở các tệp tin - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) Tất cả tệp tin (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. %1 đã được cài đặt thành công. - + Unable to open File Không thể mở tệp tin - + Could not open %1 Không thể mở %1 - + Installation aborted Việc cài đặt đã bị hoãn - + The installation of %1 was aborted. Please see the log for more details Việc cài đặt %1 đã bị hoãn. Vui lòng xem bản ghi nhật ký để biết thêm chi tiết. - + Invalid File Tệp tin không hợp lệ - + %1 is not a valid CIA %1 không phải là một tệp CIA hợp lệ - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found Không tìm thấy tệp - + File "%1" not found Không tìm thấy tệp tin "%1" - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Tệp Amiibo (%1);; Tất cả tệp (*.*) - + Load Amiibo Tải Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie Quay phim - + Movie recording cancelled. Ghi hình đã bị hủy. - - + + Movie Saved Đã lưu phim. - - + + The movie is successfully saved. Phim đã được lưu lại thành công. - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4692,264 +4702,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% Tốc độ: %1% - + Speed: %1% / %2% Tốc độ: %1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms Khung: %1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive Một tập tin hệ thống - + System Archive Not Found Không tìm thấy tập tin hệ thống - + System Archive Missing Thiếu tập tin hệ thống - + Save/load Error - + Fatal Error Lỗi nghiêm trọng - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered - + Continue Tiếp tục - + Quit Application - + OK OK - + Would you like to exit now? Bạn có muốn thoát ngay bây giờ không? - + The application is still running. Would you like to stop emulation? - + Playback Completed Phát lại hoàn tất - + Movie playback completed. Phát lại phim hoàn tất. - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5012,42 +5022,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5055,244 +5065,249 @@ Would you like to download it? GameList - - + + Compatibility Tính tương thích - - + + Region Khu vực - - + + File type Loại tệp tin - - + + Size Kích thước - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS Trích xuất RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders Quét thư mục con - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location Mở thư mục - + Clear - + Name Tên @@ -5378,7 +5393,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5386,27 +5401,27 @@ Screen. GameListSearchField - + of của - + result kết quả - + results kết quả - + Filter: Bộ lọc: - + Enter pattern to filter Nhập mẫu ký tự để lọc @@ -6038,23 +6053,23 @@ Debug Message: - - Loading Shaders %1 / %2 + + Loading %3 %1 / %2 - + Launching... - + Now Loading %1 - + Estimated Time %1 diff --git a/dist/languages/zh_CN.ts b/dist/languages/zh_CN.ts index 4464e4089..df5b6749c 100644 --- a/dist/languages/zh_CN.ts +++ b/dist/languages/zh_CN.ts @@ -328,8 +328,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - 这种后处理效果可以调整音频速度以匹配模拟速度,并有助于防止音频断断续续。 但是会增加音频延迟。 + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -338,8 +338,8 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. - 调整音频播放速度以适应模拟帧率的下降。这意味着即使应用帧率较低,音频也会全速播放。可能会导致音频不同步问题。 + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> + @@ -478,8 +478,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - 选择虚拟摄像头图像的来源。这可以是一张图片或一个真实的摄像头。 + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1494,8 +1494,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - 垂直同步可防止画面产生撕裂感。但启用垂直同步后,某些设备性能可能会有所降低。如果您没有感到性能差异,请保持启用状态。 + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1503,22 +1503,32 @@ Would you like to ignore the error and continue? 启用垂直同步 - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global 使用全局 - + Use per-application 用于每个应用程序 - + Delay Application Render Thread 延迟应用渲染线程 - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> <html><head/><body><p>每次向 GPU 提交渲染命令时,将模拟的应用渲染线程延迟指定的毫秒数。</p><p>在(极少数)动态帧率应用中调整此功能以解决性能问题。</p></body></html> @@ -2521,8 +2531,8 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. - 安装到模拟 SD 卡时,压缩 CIA 文件的内容。仅影响启用此设置时安装的 CIA 内容。 + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> + @@ -4120,19 +4130,19 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. 当前模拟速度。高于或低于 100% 的值表示模拟正在运行得比实际 3DS 更快或更慢。 - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. 应用当前显示的每秒帧数。这会因应用和场景而异。 - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. 在不计算速度限制和垂直同步的情况下,模拟一个 3DS 帧的实际时间。若要进行全速模拟,这个数值不应超过 16.67 毫秒。 @@ -4206,7 +4216,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server Artic 服务器 @@ -4283,7 +4293,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! 文件夹不存在! @@ -4298,317 +4308,317 @@ Please check your FFmpeg installation used for compilation. 重置游戏时间? - - - - + + + + Create Shortcut 创建快捷方式 - + Do you want to launch the application in fullscreen? 您想以全屏幕运行应用吗? - + Successfully created a shortcut to %1 已经在 %1 上创建了快捷方式。 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? 这将会为当前的 AppImage 创建一个快捷方式。如果您更新,此快捷方式可能会无效。继续吗? - + Failed to create a shortcut to %1 在 %1 上创建快捷方式失败。 - + Create Icon 创建图标 - + Cannot create icon file. Path "%1" does not exist and cannot be created. 无法创建图标文件。路径“%1”不存在,且无法创建。 - + Dumping... 转储中... - - + + Cancel 取消 - - + - - + + - - - + + + + Azahar Azahar - + Could not dump base RomFS. Refer to the log for details. 无法转储 RomFS 。 有关详细信息,请参考日志文件。 - + Error Opening %1 无法打开 %1 - + Select Directory 选择目录 - + Properties 属性 - + The application properties could not be loaded. 无法加载应用属性。 - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS 可执行文件 (%1);;所有文件 (*.*) - + Load File 加载文件 - - + + Set Up System Files 设置系统文件 - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> <p>Azahar 需要来自真实掌机的独有数据和固件文件才能使用其部分功能。<br>此类文件和数据可通过 <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic 设置工具</a>进行设置<br>注意:<ul><li><b>此操作会将掌机独有数据安装到 Azahar,<br>执行设置过程后请勿共享您的用户或 nand 文件夹!</b></li><li>在执行设置过程时,Azahar 将关联到运行设置工具的掌机。<br>您可以随时从模拟器配置菜单的“系统”选项卡中取消关联掌机。</li><li>设置系统文件后,请勿同时使用 Azahar 和 3DS 掌机联网,<br>因为这可能会导致问题。</li><li>新 3DS 设置需要先进行老 3DS 设置才能运作(建议两种设置模式都执行)。</li><li>无论运行设置工具的掌机型号如何,两种设置模式均可运作。</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: 输入 Azahar Artic 设置工具地址: - + <br>Choose setup mode: <br>选择设置模式: - + (ℹ️) Old 3DS setup (ℹ️) 老 3DS 设置 - - + + Setup is possible. 可以进行设置。 - + (⚠) New 3DS setup (⚠) 新 3DS 设置 - + Old 3DS setup is required first. 首先需要设置老 3DS。 - + (✅) Old 3DS setup (✅) 老 3DS 设置 - - + + Setup completed. 设置完成。 - + (ℹ️) New 3DS setup (ℹ️) 新 3DS 设置 - + (✅) New 3DS setup (✅) 新 3DS 设置 - + The system files for the selected mode are already set up. Reinstall the files anyway? 所选模式的系统文件已设置。 是否要重新安装文件? - + Load Files 加载多个文件 - + 3DS Installation File (*.cia *.zcia) 3DS 安装文件 (*.cia *.zcia) - - - + + + All Files (*.*) 所有文件 (*.*) - + Connect to Artic Base 连接到 Artic Base - + Enter Artic Base server address: 输入 Artic Base 服务器地址: - + %1 has been installed successfully. %1 已成功安装。 - + Unable to open File 无法打开文件 - + Could not open %1 无法打开 %1 - + Installation aborted 安装失败 - + The installation of %1 was aborted. Please see the log for more details %1 的安装过程失败。请参阅日志以了解细节。 - + Invalid File 文件无效 - + %1 is not a valid CIA %1 不是有效的 CIA 文件 - + CIA Encrypted CIA 已加密 - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> 您的 CIA 文件已加密。 <br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>请查看我们的博客以了解更多信息。</a> - + Unable to find File 无法找到文件 - + Could not find %1 找不到 %1 - - - - + + + + Z3DS Compression Z3DS 压缩 - + Failed to compress some files, check log for details. 部分文件压缩失败,请查看日志了解详情。 - + Failed to decompress some files, check log for details. 部分文件解压缩失败,请查看日志了解详情。 - + All files have been compressed successfully. 所有文件已成功压缩。 - + All files have been decompressed successfully. 所有文件已成功解压缩。 - + Uninstalling '%1'... 正在卸载“%1”... - + Failed to uninstall '%1'. 卸载“%1”失败。 - + Successfully uninstalled '%1'. “%1”卸载成功。 - + File not found 找不到文件 - + File "%1" not found 找不到文件“%1” - + Savestates 保存状态 - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! @@ -4617,86 +4627,86 @@ Use at your own risk! 您必须自行承担使用风险! - - - + + + Error opening amiibo data file 打开 Amiibo 数据文件时出错 - + A tag is already in use. 当前已有 Amiibo 标签在使用中。 - + Application is not looking for amiibos. 应用未在寻找 Amiibo。 - + Amiibo File (%1);; All Files (*.*) Amiibo 文件 (%1);;所有文件 (*.*) - + Load Amiibo 加载 Amiibo - + Unable to open amiibo file "%1" for reading. 无法打开 Amiibo 文件 %1 。 - + Record Movie 录制影像 - + Movie recording cancelled. 影像录制已取消。 - - + + Movie Saved 影像已保存 - - + + The movie is successfully saved. 影像已成功保存。 - + Application will unpause 应用将取消暂停 - + The application will be unpaused, and the next frame will be captured. Is this okay? 将取消暂停应用,并捕获下一帧。这样可以吗? - + Invalid Screenshot Directory 无效的截图保存目录 - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. 无法创建指定的截图保存目录。截图保存路径将重设为默认值。 - + Could not load video dumper 无法加载视频转储器 - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4709,265 +4719,265 @@ To view a guide on how to install FFmpeg, press Help. 要查看如何安装 FFmpeg 的指南,请按“帮助”。 - + Load 3DS ROM Files 加载 3DS ROM 文件 - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) 3DS 压缩 ROM 文件 (*.%1) - + Save 3DS Compressed ROM File 保存 3DS 压缩 ROM 文件 - + Select Output 3DS Compressed ROM Folder 选择输出 3DS 压缩 ROM 的文件夹 - + Load 3DS Compressed ROM Files 加载 3DS 压缩 ROM 文件 - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) 3DS 压缩 ROM 文件 (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) 3DS ROM 文件 (*.%1) - + Save 3DS ROM File 保存 3DS ROM 文件 - + Select Output 3DS ROM Folder 选择输出 3DS ROM 的文件夹 - + Select FFmpeg Directory 选择 FFmpeg 目录 - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. 选择的 FFmpeg 目录中缺少 %1 。请确保选择了正确的目录。 - + FFmpeg has been sucessfully installed. FFmpeg 已成功安装。 - + Installation of FFmpeg failed. Check the log file for details. 安装 FFmpeg 失败。详情请参阅日志文件。 - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. 无法开始视频转储。<br>请确保视频编码器配置正确。<br>有关详细信息,请参阅日志。 - + Recording %1 录制中 %1 - + Playing %1 / %2 播放中 %1 / %2 - + Movie Finished 录像播放完毕 - + (Accessing SharedExtData) (正在获取 SharedExtData) - + (Accessing SystemSaveData) (正在获取 SystemSaveData) - + (Accessing BossExtData) (正在获取 BossExtData) - + (Accessing ExtData) (正在获取 ExtData) - + (Accessing SaveData) 正在获取(SaveData) - + MB/s MB/s - + KB/s KB/s - + Artic Traffic: %1 %2%3 Artic 流量:%1 %2%3 - + Speed: %1% 速度:%1% - + Speed: %1% / %2% 速度:%1% / %2% - + App: %1 FPS 应用: %1 帧 - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) 帧: %1 毫秒 (GPU: [CMD: %2 毫秒, SWP: %3 毫秒], IPC: %4 毫秒, SVC: %5 毫秒, Rem: %6 毫秒) - + Frame: %1 ms 帧延迟:%1 毫秒 - + VOLUME: MUTE 音量:静音 - + VOLUME: %1% Volume percentage (e.g. 50%) 音量:%1% - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. %1 缺失。请 <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>转储您的系统档案</a>。<br/>继续进行模拟可能会导致崩溃和错误。 - + A system archive 系统档案 - + System Archive Not Found 未找到系统档案 - + System Archive Missing 系统档案丢失 - + Save/load Error 保存/读取出现错误 - + Fatal Error 致命错误 - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. 发生了致命错误。请<a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>参阅日志</a>了解详细信息。<br/>继续进行模拟可能会导致崩溃和错误。 - + Fatal Error encountered 发生致命错误 - + Continue 继续 - + Quit Application 退出应用 - + OK 确定 - + Would you like to exit now? 您现在要退出么? - + The application is still running. Would you like to stop emulation? 应用仍在运行。您想停止模拟吗? - + Playback Completed 播放完成 - + Movie playback completed. 影像播放完成。 - + Update Available 有可用更新 - + Update %1 for Azahar is available. Would you like to download it? Azahar 的更新 %1 已发布。 您要下载吗? - + Primary Window 主窗口 - + Secondary Window 次级窗口 @@ -5030,42 +5040,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! OpenGL 不可用! - + OpenGL shared contexts are not supported. 不支持 OpenGL 共享上下文。 - + Error while initializing OpenGL! 初始化 OpenGL 时出错! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. 您的 GPU 可能不支持 OpenGL,或没有安装最新的显卡驱动程序。 - + Error while initializing OpenGL 4.3! 初始化 OpenGL 4.3 时出错! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 您的 GPU 可能不支持 OpenGL 4.3,或没有安装最新的显卡驱动程序。<br><br>GL 渲染器:<br>%1 - + Error while initializing OpenGL ES 3.2! 初始化 OpenGL ES 3.2 时出错! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 您的 GPU 可能不支持 OpenGL ES 3.2,或没有安装最新的 GPU 驱动程序。<br><br>GL 渲染器:<br>%1 @@ -5073,180 +5083,185 @@ Would you like to download it? GameList - - + + Compatibility 兼容性 - - + + Region 地区 - - + + File type 文件类型 - - + + Size 大小 - - + + Play time 游戏时间 - + Favorite 收藏 - + Eject Cartridge - + Insert Cartridge - + Open 打开 - + Application Location 应用路径 - + Save Data Location 存档数据路径 - + Extra Data Location 额外数据路径 - + Update Data Location 更新数据路径 - + DLC Data Location DLC 数据路径 - + Texture Dump Location 纹理转储路径 - + Custom Texture Location 自定义纹理路径 - + Mods Location Mods 路径 - + Dump RomFS 转储 RomFS - + Disk Shader Cache 磁盘着色器缓存 - + Open Shader Cache Location 打开着色器缓存位置 - + Delete OpenGL Shader Cache 删除 OpenGL 着色器缓存 - + + Delete Vulkan Shader Cache + + + + Uninstall 卸载 - + Everything 所有内容 - + Application 应用 - + Update 更新补丁 - + DLC DLC - + Remove Play Time Data 删除游玩时间 - + Create Shortcut 创建快捷方式 - + Add to Desktop 添加到桌面 - + Add to Applications Menu 添加到应用菜单 - + Stress Test: App Launch 压力测试:应用启动 - + Properties 属性 - - - - + + + + Azahar Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. @@ -5255,64 +5270,64 @@ This will delete the application if installed, as well as any installed updates 这将删除应用、已安装的更新补丁或 DLC。 - - + + %1 (Update) %1(更新补丁) - - + + %1 (DLC) %1(DLC) - + Are you sure you want to uninstall '%1'? 您确定要卸载“%1”吗? - + Are you sure you want to uninstall the update for '%1'? 您确定要卸载“%1”的更新补丁吗? - + Are you sure you want to uninstall all DLC for '%1'? 您确定要卸载“%1”的所有 DLC 吗? - + Scan Subfolders 扫描子文件夹 - + Remove Application Directory 移除应用目录 - + Move Up 向上移动 - + Move Down 向下移动 - + Open Directory Location 打开目录位置 - + Clear 清除 - + Name 名称 @@ -5403,7 +5418,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list 双击将新文件夹添加到应用列表 @@ -5411,27 +5426,27 @@ Screen. GameListSearchField - + of / - + result 结果 - + results 结果 - + Filter: 搜索: - + Enter pattern to filter 搜索游戏 @@ -6064,24 +6079,24 @@ Debug Message: 正在准备着色器... %1 / %2 - - Loading Shaders %1 / %2 - 正在加载着色器... %1 / %2 + + Loading %3 %1 / %2 + - + Launching... 载入中... - + Now Loading %1 正在加载 %1 - + Estimated Time %1 所需时间:%1 diff --git a/dist/languages/zh_TW.ts b/dist/languages/zh_TW.ts index fa4f44cad..8c6f5ad4a 100644 --- a/dist/languages/zh_TW.ts +++ b/dist/languages/zh_TW.ts @@ -322,8 +322,8 @@ This would ban both their forum username and their IP address. - This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency. - 讓音訊速度與遊戲的模擬速度透過後處理效果同步,這有助於防止音訊斷斷續續,但是也增加了音訊延遲。 + <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> + @@ -332,7 +332,7 @@ This would ban both their forum username and their IP address. - Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues. + <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> @@ -472,8 +472,8 @@ This would ban both their forum username and their IP address. - Select where the image of the emulated camera comes from. It may be an image or a real camera. - 選擇相機畫面的來源,可以使用圖片或電腦相機。 + <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> + @@ -1488,8 +1488,8 @@ Would you like to ignore the error and continue? - VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference. - 垂直同步可防止畫面產生撕裂感。但啟用垂直同步後,某些設備性能可能會有所降低。如果您沒有感到性能差異,請保持啟用狀態。 + <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> + @@ -1497,22 +1497,32 @@ Would you like to ignore the error and continue? - + + <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> + + + + + Enable display refresh rate detection + + + + Use global - + Use per-application - + Delay Application Render Thread - + <html><head/><body><p>Delays the emulated application render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic framerate applications to fix performance issues.</p></body></html> @@ -2515,7 +2525,7 @@ Would you like to ignore the error and continue? - Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled. + <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> @@ -4111,20 +4121,20 @@ Please check your FFmpeg installation used for compilation. - + Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a 3DS. 目前模擬速度, 「高於/低於」100% 代表模擬速度比 3DS 實機「更快/更慢」。 - + How many frames per second the app is currently displaying. This will vary from app to app and scene to scene. - + Time taken to emulate a 3DS frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms. 不計算影格限制或垂直同步時, 模擬一個 3DS 影格所花的時間。全速模擬時,這個數值最多應為 16.67 毫秒。 @@ -4199,7 +4209,7 @@ Please check your FFmpeg installation used for compilation. - + Artic Server @@ -4276,7 +4286,7 @@ Please check your FFmpeg installation used for compilation. - + Folder does not exist! 資料夾不存在! @@ -4291,401 +4301,401 @@ Please check your FFmpeg installation used for compilation. - - - - + + + + Create Shortcut - + Do you want to launch the application in fullscreen? - + Successfully created a shortcut to %1 - + This will create a shortcut to the current AppImage. This may not work well if you update. Continue? - + Failed to create a shortcut to %1 - + Create Icon - + Cannot create icon file. Path "%1" does not exist and cannot be created. - + Dumping... - - + + Cancel 取消 - - + - - + + - - - + + + + Azahar - + Could not dump base RomFS. Refer to the log for details. - + Error Opening %1 開啟 %1 時錯誤 - + Select Directory 選擇目錄 - + Properties - + The application properties could not be loaded. - + 3DS Executable (%1);;All Files (*.*) %1 is an identifier for the 3DS executable file extensions. 3DS 可執行檔案 (%1);;所有檔案 (*.*) - + Load File 讀取檔案 - - + + Set Up System Files - + <p>Azahar needs console unique data and firmware files from a real console to be able to use some of its features.<br>Such files and data can be set up with the <a href=https://github.com/azahar-emu/ArticSetupTool>Azahar Artic Setup Tool</a><br>Notes:<ul><li><b>This operation will install console unique data to Azahar, do not share your user or nand folders<br>after performing the setup process!</b></li><li>While doing the setup process, Azahar will link to the console running the setup tool. You can unlink the<br>console later from the System tab in the emulator configuration menu.</li><li>Do not go online with both Azahar and your 3DS console at the same time after setting up system files,<br>as it could cause issues.</li><li>Old 3DS setup is needed for the New 3DS setup to work (doing both setup modes is recommended).</li><li>Both setup modes will work regardless of the model of the console running the setup tool.</li></ul><hr></p> - + Enter Azahar Artic Setup Tool address: - + <br>Choose setup mode: - + (ℹ️) Old 3DS setup - - + + Setup is possible. - + (⚠) New 3DS setup - + Old 3DS setup is required first. - + (✅) Old 3DS setup - - + + Setup completed. - + (ℹ️) New 3DS setup - + (✅) New 3DS setup - + The system files for the selected mode are already set up. Reinstall the files anyway? - + Load Files 讀取多個檔案 - + 3DS Installation File (*.cia *.zcia) - - - + + + All Files (*.*) 所有檔案 (*.*) - + Connect to Artic Base - + Enter Artic Base server address: - + %1 has been installed successfully. 已成功安裝 %1。 - + Unable to open File 無法開啟檔案 - + Could not open %1 無法開啟 %1 - + Installation aborted 安裝中斷 - + The installation of %1 was aborted. Please see the log for more details 安裝 %1 時中斷,請參閱日誌了解細節。 - + Invalid File 無效的檔案 - + %1 is not a valid CIA %1 不是有效的 CIA 檔案 - + CIA Encrypted - + Your CIA file is encrypted.<br/><a href='https://azahar-emu.org/blog/game-loading-changes/'>Please check our blog for more info.</a> - + Unable to find File - + Could not find %1 - - - - + + + + Z3DS Compression - + Failed to compress some files, check log for details. - + Failed to decompress some files, check log for details. - + All files have been compressed successfully. - + All files have been decompressed successfully. - + Uninstalling '%1'... - + Failed to uninstall '%1'. - + Successfully uninstalled '%1'. - + File not found 找不到檔案 - + File "%1" not found 找不到「%1」 - + Savestates - + Warning: Savestates are NOT a replacement for in-application saves, and are not meant to be reliable. Use at your own risk! - - - + + + Error opening amiibo data file - + A tag is already in use. - + Application is not looking for amiibos. - + Amiibo File (%1);; All Files (*.*) Amiibo 檔案 (%1);;所有檔案 (*.*) - + Load Amiibo 讀取 Amiibo - + Unable to open amiibo file "%1" for reading. - + Record Movie 錄影 - + Movie recording cancelled. 錄影已取消。 - - + + Movie Saved 已儲存影片 - - + + The movie is successfully saved. 影片儲存成功。 - + Application will unpause - + The application will be unpaused, and the next frame will be captured. Is this okay? - + Invalid Screenshot Directory - + Cannot create specified screenshot directory. Screenshot path is set back to its default value. - + Could not load video dumper - + FFmpeg could not be loaded. Make sure you have a compatible version installed. To install FFmpeg to Azahar, press Open and select your FFmpeg directory. @@ -4694,264 +4704,264 @@ To view a guide on how to install FFmpeg, press Help. - + Load 3DS ROM Files - + 3DS ROM Files (*.cia *.cci *.3dsx *.cxi *.3ds) - + 3DS Compressed ROM File (*.%1) - + Save 3DS Compressed ROM File - + Select Output 3DS Compressed ROM Folder - + Load 3DS Compressed ROM Files - + 3DS Compressed ROM Files (*.zcia *zcci *z3dsx *zcxi) - + 3DS ROM File (*.%1) - + Save 3DS ROM File - + Select Output 3DS ROM Folder - + Select FFmpeg Directory - + The provided FFmpeg directory is missing %1. Please make sure the correct directory was selected. - + FFmpeg has been sucessfully installed. - + Installation of FFmpeg failed. Check the log file for details. - + Could not start video dumping.<br>Please ensure that the video encoder is configured correctly.<br>Refer to the log for details. - + Recording %1 - + Playing %1 / %2 - + Movie Finished - + (Accessing SharedExtData) - + (Accessing SystemSaveData) - + (Accessing BossExtData) - + (Accessing ExtData) - + (Accessing SaveData) - + MB/s - + KB/s - + Artic Traffic: %1 %2%3 - + Speed: %1% 速度:%1% - + Speed: %1% / %2% 速度:%1% / %2% - + App: %1 FPS - + Frame: %1 ms (GPU: [CMD: %2 ms, SWP: %3 ms], IPC: %4 ms, SVC: %5 ms, Rem: %6 ms) - + Frame: %1 ms 影格:%1 ms - + VOLUME: MUTE - + VOLUME: %1% Volume percentage (e.g. 50%) - + %1 is missing. Please <a href='https://web.archive.org/web/20240304201103/https://citra-emu.org/wiki/dumping-system-archives-and-the-shared-fonts-from-a-3ds-console/'>dump your system archives</a>.<br/>Continuing emulation may result in crashes and bugs. - + A system archive - + System Archive Not Found 找不到系統檔案 - + System Archive Missing - + Save/load Error - + Fatal Error 嚴重錯誤 - + A fatal error occurred. <a href='https://web.archive.org/web/20240228001712/https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>Check the log</a> for details.<br/>Continuing emulation may result in crashes and bugs. - + Fatal Error encountered - + Continue 繼續 - + Quit Application - + OK 確定 - + Would you like to exit now? 您確定要離開嗎? - + The application is still running. Would you like to stop emulation? - + Playback Completed 播放完成 - + Movie playback completed. 影片已結束播放。 - + Update Available - + Update %1 for Azahar is available. Would you like to download it? - + Primary Window - + Secondary Window @@ -5014,42 +5024,42 @@ Would you like to download it? GRenderWindow - + OpenGL not available! - + OpenGL shared contexts are not supported. - + Error while initializing OpenGL! - + Your GPU may not support OpenGL, or you do not have the latest graphics driver. - + Error while initializing OpenGL 4.3! - + Your GPU may not support OpenGL 4.3, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 - + Error while initializing OpenGL ES 3.2! - + Your GPU may not support OpenGL ES 3.2, or you do not have the latest graphics driver.<br><br>GL Renderer:<br>%1 @@ -5057,244 +5067,249 @@ Would you like to download it? GameList - - + + Compatibility 相容性 - - + + Region 地區 - - + + File type 檔案類型 - - + + Size 大小 - - + + Play time - + Favorite - + Eject Cartridge - + Insert Cartridge - + Open - + Application Location - + Save Data Location - + Extra Data Location - + Update Data Location - + DLC Data Location - + Texture Dump Location - + Custom Texture Location - + Mods Location - + Dump RomFS - + Disk Shader Cache - + Open Shader Cache Location - + Delete OpenGL Shader Cache - + + Delete Vulkan Shader Cache + + + + Uninstall - + Everything - + Application - + Update - + DLC - + Remove Play Time Data - + Create Shortcut - + Add to Desktop - + Add to Applications Menu - + Stress Test: App Launch - + Properties - - - - + + + + Azahar - + Are you sure you want to completely uninstall '%1'? This will delete the application if installed, as well as any installed updates or DLC. - - + + %1 (Update) - - + + %1 (DLC) - + Are you sure you want to uninstall '%1'? - + Are you sure you want to uninstall the update for '%1'? - + Are you sure you want to uninstall all DLC for '%1'? - + Scan Subfolders 掃描子資料夾 - + Remove Application Directory - + Move Up - + Move Down - + Open Directory Location 開啟資料夾位置 - + Clear 清除 - + Name 名稱 @@ -5380,7 +5395,7 @@ Screen. GameListPlaceholder - + Double-click to add a new folder to the application list @@ -5388,27 +5403,27 @@ Screen. GameListSearchField - + of / - + result 項符合 - + results 項符合 - + Filter: 項目篩選 - + Enter pattern to filter 輸入項目關鍵字 @@ -6040,23 +6055,23 @@ Debug Message: - - Loading Shaders %1 / %2 + + Loading %3 %1 / %2 - + Launching... - + Now Loading %1 - + Estimated Time %1 diff --git a/src/android/app/src/main/res/values-b+ca+ES+valencia/strings.xml b/src/android/app/src/main/res/values-b+ca+ES+valencia/strings.xml index d95239adf..cfbaacf0f 100644 --- a/src/android/app/src/main/res/values-b+ca+ES+valencia/strings.xml +++ b/src/android/app/src/main/res/values-b+ca+ES+valencia/strings.xml @@ -511,8 +511,6 @@ S\'esperen errors gràfics temporals quan estigue activat. Aplicació cifrada no suportada Preparant ombrejadors - Construint ombrejadors - Jugar Desinstal·lar Aplicació diff --git a/src/android/app/src/main/res/values-b+da+DK/strings.xml b/src/android/app/src/main/res/values-b+da+DK/strings.xml index 85989da11..857c14106 100644 --- a/src/android/app/src/main/res/values-b+da+DK/strings.xml +++ b/src/android/app/src/main/res/values-b+da+DK/strings.xml @@ -118,6 +118,8 @@ Nogle kontrollere er muligvis ikke i stand til at tilknytte deres D-Pad som en akse. Hvis det er tilfældet, skal du bruge afsnittet D-Pad (knapper). D-Pad (knapper) Udfyld kun disse D-Pad, hvis du har problemer med opsætningen af D-Pad (akser). + Vertikal akse + Horisontal akse Op Ned Venstre @@ -126,6 +128,8 @@ Tryk på eller flyt et input. Inputbinding Tryk på eller flyt et input for at binde det til %1$s. + Tryk OP på dit joystick. + Tryk HØJRE på dit joystick. HOME Byt skærme Turbo @@ -546,8 +550,6 @@ Forberedelse af shaders - Bygning af shaders - Spil Afinstaller applikation diff --git a/src/android/app/src/main/res/values-b+es+ES/strings.xml b/src/android/app/src/main/res/values-b+es+ES/strings.xml index 3b6d0e62f..3ffed8a57 100644 --- a/src/android/app/src/main/res/values-b+es+ES/strings.xml +++ b/src/android/app/src/main/res/values-b+es+ES/strings.xml @@ -115,6 +115,8 @@ Es posible que algunos controladores no puedan asignar su D-pad como un eje. Si ese es el caso, utilice la sección D-Pad (botones). D-Pad (Botón) Asigne solo el D-pad a éstos si tiene problemas con las asignaciones de botones del D-Pad (Eje). + Eje Vertical + Eje Horizontal Arriba Abajo Izquierda @@ -123,6 +125,8 @@ Pulsa o mueve un botón/palanca. Asignación de botones Pulsa o mueve un botón para asignarlo a %1$s. + Presiona ARRIBA en tu joystick. + Presiona DERECHA en tu joystick. HOME Intercambiar Pantallas Turbo @@ -534,7 +538,7 @@ Se esperan fallos gráficos temporales cuando ésta esté activado. Modo de sistema no válido Preparando shaders - Construyendo shaders + Construyendo%s Jugar diff --git a/src/android/app/src/main/res/values-b+pl+PL/strings.xml b/src/android/app/src/main/res/values-b+pl+PL/strings.xml index 56277ebbd..3e14f6051 100644 --- a/src/android/app/src/main/res/values-b+pl+PL/strings.xml +++ b/src/android/app/src/main/res/values-b+pl+PL/strings.xml @@ -550,7 +550,7 @@ Przygotowanie shaderów - Tworzenie shaderów + Tworzenie%s Odtwórz diff --git a/src/android/app/src/main/res/values-b+pt+BR/strings.xml b/src/android/app/src/main/res/values-b+pt+BR/strings.xml index 4ade6f242..f963b9ad0 100644 --- a/src/android/app/src/main/res/values-b+pt+BR/strings.xml +++ b/src/android/app/src/main/res/values-b+pt+BR/strings.xml @@ -546,8 +546,6 @@ Preparando Shaders - Construindo Shaders - Jogar Desinstalar Aplicativo diff --git a/src/android/app/src/main/res/values-b+ru+RU/strings.xml b/src/android/app/src/main/res/values-b+ru+RU/strings.xml index 726c27f4b..5772eb4c2 100644 --- a/src/android/app/src/main/res/values-b+ru+RU/strings.xml +++ b/src/android/app/src/main/res/values-b+ru+RU/strings.xml @@ -304,8 +304,6 @@ Возникла критическая ошибка. Откройте лог для получения информации.\nВозобновление эмуляции может привести к сбоям и вылетам. Подготовка шейдеров - Построение шейдеров - Чит-коды Добавить чит-код diff --git a/src/android/app/src/main/res/values-b+tr+TR/strings.xml b/src/android/app/src/main/res/values-b+tr+TR/strings.xml index 93588445f..574a9d0e8 100644 --- a/src/android/app/src/main/res/values-b+tr+TR/strings.xml +++ b/src/android/app/src/main/res/values-b+tr+TR/strings.xml @@ -481,8 +481,6 @@ Desteklenmeyen şifreli uygulama Gölgelendiriciler Hazırlanıyor - Gölgelendiriciler Oluşturuluyor - Oyna Uygulamayı Sil diff --git a/src/android/app/src/main/res/values-b+zh+CN/strings.xml b/src/android/app/src/main/res/values-b+zh+CN/strings.xml index c2a7ff839..b417e255f 100644 --- a/src/android/app/src/main/res/values-b+zh+CN/strings.xml +++ b/src/android/app/src/main/res/values-b+zh+CN/strings.xml @@ -528,8 +528,6 @@ 不支持的加密应用 正在准备着色器 - 正在构建着色器 - 开始游戏 卸载应用 diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml index 80cf295a1..5286641ed 100644 --- a/src/android/app/src/main/res/values-de/strings.xml +++ b/src/android/app/src/main/res/values-de/strings.xml @@ -503,8 +503,6 @@ Nicht unterstützte verschlüsselte Anwendung Shader werden vorbereitet - Shader werden erstellt - Spielen Anwendung deinstallieren diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index 015da169e..95a18358a 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -550,8 +550,6 @@ Préparation des shaders - Construction des shaders - Jouer Désinstaller l\'application diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index f1afef6ea..7c3f37af6 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -143,11 +143,11 @@ Divertiti usando l\'emulatore! Velocità Turbo disabilitata - File di Sistema + File di sistema Esegui operazioni sui file di sistema come installare file di sistema o avviare il Menu Home Connettiti a Artic Setup Tool Azahar Artic Setup Tool.
Note:
  • Questa operazione installerà dati unici della console su Azahar, non condividere le tue cartelle utente o NAND dopo aver completato il processo di configurazione!
  • Durante il processo di configurazione, Azahar si collegherà alla console che sta eseguendo lo strumento di configurazione. Puoi scollegare la console in seguito dalla scheda \"File di sistema\" nel menu delle opzioni dell\'emulatore.
  • Non andare online contemporaneamente con Azahar e la tua console 3DS dopo aver configurato i file di sistema, poiché ciò potrebbe causare problemi.
  • La configurazione del vecchio 3DS è necessaria affinché la configurazione del nuovo 3DS funzioni (si consiglia di configurare entrambi).
  • Entrambe le modalità di configurazione funzioneranno indipendentemente dal modello della console che esegue lo strumento di configurazione.
]]>
- Recupero dello stato attuale dei file di sistema, per favore attendi... + Recupero dello stato attuale dei file di sistema, attendere... Scollega i dati univoci della console
  • il tuo OTP, SecureInfo e LocalFriendCodeSeed verranno rimossi da Azahar.
  • La tua lista amici verrà reimpostata e verrai disconnesso dal tuo account NNID/PNID.
  • I file di sistema e i titoli eshop ottenuti tramite Azahar diventeranno inaccessibili finché la stessa console non verrà nuovamente collegata tramite lo strumento di configurazione (i dati di salvataggio non andranno persi).

Continuare?]]>
Setup Old 3DS @@ -553,8 +553,6 @@ Divertiti usando l\'emulatore! Preparazione degli shader - Compilazione degli shader - Riproduci Disinstalla applicazione diff --git a/src/android/app/src/main/res/values-sv/strings.xml b/src/android/app/src/main/res/values-sv/strings.xml index 8e658bf93..09828b12e 100644 --- a/src/android/app/src/main/res/values-sv/strings.xml +++ b/src/android/app/src/main/res/values-sv/strings.xml @@ -550,8 +550,6 @@ Förbereder shaders - Bygger shaders - Spela Avinstallera applikation From d0eaf07a400685a489c99c54fcbf6ecee82ac7ac Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Tue, 17 Feb 2026 15:25:54 +0000 Subject: [PATCH 08/94] cmake: Added missing newline to missing submodule message --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c0e51b67..6a3635988 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -223,7 +223,7 @@ function(check_submodules_present) foreach(module ${gitmodules}) string(REGEX REPLACE "path *= *" "" module ${module}) if (NOT EXISTS "${PROJECT_SOURCE_DIR}/${module}/.git") - message(SEND_ERROR "Git submodule ${module} not found." + message(SEND_ERROR "Git submodule ${module} not found.\n" "Please run: git submodule update --init --recursive") endif() endforeach() From d9b77cc21ed425a6b7c7d91c806c1c90146b1de7 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Thu, 19 Feb 2026 17:30:25 -0500 Subject: [PATCH 09/94] Implement libretro core (#1215) * libretro core * Bringing citra libretro implementation over * libretro: hook up vulkan renderer * libretro: github actions * libretro: gyro * libretro: core options v2 * libretro: on ios turn off shader jit if unavailable * moltenvk 1.3.0 introduces 8-bit indexes but allocates 16-bit for metal; this ends up allocating stream buffer * 2 = 132MiB. Instead, just use 16-bit indexes. (This will be necessary for standalone when bumping moltenvk version.) * libretro core: address review feedback * libretro: microphone support * cmake: Add ENABLE_ROOM_STANDALONE to list of incompatible libretro flags * libretro: proper initial geometry * libretro: fix software renderer * libretro: address review feedback * .github/libretro.yml: Pin macOS runners at macOS 26 * ci: Remove explicit selection of Xcode 16.0 * .github/libretro.yml: remove unnecessary windows builder apt commands * .github/libretro.yml: bump min macos version to 11.0 * ci: Re-enable CI jobs for all libretro cores This is under the condition that we don't introduce build cache for these builds --------- Co-authored-by: OpenSauce04 Co-authored-by: PabloMK7 --- .github/workflows/libretro.yml | 166 +++ .gitlab-ci.yml | 127 +++ .gitmodules | 3 + CMakeLists.txt | 48 +- externals/CMakeLists.txt | 9 + externals/libretro-common/CMakeLists.txt | 16 + externals/libretro-common/libretro-common | 1 + src/CMakeLists.txt | 14 +- src/audio_core/CMakeLists.txt | 1 + src/audio_core/dsp_interface.cpp | 12 +- src/audio_core/input_details.cpp | 17 +- src/audio_core/input_details.h | 3 +- src/audio_core/libretro_input.cpp | 327 ++++++ src/audio_core/libretro_input.h | 36 + src/audio_core/libretro_sink.cpp | 27 + src/audio_core/libretro_sink.h | 33 + src/audio_core/sink.h | 19 +- src/audio_core/sink_details.cpp | 12 +- src/audio_core/sink_details.h | 3 +- src/citra_libretro/CMakeLists.txt | 97 ++ src/citra_libretro/citra_libretro.cpp | 717 ++++++++++++ src/citra_libretro/citra_libretro.h | 10 + src/citra_libretro/core_settings.cpp | 1014 +++++++++++++++++ src/citra_libretro/core_settings.h | 41 + .../emu_window/libretro_window.cpp | 342 ++++++ .../emu_window/libretro_window.h | 79 ++ src/citra_libretro/environment.cpp | 281 +++++ src/citra_libretro/environment.h | 129 +++ src/citra_libretro/input/input_factory.cpp | 216 ++++ src/citra_libretro/input/input_factory.h | 20 + src/citra_libretro/input/mouse_tracker.cpp | 440 +++++++ src/citra_libretro/input/mouse_tracker.h | 111 ++ src/citra_libretro/libretro.osx.def | 27 + src/citra_libretro/libretro_vk.cpp | 860 ++++++++++++++ src/citra_libretro/libretro_vk.h | 175 +++ src/common/CMakeLists.txt | 12 +- src/common/error.cpp | 8 +- src/common/file_util.cpp | 124 +- src/common/file_util.h | 33 +- src/common/logging/backend.cpp | 89 +- src/common/logging/backend.h | 8 +- src/common/zstd_compression.cpp | 1 - src/core/core.h | 4 + src/core/frontend/emu_window.h | 11 + src/core/hle/service/cfg/cfg.cpp | 8 + src/core/hle/service/mic/mic_u.cpp | 6 +- src/core/hle/service/soc/soc_u.cpp | 4 +- src/core/savestate.cpp | 75 +- src/tests/CMakeLists.txt | 4 + src/video_core/CMakeLists.txt | 10 +- src/video_core/gpu.cpp | 48 + src/video_core/gpu.h | 6 + .../renderer_opengl/gl_shader_disk_cache.cpp | 23 + .../renderer_opengl/renderer_opengl.cpp | 12 +- .../renderer_vulkan/renderer_vulkan.cpp | 6 +- .../renderer_vulkan/renderer_vulkan.h | 8 + .../renderer_vulkan/vk_instance.cpp | 19 +- src/video_core/renderer_vulkan/vk_instance.h | 20 +- .../renderer_vulkan/vk_resource_pool.cpp | 9 +- .../renderer_vulkan/vk_scheduler.cpp | 9 +- .../renderer_vulkan/vk_texture_runtime.cpp | 10 +- 61 files changed, 5907 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/libretro.yml create mode 100644 .gitlab-ci.yml create mode 100644 externals/libretro-common/CMakeLists.txt create mode 160000 externals/libretro-common/libretro-common create mode 100644 src/audio_core/libretro_input.cpp create mode 100644 src/audio_core/libretro_input.h create mode 100644 src/audio_core/libretro_sink.cpp create mode 100644 src/audio_core/libretro_sink.h create mode 100644 src/citra_libretro/CMakeLists.txt create mode 100644 src/citra_libretro/citra_libretro.cpp create mode 100644 src/citra_libretro/citra_libretro.h create mode 100644 src/citra_libretro/core_settings.cpp create mode 100644 src/citra_libretro/core_settings.h create mode 100644 src/citra_libretro/emu_window/libretro_window.cpp create mode 100644 src/citra_libretro/emu_window/libretro_window.h create mode 100644 src/citra_libretro/environment.cpp create mode 100644 src/citra_libretro/environment.h create mode 100644 src/citra_libretro/input/input_factory.cpp create mode 100644 src/citra_libretro/input/input_factory.h create mode 100644 src/citra_libretro/input/mouse_tracker.cpp create mode 100644 src/citra_libretro/input/mouse_tracker.h create mode 100644 src/citra_libretro/libretro.osx.def create mode 100644 src/citra_libretro/libretro_vk.cpp create mode 100644 src/citra_libretro/libretro_vk.h diff --git a/.github/workflows/libretro.yml b/.github/workflows/libretro.yml new file mode 100644 index 000000000..5225a43fa --- /dev/null +++ b/.github/workflows/libretro.yml @@ -0,0 +1,166 @@ +name: citra-libretro + +on: + push: + branches: [ "*" ] + tags: [ "*" ] + pull_request: + branches: [ master ] + workflow_dispatch: + +env: + CORE_ARGS: -DENABLE_LIBRETRO=ON + +jobs: + android: + runs-on: ubuntu-22.04 + env: + OS: android + TARGET: arm64-v8a + API_LEVEL: 21 + ANDROID_NDK_VERSION: 26.2.11394342 + ANDROID_ABI: arm64-v8a + BUILD_DIR: build/android-arm64-v8a + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Set tag name + run: | + if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then + echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV + fi + echo $GIT_TAG_NAME + - name: Update Android SDK CMake version + run: | + echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "ndk;$ANDROID_NDK_VERSION" + echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3" + - name: Build + run: | + export NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/$ANDROID_NDK_VERSION + ${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake $CORE_ARGS -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI . -B $BUILD_DIR + ${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc) + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro_android.so + linux: + runs-on: ubuntu-22.04 + env: + OS: linux + TARGET: x86_64 + BUILD_DIR: build/linux-x86_64 + EXTRA_PATH: bin/Release + EXTRA_CORE_ARGS: -DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12 -DENABLE_LTO=OFF + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc) + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.so + windows: + runs-on: ubuntu-latest + env: + OS: windows + TARGET: x86_64 + BUILD_DIR: build/windows-x86_64 + EXTRA_CORE_ARGS: -DENABLE_LTO=OFF -G Ninja + CMAKE: x86_64-w64-mingw32.static-cmake + IMAGE: git.libretro.com:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12 + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build in cross-container + run: | + docker pull $IMAGE + docker run --rm --user root \ + -v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \ + -w "${GITHUB_WORKSPACE}" \ + $IMAGE \ + bash -lc "\ + ${CMAKE} $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR && \ + ${CMAKE} --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)" + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dll + macos: + runs-on: macos-26 + strategy: + matrix: + target: ["x86_64", "arm64"] + env: + OS: macos + TARGET: ${{ matrix.target }} + MACOSX_DEPLOYMENT_TARGET: 11.0 + BUILD_DIR: build/osx-${{ matrix.target }} + EXTRA_PATH: bin/Release + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install tools + run: brew install spirv-tools + - name: Build + run: | + cmake $CORE_ARGS -DCMAKE_OSX_ARCHITECTURES=$TARGET . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib + ios: + runs-on: macos-26 + env: + OS: ios + TARGET: arm64 + BUILD_DIR: build/ios-arm64 + EXTRA_PATH: bin/Release + EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib + tvos: + runs-on: macos-26 + env: + OS: tvos + TARGET: arm64 + BUILD_DIR: build/tvos-arm64 + EXTRA_PATH: bin/Release + EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Build + run: | + cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR + cmake --build $BUILD_DIR --target azahar_libretro --config Release + - name: Upload + uses: actions/upload-artifact@v4 + with: + name: ${{ env.OS }}-${{ env.TARGET }} + path: ${{ env.BUILD_DIR }}/${{ env.EXTRA_PATH }}/azahar_libretro.dylib diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..e8f4f141b --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,127 @@ +.core-defs: + variables: + JNI_PATH: . + CORENAME: azahar + API_LEVEL: 21 + BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_TESTS=OFF + CORE_ARGS: ${BASE_CORE_ARGS} + EXTRA_PATH: bin/Release + +variables: + STATIC_RETROARCH_BRANCH: master + GIT_SUBMODULE_STRATEGY: recursive + +# Inclusion templates, required for the build to work +include: + ################################## DESKTOPS ############################## ## + # Windows 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/windows-cmake-mingw.yml' + + # Linux 64-bit + - project: 'libretro-infrastructure/ci-templates' + file: '/linux-cmake.yml' + + # MacOS x86_64 + - project: 'libretro-infrastructure/ci-templates' + file: '/osx-cmake-x86.yml' + + # MacOS ARM64 + - project: 'libretro-infrastructure/ci-templates' + file: '/osx-cmake-arm64.yml' + + ################################## CELLULAR ############################## ## + # Android + - project: 'libretro-infrastructure/ci-templates' + file: '/android-cmake.yml' + + # iOS + - project: 'libretro-infrastructure/ci-templates' + file: '/ios-cmake.yml' + + # tvOS + - project: 'libretro-infrastructure/ci-templates' + file: '/tvos-cmake.yml' + + ################################## CONSOLES ############################## ## + +# Stages for building +stages: + - build-prepare + - build-shared + - build-static + +############################################################################## +#################################### STAGES ################################## +############################################################################## +# +################################### DESKTOPS ################################# +# Windows 64-bit +libretro-build-windows-x64: + extends: + - .core-defs + - .libretro-windows-cmake-x86_64 + image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12 + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF -G Ninja + +# Linux 64-bit +libretro-build-linux-x64: + extends: + - .core-defs + - .libretro-linux-cmake-x86_64 + image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:backports + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF + CC: /usr/bin/gcc-12 + CXX: /usr/bin/g++-12 + +# MacOS x86_64 +libretro-build-osx-x64: + tags: + - mac-apple-silicon + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64 + MACOSX_DEPLOYMENT_TARGET: "10.15" + extends: + - .core-defs + - .libretro-osx-cmake-x86_64 + +# MacOS ARM64 +libretro-build-osx-arm64: + extends: + - .core-defs + - .libretro-osx-cmake-arm64 + +################################### CELLULAR ################################# +# Android ARMv8a +android-arm64-v8a: + extends: + - .libretro-android-cmake-arm64-v8a + - .core-defs + variables: + ANDROID_NDK_VERSION: 26.2.11394342 + NDK_ROOT: /android-sdk-linux/ndk/$ANDROID_NDK_VERSION + +# iOS arm64 +libretro-build-ios-arm64: + extends: + - .libretro-ios-cmake-arm64 + - .core-defs + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + IOS_MINVER: "14.0" + EXTRA_PATH: bin/RelWithDebInfo + +# tvOS arm64 +libretro-build-tvos-arm64: + extends: + - .libretro-tvos-cmake-arm64 + - .core-defs + variables: + CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF + MINVER: "14.0" + EXTRA_PATH: bin/RelWithDebInfo + +################################### CONSOLES ################################# + diff --git a/.gitmodules b/.gitmodules index cb600a64c..18c5cc79a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -103,3 +103,6 @@ [submodule "externals/xxHash"] path = externals/xxHash url = https://github.com/Cyan4973/xxHash.git +[submodule "externals/libretro-common"] + path = externals/libretro-common/libretro-common + url = https://github.com/libretro/libretro-common.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a3635988..df86bf28c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,20 +17,23 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules") include(DownloadExternals) include(CMakeDependentOption) -include(FindPkgConfig) project(citra LANGUAGES C CXX ASM) +# must be invoked after project() command when using CMAKE_TOOLCHAIN_FILE +include(FindPkgConfig) if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS") enable_language(OBJC OBJCXX) endif() +option(ENABLE_LIBRETRO "Build as a LibRetro core" OFF) + # Some submodules like to pick their own default build type if not specified. # Make sure we default to Release build type always, unless the generator has custom types. if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() -if (APPLE) +if (APPLE AND NOT ENABLE_LIBRETRO) # Silence warnings on empty objects, for example when platform-specific code is #ifdef'd out. set(CMAKE_C_ARCHIVE_CREATE " Scr ") set(CMAKE_CXX_ARCHIVE_CREATE " Scr ") @@ -90,6 +93,17 @@ else() set(DEFAULT_ENABLE_OPENGL ON) endif() +# Track which options were explicitly set by the user (for libretro conflict detection) +set(_LIBRETRO_INCOMPATIBLE_OPTIONS + ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING + ENABLE_OPENAL ENABLE_ROOM ENABLE_ROOM_STANDALONE ENABLE_CUBEB ENABLE_LIBUSB) +set(_USER_SET_OPTIONS "") +foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) + if(DEFINED ${_opt}) + list(APPEND _USER_SET_OPTIONS ${_opt}) + endif() +endforeach() + option(ENABLE_SDL2 "Enable using SDL2" ON) CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF) option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF) @@ -130,6 +144,31 @@ option(ENABLE_NATIVE_OPTIMIZATION "Enables processor-specific optimizations via option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON) option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON) +# Handle incompatible options for libretro builds +if(ENABLE_LIBRETRO) + # Check for explicitly-set conflicting options + set(_CONFLICTS "") + foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) + list(FIND _USER_SET_OPTIONS ${_opt} _idx) + if(NOT _idx EQUAL -1 AND ${_opt}) + list(APPEND _CONFLICTS ${_opt}) + endif() + endforeach() + + if(_CONFLICTS) + string(REPLACE ";" ", " _CONFLICTS_STR "${_CONFLICTS}") + message(FATAL_ERROR + "ENABLE_LIBRETRO is incompatible with: ${_CONFLICTS_STR}\n" + "These options were explicitly enabled but are not supported for libretro builds.\n" + "Remove these options or set them to OFF.") + endif() + + # Force disable incompatible options (handles defaulted-on options) + foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) + set(${_opt} OFF CACHE BOOL "Disabled for libretro" FORCE) + endforeach() +endif() + # Pass the following values to C++ land if (ENABLE_QT) add_definitions(-DENABLE_QT) @@ -300,6 +339,9 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN NO) # set up output paths for executable binaries set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/$) +if (ENABLE_LIBRETRO) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() # System imported libraries # ====================== @@ -359,7 +401,7 @@ if (APPLE) find_library(IOSURFACE_LIBRARY IOSurface REQUIRED) set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY}) - if (ENABLE_VULKAN) + if (ENABLE_VULKAN AND NOT ENABLE_LIBRETRO) if (NOT USE_SYSTEM_MOLTENVK) download_moltenvk() endif() diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index 01e76a095..d7cf35f3b 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -292,6 +292,15 @@ if (USE_DISCORD_PRESENCE) target_include_directories(discord-rpc INTERFACE ./discord-rpc/include) endif() +# LibRetro +if (ENABLE_LIBRETRO) + add_library(libretro INTERFACE) + target_include_directories(libretro INTERFACE ./libretro-common/libretro-common/include) + if (ANDROID) + add_subdirectory(libretro-common EXCLUDE_FROM_ALL) + endif() +endif() + # JSON add_library(json-headers INTERFACE) if (USE_SYSTEM_JSON) diff --git a/externals/libretro-common/CMakeLists.txt b/externals/libretro-common/CMakeLists.txt new file mode 100644 index 000000000..0bb4e6b5e --- /dev/null +++ b/externals/libretro-common/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libretro_common STATIC + libretro-common/compat/compat_posix_string.c + libretro-common/compat/fopen_utf8.c + libretro-common/encodings/encoding_utf.c + libretro-common/compat/compat_strl.c + libretro-common/file/file_path.c + libretro-common/streams/file_stream.c + libretro-common/streams/file_stream_transforms.c + libretro-common/string/stdstring.c + libretro-common/time/rtime.c + libretro-common/vfs/vfs_implementation.c +) +target_include_directories(libretro_common PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/libretro-common + ${CMAKE_CURRENT_SOURCE_DIR}/libretro-common/include +) diff --git a/externals/libretro-common/libretro-common b/externals/libretro-common/libretro-common new file mode 160000 index 000000000..7fc7feedd --- /dev/null +++ b/externals/libretro-common/libretro-common @@ -0,0 +1 @@ +Subproject commit 7fc7feeddca391be65c94e6541381467684b814d diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 69d17bac2..b18925867 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -110,10 +110,14 @@ else() # In case a flag isn't supported on e.g. a certain architecture, don't error. -Wno-unused-command-line-argument # Build fortification options - -Wp,-D_GLIBCXX_ASSERTIONS -fstack-protector-strong - -fstack-clash-protection ) + if (NOT ENABLE_LIBRETRO) + add_compile_options( + -Wp,-D_GLIBCXX_ASSERTIONS + -fstack-clash-protection + ) + endif() # If we define _FORTIFY_SOURCE when it is already defined, compilation will fail string(FIND "-D_FORTIFY_SOURCE" "${CMAKE_CXX_FLAGS} " FORTIFY_SOURCE_DEFINED) @@ -201,6 +205,10 @@ if (ENABLE_QT OR ENABLE_SDL2_FRONTEND) add_subdirectory(citra_meta) endif() +if (ENABLE_LIBRETRO) + add_subdirectory(citra_libretro) +endif() + if (ENABLE_ROOM) add_subdirectory(citra_room) endif() @@ -209,7 +217,7 @@ if (ENABLE_ROOM_STANDALONE) add_subdirectory(citra_room_standalone) endif() -if (ANDROID) +if (ANDROID AND NOT ENABLE_LIBRETRO) add_subdirectory(android/app/src/main/jni) target_include_directories(citra-android PRIVATE android/app/src/main) endif() diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index a000e1fc1..6ea16672e 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -38,6 +38,7 @@ add_library(audio_core STATIC $<$:sdl2_sink.cpp sdl2_sink.h> $<$:cubeb_sink.cpp cubeb_sink.h cubeb_input.cpp cubeb_input.h> + $<$:libretro_sink.cpp libretro_sink.h libretro_input.cpp libretro_input.h> $<$:openal_input.cpp openal_input.h openal_sink.cpp openal_sink.h> ) diff --git a/src/audio_core/dsp_interface.cpp b/src/audio_core/dsp_interface.cpp index 259459a5b..52e781ca4 100644 --- a/src/audio_core/dsp_interface.cpp +++ b/src/audio_core/dsp_interface.cpp @@ -41,7 +41,11 @@ void DspInterface::OutputFrame(StereoFrame16 frame) { return; } - fifo.Push(frame.data(), frame.size()); + if (sink->ImmediateSubmission()) { + sink->PushSamples(frame.data(), frame.size()); + } else { + fifo.Push(frame.data(), frame.size()); + } auto video_dumper = system.GetVideoDumper(); if (video_dumper && video_dumper->IsDumping()) { @@ -54,7 +58,11 @@ void DspInterface::OutputSample(std::array sample) { return; } - fifo.Push(&sample, 1); + if (sink->ImmediateSubmission()) { + sink->PushSamples(&sample, 1); + } else { + fifo.Push(&sample, 1); + } auto video_dumper = system.GetVideoDumper(); if (video_dumper && video_dumper->IsDumping()) { diff --git a/src/audio_core/input_details.cpp b/src/audio_core/input_details.cpp index e98310d31..f0a3b80b4 100644 --- a/src/audio_core/input_details.cpp +++ b/src/audio_core/input_details.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -15,6 +15,9 @@ #ifdef HAVE_OPENAL #include "audio_core/openal_input.h" #endif +#ifdef HAVE_LIBRETRO +#include "audio_core/libretro_input.h" +#endif #include "common/logging/log.h" #include "core/core.h" @@ -22,6 +25,18 @@ namespace AudioCore { namespace { // input_details is ordered in terms of desirability, with the best choice at the top. constexpr std::array input_details = { +#ifdef HAVE_LIBRETRO + InputDetails{InputType::LibRetro, "Real Device (LibRetro)", true, + [](Core::System& system, std::string_view device_id) -> std::unique_ptr { + if (!system.HasMicPermission()) { + LOG_WARNING(Audio, + "Microphone permission denied, falling back to null input."); + return std::make_unique(); + } + return std::make_unique(); + }, + [] { return std::vector{"LibRetro Microphone"}; }}, +#endif #ifdef HAVE_CUBEB InputDetails{InputType::Cubeb, "Real Device (Cubeb)", true, [](Core::System& system, std::string_view device_id) -> std::unique_ptr { diff --git a/src/audio_core/input_details.h b/src/audio_core/input_details.h index a091dfe6c..709453eac 100644 --- a/src/audio_core/input_details.h +++ b/src/audio_core/input_details.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -24,6 +24,7 @@ enum class InputType : u32 { Static = 2, Cubeb = 3, OpenAL = 4, + LibRetro = 5, }; struct InputDetails { diff --git a/src/audio_core/libretro_input.cpp b/src/audio_core/libretro_input.cpp new file mode 100644 index 000000000..49354a65d --- /dev/null +++ b/src/audio_core/libretro_input.cpp @@ -0,0 +1,327 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include "audio_core/libretro_input.h" +#include "citra_libretro/environment.h" +#include "common/logging/log.h" +#include "common/ring_buffer.h" +#include "libretro.h" + +namespace AudioCore { + +namespace { +// Global instance pointer for access from retro_run +LibRetroInput* g_libretro_input = nullptr; +} // namespace + +struct LibRetroInput::Impl { + std::optional mic_interface; + retro_microphone_t* mic_handle = nullptr; + bool is_sampling = false; + u8 sample_size_in_bytes = 2; + int warmup_frames = 0; + + // The rate at which the frontend actually provides samples (may differ from + // what the 3DS mic service requested). We open the mic at this rate to avoid + // RetroArch's internal resampler path, which has a convergence bug when + // downsampling (ratio < 1). We resample ourselves in Read() instead. + u32 native_sample_rate = 0; + + // Ring buffer for thread-safe sample storage + // Capacity: 4096 samples should be plenty for buffering between frames + // The 3DS mic service reads 16 samples at a time at ~32728 Hz + Common::RingBuffer sample_buffer; + + // Temporary buffer for reading from frontend + std::vector read_buffer; + + Impl() { + // Try to get the microphone interface from the frontend + retro_microphone_interface interface{}; + interface.interface_version = RETRO_MICROPHONE_INTERFACE_VERSION; + + if (LibRetro::GetMicrophoneInterface(&interface)) { + if (interface.interface_version == RETRO_MICROPHONE_INTERFACE_VERSION) { + mic_interface = interface; + LOG_INFO(Audio, "LibRetro microphone interface available (version {})", + interface.interface_version); + } else { + LOG_WARNING(Audio, + "LibRetro microphone interface version mismatch: expected {}, got {}", + RETRO_MICROPHONE_INTERFACE_VERSION, interface.interface_version); + } + } else { + LOG_WARNING(Audio, "LibRetro microphone interface not available"); + } + + // Keep this small enough that RetroArch's microphone_driver_read can + // fill its outgoing FIFO in a single flush iteration. The CoreAudio + // driver's internal FIFO is ~480 samples (10ms at 48kHz). If we + // request more than that, the blocking while-loop in + // microphone_driver_read must wait for the next hardware callback, + // and on ARM64 without memory barriers in the FIFO, it may never + // see the new data. 128 samples is conservative enough to succeed + // in one pass. + read_buffer.resize(128); + } + + ~Impl() { + CloseMicrophone(); + } + + bool EnsureMicrophoneOpen() { + if (mic_handle) { + return true; + } + + if (!mic_interface) { + return false; + } + + // Always open at 48000 Hz regardless of what the game requests. + // RetroArch's microphone_driver_read has a resampler whose while-loop + // deadlocks when the ratio is < 1 (core rate < device rate). The + // libretro get_params API only returns the effective (requested) rate, + // not the device's native rate, so we can't detect the mismatch. + // Opening at 48000 Hz (the most common hardware rate) keeps the + // frontend's internal resampling ratio at or near 1.0, avoiding the + // bug. We resample to the game's requested rate ourselves in Read(). + static constexpr u32 kMicOpenRate = 48000; + native_sample_rate = kMicOpenRate; + + retro_microphone_params_t params{}; + params.rate = kMicOpenRate; + + mic_handle = mic_interface->open_mic(¶ms); + if (!mic_handle) { + LOG_ERROR(Audio, "Failed to open LibRetro microphone"); + return false; + } + + // The frontend may start recording immediately in open_mic (e.g. + // CoreAudio calls AudioOutputUnitStart). Pause it right away so the + // mic is available but idle until StartSampling enables it. + mic_interface->set_mic_state(mic_handle, false); + + LOG_INFO(Audio, "LibRetro microphone opened at {} Hz (idle)", native_sample_rate); + return true; + } + + void CloseMicrophone() { + if (mic_interface && mic_handle) { + mic_interface->close_mic(mic_handle); + mic_handle = nullptr; + } + } + + bool SetMicrophoneActive(bool active) { + if (!mic_interface || !mic_handle) { + return false; + } + return mic_interface->set_mic_state(mic_handle, active); + } + + bool IsMicrophoneActive() const { + if (!mic_interface || !mic_handle) { + return false; + } + return mic_interface->get_mic_state(mic_handle); + } +}; + +LibRetroInput::LibRetroInput() : impl(std::make_unique()) { + g_libretro_input = this; +} + +LibRetroInput::~LibRetroInput() { + StopSampling(); + if (g_libretro_input == this) { + g_libretro_input = nullptr; + } +} + +void LibRetroInput::StartSampling(const InputParameters& params) { + if (IsSampling()) { + return; + } + + // LibRetro only provides signed 16-bit PCM samples + // We'll convert to the requested format in Read() + if (params.sign == Signedness::Unsigned) { + LOG_DEBUG(Audio, "Application requested unsigned PCM format; will convert from signed."); + } + + parameters = params; + impl->sample_size_in_bytes = params.sample_size / 8; + + if (!impl->EnsureMicrophoneOpen()) { + LOG_WARNING(Audio, "Cannot start sampling: microphone not available"); + return; + } + + // Enable the microphone (transitions from idle to recording) + if (!impl->SetMicrophoneActive(true)) { + LOG_ERROR(Audio, "Failed to activate microphone"); + return; + } + + impl->is_sampling = true; + // Give the audio hardware a few frames to start delivering data before + // we attempt a (blocking) read_mic call. Without this, the very first + // read can hang because the CoreAudio callback hasn't fired yet. + impl->warmup_frames = 10; + LOG_INFO(Audio, "LibRetro microphone sampling started at {} Hz, {} bit", params.sample_rate, + params.sample_size); +} + +void LibRetroInput::StopSampling() { + if (!impl->is_sampling) { + return; + } + + impl->SetMicrophoneActive(false); + impl->is_sampling = false; + + LOG_INFO(Audio, "LibRetro microphone sampling stopped (mic remains idle)"); +} + +bool LibRetroInput::IsSampling() { + return impl->is_sampling; +} + +void LibRetroInput::AdjustSampleRate(u32 sample_rate) { + if (!IsSampling()) { + return; + } + + // Restart with new sample rate + auto new_parameters = parameters; + new_parameters.sample_rate = sample_rate; + StopSampling(); + StartSampling(new_parameters); +} + +void LibRetroInput::PollMicrophone() { + // This is called from the main thread (retro_run) + // Read samples from the frontend and push to the ring buffer + + if (!impl->is_sampling || !impl->mic_interface || !impl->mic_handle) { + return; + } + + // Wait for the audio hardware to start delivering data before making + // any blocking read_mic calls. + if (impl->warmup_frames > 0) { + impl->warmup_frames--; + return; + } + + // Issue a memory fence before reading. RetroArch's CoreAudio mic driver + // fills its FIFO from a callback thread without memory barriers. On ARM64 + // (weak memory model), the main thread may not see the callback's writes + // without an explicit barrier. + std::atomic_thread_fence(std::memory_order_acquire); + + int samples_read = impl->mic_interface->read_mic(impl->mic_handle, impl->read_buffer.data(), + static_cast(impl->read_buffer.size())); + + if (samples_read > 0) { + impl->sample_buffer.Push( + std::span(impl->read_buffer.data(), static_cast(samples_read))); + } +} + +Samples LibRetroInput::Read() { + // This is called from the CoreTiming scheduler thread + // Pop samples from the ring buffer (thread-safe) + + if (!impl->is_sampling) { + return {}; + } + + // Pop available samples from the buffer (at native device rate) + std::vector raw_samples = impl->sample_buffer.Pop(); + + if (raw_samples.empty()) { + return {}; + } + + // Resample from native device rate to the rate the 3DS mic service expects + if (impl->native_sample_rate != 0 && impl->native_sample_rate != parameters.sample_rate) { + double ratio = static_cast(parameters.sample_rate) / impl->native_sample_rate; + auto output_count = static_cast(raw_samples.size() * ratio); + if (output_count == 0) { + return {}; + } + std::vector resampled(output_count); + for (std::size_t i = 0; i < output_count; i++) { + double src_pos = i / ratio; + auto idx = static_cast(src_pos); + double frac = src_pos - idx; + if (idx + 1 < raw_samples.size()) { + resampled[i] = + static_cast(raw_samples[idx] * (1.0 - frac) + raw_samples[idx + 1] * frac); + } else { + resampled[i] = raw_samples[std::min(idx, raw_samples.size() - 1)]; + } + } + raw_samples = std::move(resampled); + } + + // Convert sample format if needed + constexpr auto convert_s16_to_u16 = [](s16 sample) -> u16 { + return static_cast(sample) ^ 0x8000; + }; + + constexpr auto convert_s16_to_s8 = [](s16 sample) -> s8 { + return static_cast(sample >> 8); + }; + + constexpr auto convert_s16_to_u8 = [](s16 sample) -> u8 { + return static_cast((static_cast(sample) ^ 0x8000) >> 8); + }; + + Samples output; + output.reserve(raw_samples.size() * impl->sample_size_in_bytes); + + if (impl->sample_size_in_bytes == 1) { + // 8-bit output + if (parameters.sign == Signedness::Unsigned) { + for (s16 sample : raw_samples) { + output.push_back(convert_s16_to_u8(sample)); + } + } else { + for (s16 sample : raw_samples) { + output.push_back(static_cast(convert_s16_to_s8(sample))); + } + } + } else { + // 16-bit output + if (parameters.sign == Signedness::Unsigned) { + for (s16 sample : raw_samples) { + u16 converted = convert_s16_to_u16(sample); + output.push_back(static_cast(converted & 0xFF)); + output.push_back(static_cast(converted >> 8)); + } + } else { + // Signed 16-bit - just copy the raw bytes + const u8* data = reinterpret_cast(raw_samples.data()); + output.insert(output.end(), data, data + raw_samples.size() * 2); + } + } + + return output; +} + +LibRetroInput* GetLibRetroInput() { + return g_libretro_input; +} + +} // namespace AudioCore diff --git a/src/audio_core/libretro_input.h b/src/audio_core/libretro_input.h new file mode 100644 index 000000000..2320e4ef2 --- /dev/null +++ b/src/audio_core/libretro_input.h @@ -0,0 +1,36 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "audio_core/input.h" + +namespace AudioCore { + +class LibRetroInput final : public Input { +public: + LibRetroInput(); + ~LibRetroInput() override; + + void StartSampling(const InputParameters& params) override; + void StopSampling() override; + bool IsSampling() override; + void AdjustSampleRate(u32 sample_rate) override; + Samples Read() override; + + /// Called from main thread (retro_run) to read samples from the frontend + /// and store them in the thread-safe buffer for Read() to consume. + void PollMicrophone(); + +private: + struct Impl; + std::unique_ptr impl; +}; + +/// Returns the global LibRetroInput instance, or nullptr if not initialized. +/// This is used by citra_libretro.cpp to poll the microphone from the main thread. +LibRetroInput* GetLibRetroInput(); + +} // namespace AudioCore diff --git a/src/audio_core/libretro_sink.cpp b/src/audio_core/libretro_sink.cpp new file mode 100644 index 000000000..d3bcf0ca5 --- /dev/null +++ b/src/audio_core/libretro_sink.cpp @@ -0,0 +1,27 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/libretro_sink.h" +#include "citra_libretro/environment.h" + +namespace AudioCore { + +LibRetroSink::LibRetroSink(std::string) {} + +LibRetroSink::~LibRetroSink() = default; + +unsigned int LibRetroSink::GetNativeSampleRate() const { + return native_sample_rate; +} + +void LibRetroSink::PushSamples(const void* data, std::size_t num_samples) { + // libretro calls stereo pairs "frames", Azahar calls them "samples" + LibRetro::SubmitAudio(static_cast(data), num_samples); +} + +std::vector ListLibretroSinkDevices() { + return std::vector{"LibRetro"}; +} + +} // namespace AudioCore diff --git a/src/audio_core/libretro_sink.h b/src/audio_core/libretro_sink.h new file mode 100644 index 000000000..b9685fb80 --- /dev/null +++ b/src/audio_core/libretro_sink.h @@ -0,0 +1,33 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include "audio_core/sink.h" + +namespace AudioCore { + +class LibRetroSink final : public Sink { +public: + explicit LibRetroSink(std::string target_device_name); + ~LibRetroSink() override; + + unsigned int GetNativeSampleRate() const override; + + // Not used for immediate submission sinks + void SetCallback(std::function cb) override {}; + + bool ImmediateSubmission() override { + return true; + } + + void PushSamples(const void* data, std::size_t num_samples) override; +}; + +std::vector ListLibretroSinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h index 73a10f567..d0d0ad02a 100644 --- a/src/audio_core/sink.h +++ b/src/audio_core/sink.h @@ -5,7 +5,7 @@ #pragma once #include -#include "common/common_types.h" +#include "audio_types.h" namespace AudioCore { @@ -30,6 +30,23 @@ public: * @param sample_count Number of samples. */ virtual void SetCallback(std::function cb) = 0; + + /** + * Override and set this to true if the sink wants audio data submitted + * immediately rather than requesting audio on demand + * @return true if audio data should be pushed to the sink + */ + virtual bool ImmediateSubmission() { + return false; + } + + /** + * Push audio samples directly to the sink, bypassing the FIFO. + * Only called when ImmediateSubmission() returns true. + * @param data Pointer to stereo PCM16 samples (each sample is L+R pair) + * @param num_samples Number of stereo samples + */ + virtual void PushSamples(const void* data, std::size_t num_samples) {} }; } // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index 961e040b3..ca48b1f23 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -11,6 +11,9 @@ #ifdef HAVE_SDL2 #include "audio_core/sdl2_sink.h" #endif +#ifdef HAVE_LIBRETRO +#include "audio_core/libretro_sink.h" +#endif #ifdef HAVE_CUBEB #include "audio_core/cubeb_sink.h" #endif @@ -23,6 +26,13 @@ namespace AudioCore { namespace { // sink_details is ordered in terms of desirability, with the best choice at the top. constexpr std::array sink_details = { +#ifdef HAVE_LIBRETRO + SinkDetails{SinkType::LibRetro, "libretro", + [](std::string_view device_id) -> std::unique_ptr { + return std::make_unique(std::string(device_id)); + }, + &ListLibretroSinkDevices}, +#endif #ifdef HAVE_CUBEB SinkDetails{SinkType::Cubeb, "Cubeb", [](std::string_view device_id) -> std::unique_ptr { diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h index 2a0d202a7..a3e7a4edb 100644 --- a/src/audio_core/sink_details.h +++ b/src/audio_core/sink_details.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -20,6 +20,7 @@ enum class SinkType : u32 { Cubeb = 2, OpenAL = 3, SDL2 = 4, + LibRetro = 5, }; struct SinkDetails { diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt new file mode 100644 index 000000000..8ea9b7b10 --- /dev/null +++ b/src/citra_libretro/CMakeLists.txt @@ -0,0 +1,97 @@ +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/$) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) + +# Object library for libretro code (can be linked into both shared lib and tests) +add_library(azahar_libretro_common OBJECT + emu_window/libretro_window.cpp + emu_window/libretro_window.h + input/input_factory.cpp + input/input_factory.h + input/mouse_tracker.cpp + input/mouse_tracker.h + $<$: libretro_vk.cpp libretro_vk.h> + environment.cpp + environment.h + core_settings.cpp + core_settings.h) + +target_compile_definitions(azahar_libretro_common PRIVATE HAVE_LIBRETRO) +target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro robin_map) +if(ENABLE_OPENGL) + target_link_libraries(azahar_libretro_common PRIVATE glad) +endif() +if(ENABLE_VULKAN) + target_link_libraries(azahar_libretro_common PRIVATE sirit vulkan-headers vma) +endif() + +add_library(azahar_libretro SHARED + citra_libretro.cpp + citra_libretro.h + $) + +create_target_directory_groups(azahar_libretro) + +target_link_libraries(citra_common PRIVATE libretro) +target_link_libraries(citra_core PRIVATE libretro) +target_link_libraries(video_core PRIVATE libretro) +target_link_libraries(audio_core PRIVATE libretro) +target_link_libraries(input_common PRIVATE libretro) +target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO) +target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO) +target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO) +target_compile_definitions(audio_core PRIVATE HAVE_LIBRETRO) +target_compile_definitions(input_common PRIVATE HAVE_LIBRETRO) + +target_link_libraries(azahar_libretro PRIVATE citra_common citra_core) +target_link_libraries(azahar_libretro PRIVATE boost dds-ktx libretro robin_map) +if(ENABLE_VULKAN) + target_link_libraries(azahar_libretro PRIVATE sirit vulkan-headers vma) +endif() +if(ENABLE_OPENGL) + target_link_libraries(azahar_libretro PRIVATE glad) +endif() +target_link_libraries(azahar_libretro PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) +if(DEFINED LIBRETRO_STATIC) + target_link_libraries(azahar_libretro PRIVATE -static-libstdc++) +endif() + +set_target_properties(azahar_libretro PROPERTIES PREFIX "") +target_compile_definitions(azahar_libretro PRIVATE HAVE_LIBRETRO) + +if(ANDROID) + target_compile_definitions(citra_common PRIVATE HAVE_LIBRETRO_VFS) + target_compile_definitions(citra_core PRIVATE HAVE_LIBRETRO_VFS) + target_compile_definitions(video_core PRIVATE HAVE_LIBRETRO_VFS) + target_compile_definitions(azahar_libretro_common PRIVATE USING_GLES HAVE_LIBRETRO_VFS) + target_compile_definitions(azahar_libretro PRIVATE USING_GLES HAVE_LIBRETRO_VFS) + target_link_libraries(citra_common PRIVATE libretro_common) + target_link_libraries(citra_core PRIVATE libretro_common) + target_link_libraries(video_core PRIVATE libretro_common) + target_link_libraries(azahar_libretro_common PRIVATE libretro_common) + target_link_libraries(azahar_libretro PRIVATE libretro_common) + # Link Android log library for __android_log_print + target_link_libraries(azahar_libretro PRIVATE log) + set_target_properties(azahar_libretro PROPERTIES SUFFIX "_android.so") +endif() + +if(MINGW) + target_link_libraries(azahar_libretro PRIVATE crypt32) +endif() + +if(IOS) + target_compile_definitions(azahar_libretro_common PRIVATE IOS) + target_compile_definitions(azahar_libretro PRIVATE IOS) + target_link_libraries(azahar_libretro PRIVATE "-framework CoreFoundation" "-framework Foundation") +endif() + +if (SSE42_COMPILE_OPTION) + target_compile_definitions(azahar_libretro PRIVATE CITRA_HAS_SSE42) +endif() + +if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "iOS" OR + CMAKE_SYSTEM_NAME STREQUAL "tvOS") + target_link_libraries(azahar_libretro PRIVATE "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/libretro.osx.def") +else() + target_link_libraries(azahar_libretro PRIVATE "-Wl,-Bsymbolic") +endif() diff --git a/src/citra_libretro/citra_libretro.cpp b/src/citra_libretro/citra_libretro.cpp new file mode 100644 index 000000000..1fb73cb23 --- /dev/null +++ b/src/citra_libretro/citra_libretro.cpp @@ -0,0 +1,717 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include +#include + +#ifdef ENABLE_OPENGL +#include "glad/glad.h" +#include "video_core/renderer_opengl/gl_vars.h" +#endif +#include "libretro.h" + +#include "audio_core/libretro_input.h" +#include "audio_core/libretro_sink.h" +#include "video_core/gpu.h" +#ifdef ENABLE_OPENGL +#include "video_core/renderer_opengl/renderer_opengl.h" +#endif +#ifdef ENABLE_VULKAN +#include "citra_libretro/libretro_vk.h" +#endif +#include "video_core/renderer_software/renderer_software.h" +#include "video_core/video_core.h" + +#include "citra_libretro/citra_libretro.h" +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" +#include "citra_libretro/input/input_factory.h" + +#include "common/arch.h" +#if CITRA_ARCH(x86_64) +#include "common/x64/cpu_detect.h" +#endif +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/settings.h" +#include "common/string_util.h" +#include "core/core.h" +#include "core/frontend/applets/default_applets.h" +#include "core/frontend/image_interface.h" +#include "core/hle/kernel/kernel.h" +#include "core/hle/kernel/memory.h" +#include "core/hle/kernel/process.h" +#include "core/loader/loader.h" +#include "core/memory.h" + +#ifdef HAVE_LIBRETRO_VFS +#include +#endif + +class CitraLibRetro { +public: + CitraLibRetro() : log_filter(Common::Log::Level::Debug) {} + + Common::Log::Filter log_filter; + std::unique_ptr emu_window; + bool game_loaded = false; + struct retro_hw_render_callback hw_render{}; +}; + +CitraLibRetro* emu_instance; + +void retro_init() { + emu_instance = new CitraLibRetro(); + Common::Log::LibRetroStart(LibRetro::GetLoggingBackend()); + Common::Log::SetGlobalFilter(emu_instance->log_filter); + + LOG_DEBUG(Frontend, "Initializing core..."); + + // Set up LLE cores + for (const auto& service_module : Service::service_module_map) { + Settings::values.lle_modules.emplace(service_module.name, false); + } + + // Setup default, stub handlers for HLE applets + Frontend::RegisterDefaultApplets(Core::System::GetInstance()); + + // Register generic image interface + Core::System::GetInstance().RegisterImageInterface( + std::make_shared()); + + LibRetro::Input::Init(); +} + +void retro_deinit() { + LOG_DEBUG(Frontend, "Shutting down core..."); + if (Core::System::GetInstance().IsPoweredOn()) { + Core::System::GetInstance().Shutdown(); + } + + LibRetro::Input::Shutdown(); + + delete emu_instance; + + Common::Log::Stop(); +} + +unsigned retro_api_version() { + return RETRO_API_VERSION; +} + +void LibRetro::OnConfigureEnvironment() { + +#ifdef HAVE_LIBRETRO_VFS + struct retro_vfs_interface_info vfs_iface_info{1, nullptr}; + LibRetro::SetVFSCallback(&vfs_iface_info); +#endif + + LibRetro::RegisterCoreOptions(); + + static const struct retro_controller_description controllers[] = { + {"Nintendo 3DS", RETRO_DEVICE_JOYPAD}, + }; + + static const struct retro_controller_info ports[] = { + {controllers, 1}, + {nullptr, 0}, + }; + + LibRetro::SetControllerInfo(ports); +} + +uintptr_t LibRetro::GetFramebuffer() { + return emu_instance->hw_render.get_current_framebuffer(); +} + +/** + * Updates Citra's settings with Libretro's. + */ +static void UpdateSettings() { + LibRetro::ParseCoreOptions(); + + struct retro_input_descriptor desc[] = { + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L2, "ZL"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R2, "ZR"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3, "Home/Swap screens"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3, "Touch Screen Touch"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, + "Circle Pad X"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, + "Circle Pad Y"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_X, + "C-Stick / Pointer X"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, RETRO_DEVICE_ID_ANALOG_Y, + "C-Stick / Pointer Y"}, + {0, 0}, + }; + + LibRetro::SetInputDescriptors(desc); + + Settings::values.current_input_profile.touch_device = "engine:emu_window"; + + // Hardcode buttons to bind to libretro - it is entirely redundant to have + // two methods of rebinding controls. + // Citra: A = RETRO_DEVICE_ID_JOYPAD_A (8) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::A] = + "button:8,joystick:0,engine:libretro"; + // Citra: B = RETRO_DEVICE_ID_JOYPAD_B (0) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::B] = + "button:0,joystick:0,engine:libretro"; + // Citra: X = RETRO_DEVICE_ID_JOYPAD_X (9) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::X] = + "button:9,joystick:0,engine:libretro"; + // Citra: Y = RETRO_DEVICE_ID_JOYPAD_Y (1) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Y] = + "button:1,joystick:0,engine:libretro"; + // Citra: UP = RETRO_DEVICE_ID_JOYPAD_UP (4) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Up] = + "button:4,joystick:0,engine:libretro"; + // Citra: DOWN = RETRO_DEVICE_ID_JOYPAD_DOWN (5) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Down] = + "button:5,joystick:0,engine:libretro"; + // Citra: LEFT = RETRO_DEVICE_ID_JOYPAD_LEFT (6) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Left] = + "button:6,joystick:0,engine:libretro"; + // Citra: RIGHT = RETRO_DEVICE_ID_JOYPAD_RIGHT (7) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Right] = + "button:7,joystick:0,engine:libretro"; + // Citra: L = RETRO_DEVICE_ID_JOYPAD_L (10) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::L] = + "button:10,joystick:0,engine:libretro"; + // Citra: R = RETRO_DEVICE_ID_JOYPAD_R (11) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::R] = + "button:11,joystick:0,engine:libretro"; + // Citra: START = RETRO_DEVICE_ID_JOYPAD_START (3) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Start] = + "button:3,joystick:0,engine:libretro"; + // Citra: SELECT = RETRO_DEVICE_ID_JOYPAD_SELECT (2) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Select] = + "button:2,joystick:0,engine:libretro"; + // Citra: ZL = RETRO_DEVICE_ID_JOYPAD_L2 (12) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZL] = + "button:12,joystick:0,engine:libretro"; + // Citra: ZR = RETRO_DEVICE_ID_JOYPAD_R2 (13) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::ZR] = + "button:13,joystick:0,engine:libretro"; + // Citra: HOME = RETRO_DEVICE_ID_JOYPAD_L3 (as per above bindings) (14) + Settings::values.current_input_profile.buttons[Settings::NativeButton::Values::Home] = + "button:14,joystick:0,engine:libretro"; + + // Circle Pad + Settings::values.current_input_profile.analogs[0] = "axis:0,joystick:0,engine:libretro"; + // C-Stick + if (LibRetro::settings.analog_function != LibRetro::CStickFunction::Touchscreen) { + Settings::values.current_input_profile.analogs[1] = "axis:1,joystick:0,engine:libretro"; + } else { + Settings::values.current_input_profile.analogs[1] = ""; + } + + if (!emu_instance->emu_window) { + emu_instance->emu_window = std::make_unique(); + } + + // Update the framebuffer sizing. + emu_instance->emu_window->UpdateLayout(); + + Core::System::GetInstance().ApplySettings(); +} + +/** + * libretro callback; Called every game tick. + */ +void retro_run() { + // Check to see if we actually have any config updates to process. + if (LibRetro::HasUpdatedConfig()) { + LibRetro::ParseCoreOptions(); + Core::System::GetInstance().ApplySettings(); + emu_instance->emu_window->UpdateLayout(); + } + + // Poll microphone input from the frontend and buffer it for the emulator + // This must be done from the main thread as LibRetro's mic interface is not thread-safe + if (auto* mic_input = AudioCore::GetLibRetroInput()) { + mic_input->PollMicrophone(); + } + + // Check if the screen swap button is pressed + static bool screen_swap_btn_state = false; + static bool screen_swap_toggled = false; + bool screen_swap_btn = + !!LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L3); + if (screen_swap_btn != screen_swap_btn_state) { + if (LibRetro::settings.toggle_swap_screen) { + if (!screen_swap_btn_state) + screen_swap_toggled = !screen_swap_toggled; + + if (screen_swap_toggled) + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom"; + else + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom"; + } else { + if (screen_swap_btn) + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") != "Bottom"; + else + Settings::values.swap_screen = + LibRetro::FetchVariable("citra_swap_screen", "Top") == "Bottom"; + } + + Core::System::GetInstance().ApplySettings(); + + // Update the framebuffer sizing. + emu_instance->emu_window->UpdateLayout(); + + screen_swap_btn_state = screen_swap_btn; + } + +#ifdef ENABLE_OPENGL + if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) { + // We can't assume that the frontend has been nice and preserved all OpenGL settings. Reset. + auto last_state = OpenGL::OpenGLState::GetCurState(); + ResetGLState(); + last_state.Apply(); + } +#endif + + while (!emu_instance->emu_window->HasSubmittedFrame()) { + auto result = Core::System::GetInstance().RunLoop(); + + if (result != Core::System::ResultStatus::Success) { + std::string errorContent = Core::System::GetInstance().GetStatusDetails(); + std::string msg; + + switch (result) { + case Core::System::ResultStatus::ErrorSystemFiles: + msg = "Azahar was unable to locate a 3DS system archive: " + errorContent; + break; + default: + msg = "Fatal Error encountered (" + std::to_string(static_cast(result)) + + "): " + errorContent; + break; + } + + LibRetro::DisplayMessage(msg.c_str()); + } + } +} + +static void setup_memory_maps() { + auto process = Core::System::GetInstance().Kernel().GetCurrentProcess(); + if (!process) + return; + + std::vector descs; + + for (const auto& [addr, vma] : process->vm_manager.vma_map) { + if (vma.type != Kernel::VMAType::BackingMemory) + continue; + if (vma.size == 0 || !vma.backing_memory) + continue; + + // Only expose the well-known user-accessible memory regions + uint64_t flags = 0; + if (vma.base >= Memory::HEAP_VADDR && vma.base < Memory::HEAP_VADDR_END) { + flags = RETRO_MEMDESC_SYSTEM_RAM; + } else if (vma.base >= Memory::LINEAR_HEAP_VADDR && + vma.base < Memory::LINEAR_HEAP_VADDR_END) { + flags = RETRO_MEMDESC_SYSTEM_RAM; + } else if (vma.base >= Memory::NEW_LINEAR_HEAP_VADDR && + vma.base < Memory::NEW_LINEAR_HEAP_VADDR_END) { + flags = RETRO_MEMDESC_SYSTEM_RAM; + } else if (vma.base >= Memory::VRAM_VADDR && vma.base < Memory::VRAM_VADDR_END) { + flags = RETRO_MEMDESC_VIDEO_RAM; + } else { + continue; + } + + retro_memory_descriptor desc = {}; + desc.flags = flags; + desc.ptr = const_cast(vma.backing_memory.GetPtr()); + desc.start = vma.base; + desc.len = vma.size; + + // select=0 requires power-of-2 len AND start aligned to len. + // When that doesn't hold, compute a select mask instead. + bool need_select = (vma.size & (vma.size - 1)) != 0; + if (!need_select && (vma.base & (vma.size - 1)) != 0) + need_select = true; + + if (need_select) { + uint64_t np2 = 1; + while (np2 < vma.size) + np2 <<= 1; + if (vma.base & (np2 - 1)) { + LOG_WARNING(Frontend, "VMA at 0x{:08X} size 0x{:X} not aligned, skipping", vma.base, + vma.size); + continue; + } + desc.select = ~(np2 - 1); + } + + descs.push_back(desc); + } + + if (!descs.empty()) { + retro_memory_map map = {descs.data(), static_cast(descs.size())}; + LibRetro::SetMemoryMaps(&map); + } +} + +static bool do_load_game() { + const Core::System::ResultStatus load_result{ + Core::System::GetInstance().Load(*emu_instance->emu_window, LibRetro::settings.file_path)}; + + switch (load_result) { + case Core::System::ResultStatus::Success: + break; // Expected case + case Core::System::ResultStatus::ErrorGetLoader: + LibRetro::DisplayMessage("Failed to obtain loader for specified ROM!"); + return false; + case Core::System::ResultStatus::ErrorLoader: + LibRetro::DisplayMessage("Failed to load ROM!"); + return false; + case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: + LibRetro::DisplayMessage("The game that you are trying to load must be decrypted before " + "being used with Azahar."); + return false; + case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: + LibRetro::DisplayMessage("Error while loading ROM: The ROM format is not supported."); + return false; + case Core::System::ResultStatus::ErrorLoader_ErrorGbaTitle: + LibRetro::DisplayMessage( + "Error loading the specified application as it is GBA Virtual Console"); + return false; + case Core::System::ResultStatus::ErrorNotInitialized: + LibRetro::DisplayMessage("CPUCore not initialized"); + return false; + case Core::System::ResultStatus::ErrorSystemMode: + LibRetro::DisplayMessage("Failed to determine system mode!"); + return false; + default: + LibRetro::DisplayMessage( + ("Unknown error: " + std::to_string(static_cast(load_result))).c_str()); + return false; + } + + u64 program_id{}; + Core::System::GetInstance().GetAppLoader().ReadProgramId(program_id); + Core::System::GetInstance().GPU().ApplyPerProgramSettings(program_id); + + if (Settings::values.use_disk_shader_cache) { + Core::System::GetInstance().GPU().Renderer().Rasterizer()->LoadDefaultDiskResources( + false, nullptr); + } + + setup_memory_maps(); + + return true; +} + +#ifdef ENABLE_OPENGL +static void* load_opengl_func(const char* name) { + return (void*)emu_instance->hw_render.get_proc_address(name); +} +#endif + +static void context_reset() { + LOG_DEBUG(Frontend, "context_reset"); + + switch (Settings::values.graphics_api.GetValue()) { +#ifdef ENABLE_OPENGL + case Settings::GraphicsAPI::OpenGL: +#if defined(USING_GLES) + Settings::values.use_gles = true; + // Set the global GLES flag immediately to ensure any shader compilation + // that happens before the Driver is created uses the correct version + OpenGL::GLES = true; +#else + Settings::values.use_gles = false; + OpenGL::GLES = false; +#endif + // Check to see if the frontend provides us with OpenGL symbols + if (emu_instance->hw_render.get_proc_address != nullptr) { + bool loaded = Settings::values.use_gles + ? gladLoadGLES2Loader((GLADloadproc)load_opengl_func) + : gladLoadGLLoader((GLADloadproc)load_opengl_func); + + if (!loaded) { + LOG_CRITICAL(Frontend, "Glad failed to load (frontend-provided symbols)!"); + return; + } + } else { + // Else, try to load them on our own + if (!gladLoadGL()) { + LOG_CRITICAL(Frontend, "Glad failed to load (internal symbols)!"); + return; + } + } + break; +#endif +#ifdef ENABLE_VULKAN + case Settings::GraphicsAPI::Vulkan: + LibRetro::VulkanResetContext(); + break; +#endif + default: + // software renderer never gets here + break; + } + + emu_instance->emu_window->CreateContext(); + + if (!emu_instance->game_loaded) { + emu_instance->game_loaded = do_load_game(); + } else { + // Game is already loaded, just recreate the renderer for the new GL context + if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) { + Core::System::GetInstance().GPU().RecreateRenderer(*emu_instance->emu_window, nullptr); + } + } +} + +static void context_destroy() { + LOG_DEBUG(Frontend, "context_destroy"); + if (emu_instance->game_loaded && + Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::OpenGL) { + // Release the renderer's OpenGL resources + Core::System::GetInstance().GPU().ReleaseRenderer(); + } + emu_instance->emu_window->DestroyContext(); +} + +void retro_reset() { + LOG_DEBUG(Frontend, "retro_reset"); + Core::System::GetInstance().Shutdown(); + emu_instance->game_loaded = do_load_game(); +} + +/** + * libretro callback; Called when a game is to be loaded. + */ +bool retro_load_game(const struct retro_game_info* info) { + LOG_INFO(Frontend, "Starting Azahar RetroArch game..."); + +#if CITRA_ARCH(x86_64) && CITRA_HAS_SSE42 + if (!Common::GetCPUCaps().sse4_2) { + LOG_CRITICAL(Frontend, "This CPU does not support SSE4.2, which is required by this build"); + LibRetro::DisplayMessage( + "This CPU does not support SSE4.2, which is required by this build"); + return false; + } +#endif + + UpdateSettings(); + + // If using HW rendering, don't actually load the game here. azahar wants + // the graphics context ready and available before calling System::Load. + LibRetro::settings.file_path = info->path; + + if (!LibRetro::SetPixelFormat(RETRO_PIXEL_FORMAT_XRGB8888)) { + LibRetro::DisplayMessage("XRGB8888 is not supported."); + return false; + } + + emu_instance->emu_window->UpdateLayout(); + + switch (Settings::values.graphics_api.GetValue()) { + case Settings::GraphicsAPI::OpenGL: +#ifdef ENABLE_OPENGL + LOG_INFO(Frontend, "Using OpenGL hw renderer"); + LibRetro::SetHWSharedContext(); +#if defined(USING_GLES) + emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGLES3; + emu_instance->hw_render.version_major = 3; + emu_instance->hw_render.version_minor = 2; +#else + emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_OPENGL_CORE; + emu_instance->hw_render.version_major = 4; + emu_instance->hw_render.version_minor = 3; +#endif + emu_instance->hw_render.context_reset = context_reset; + emu_instance->hw_render.context_destroy = context_destroy; + emu_instance->hw_render.cache_context = false; + emu_instance->hw_render.bottom_left_origin = true; + if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) { + LibRetro::DisplayMessage("Failed to set HW renderer"); + return false; + } +#endif + break; + case Settings::GraphicsAPI::Vulkan: +#ifdef ENABLE_VULKAN + LOG_INFO(Frontend, "Using Vulkan hw renderer"); + emu_instance->hw_render.context_type = RETRO_HW_CONTEXT_VULKAN; + emu_instance->hw_render.version_major = VK_MAKE_VERSION(1, 1, 0); + emu_instance->hw_render.version_minor = 0; + emu_instance->hw_render.context_reset = context_reset; + emu_instance->hw_render.context_destroy = context_destroy; + emu_instance->hw_render.cache_context = true; + if (!LibRetro::SetHWRenderer(&emu_instance->hw_render)) { + LibRetro::DisplayMessage("Failed to set HW renderer"); + return false; + } + + // Set up Vulkan context negotiation interface + static const struct retro_hw_render_context_negotiation_interface_vulkan vk_negotiation = { + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN, + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN_VERSION, + LibRetro::GetVulkanApplicationInfo, + LibRetro::CreateVulkanDevice, + nullptr, // destroy_device - not needed (frontend owns the device) + }; + LibRetro::SetHWRenderContextNegotiationInterface((void**)&vk_negotiation); +#endif + break; + case Settings::GraphicsAPI::Software: + emu_instance->game_loaded = do_load_game(); + if (!emu_instance->game_loaded) + return false; + break; + } + + uint64_t quirks = + RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE | RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE; + LibRetro::SetSerializationQuirks(quirks); + + return true; +} + +void retro_unload_game() { + LOG_DEBUG(Frontend, "Unloading game..."); + Core::System::GetInstance().Shutdown(); +} + +unsigned retro_get_region() { + return RETRO_REGION_NTSC; +} + +bool retro_load_game_special(unsigned game_type, const struct retro_game_info* info, + size_t num_info) { + return retro_load_game(info); +} + +/// Drain any pending async kernel operations by running the emulation loop. +/// +/// Savestates are unsafe to create while RunAsync operations (file I/O, network, etc.) +/// are in flight. The Qt frontend handles this by deferring serialization inside +/// System::RunLoop(): it sets a request flag via SendSignal(Signal::Save), and RunLoop +/// only performs the save when !kernel->AreAsyncOperationsPending() (see core.cpp). +/// +/// The Qt frontend needs that indirection because its UI and emulation run on separate +/// threads. In libretro, the frontend calls API entry points (retro_run, retro_serialize, +/// etc.) sequentially, so we can call RunLoop() directly from here to drain pending ops, +/// then call SaveStateBuffer()/LoadStateBuffer() ourselves. +/// +/// Note: RunLoop() can itself start new async operations (CPU executes HLE service calls), +/// so the pending count may not decrease monotonically. In practice games reach quiescent +/// points between frames; the 5-second timeout (matching RunLoop's existing handler) +/// covers the pathological case. +static bool DrainAsyncOperations(Core::System& system) { + if (!system.KernelRunning() || !system.Kernel().AreAsyncOperationsPending()) { + return true; + } + + emu_instance->emu_window->suppressPresentation = true; + auto start = std::chrono::steady_clock::now(); + + while (system.Kernel().AreAsyncOperationsPending()) { + if (std::chrono::steady_clock::now() - start > std::chrono::seconds(5)) { + LOG_ERROR(Frontend, "Timed out waiting for async operations to complete"); + emu_instance->emu_window->suppressPresentation = false; + return false; + } + auto result = system.RunLoop(); + if (result != Core::System::ResultStatus::Success) { + emu_instance->emu_window->suppressPresentation = false; + return false; + } + } + + emu_instance->emu_window->suppressPresentation = false; + return true; +} + +std::optional> savestate = {}; + +size_t retro_serialize_size() { + auto& system = Core::System::GetInstance(); + if (!system.IsPoweredOn()) + return 0; + + if (!DrainAsyncOperations(system)) { + savestate.reset(); + return 0; + } + + try { + savestate = system.SaveStateBuffer(); + return savestate->size(); + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Error saving state: {}", e.what()); + savestate.reset(); + return 0; + } +} + +bool retro_serialize(void* data, size_t size) { + if (!savestate.has_value()) + return false; + if (size < savestate->size()) + return false; + memcpy(data, savestate->data(), savestate->size()); + savestate.reset(); + return true; +} + +bool retro_unserialize(const void* data, size_t size) { + auto& system = Core::System::GetInstance(); + if (!system.IsPoweredOn()) + return false; + + if (!DrainAsyncOperations(system)) { + return false; + } + + std::vector buffer(static_cast(data), static_cast(data) + size); + try { + return system.LoadStateBuffer(std::move(buffer)); + } catch (const std::exception& e) { + LOG_ERROR(Frontend, "Error loading state: {}", e.what()); + return false; + } +} + +void* retro_get_memory_data(unsigned id) { + // Memory is exposed via RETRO_ENVIRONMENT_SET_MEMORY_MAPS instead, + // using virtual addresses for stable cheat/achievement support. + return NULL; +} + +size_t retro_get_memory_size(unsigned id) { + return 0; +} + +void retro_cheat_reset() {} + +void retro_cheat_set(unsigned index, bool enabled, const char* code) {} diff --git a/src/citra_libretro/citra_libretro.h b/src/citra_libretro/citra_libretro.h new file mode 100644 index 000000000..0d8a08f89 --- /dev/null +++ b/src/citra_libretro/citra_libretro.h @@ -0,0 +1,10 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/core.h" +#include "emu_window/libretro_window.h" + +namespace LibRetro {} // namespace LibRetro diff --git a/src/citra_libretro/core_settings.cpp b/src/citra_libretro/core_settings.cpp new file mode 100644 index 000000000..86e7098f6 --- /dev/null +++ b/src/citra_libretro/core_settings.cpp @@ -0,0 +1,1014 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" + +#include "common/file_util.h" +#include "common/settings.h" +#include "core/hle/service/cfg/cfg.h" + +namespace LibRetro { + +CoreSettings settings = {}; + +namespace config { + +static constexpr const char* enabled = "enabled"; +static constexpr const char* disabled = "disabled"; + +namespace category { +static constexpr const char* cpu = "cpu"; +static constexpr const char* system = "system"; +static constexpr const char* audio = "audio"; +static constexpr const char* graphics = "graphics"; +static constexpr const char* layout = "layout"; +static constexpr const char* storage = "storage"; +static constexpr const char* input = "input"; +} // namespace category + +namespace cpu { +static constexpr const char* use_cpu_jit = "citra_use_cpu_jit"; +static constexpr const char* cpu_clock_percentage = "citra_cpu_scale"; +} // namespace cpu + +namespace system { +static constexpr const char* is_new_3ds = "citra_is_new_3ds"; +static constexpr const char* region = "citra_region_value"; +static constexpr const char* language = "citra_language"; +} // namespace system + +namespace audio { +static constexpr const char* audio_emulation = "citra_audio_emulation"; +static constexpr const char* input_type = "citra_input_type"; +} // namespace audio + +namespace graphics { +static constexpr const char* graphics_api = "citra_graphics_api"; +static constexpr const char* use_hw_shader = "citra_use_hw_shaders"; +static constexpr const char* use_shader_jit = "citra_use_shader_jit"; +static constexpr const char* shaders_accurate_mul = "citra_use_acc_mul"; +static constexpr const char* use_disk_shader_cache = "citra_use_hw_shader_cache"; +static constexpr const char* resolution_factor = "citra_resolution_factor"; +static constexpr const char* texture_filter = "citra_texture_filter"; +static constexpr const char* texture_sampling = "citra_texture_sampling"; +static constexpr const char* custom_textures = "citra_custom_textures"; +static constexpr const char* dump_textures = "citra_dump_textures"; +} // namespace graphics + +namespace layout { +static constexpr const char* layout_option = "citra_layout_option"; +static constexpr const char* swap_screen = "citra_swap_screen"; +static constexpr const char* toggle_swap_screen = "citra_swap_screen_mode"; +} // namespace layout + +namespace storage { +static constexpr const char* use_virtual_sd = "citra_use_virtual_sd"; +static constexpr const char* use_libretro_save_path = "citra_use_libretro_save_path"; +} // namespace storage + +namespace input { +static constexpr const char* analog_function = "citra_analog_function"; +static constexpr const char* deadzone = "citra_deadzone"; +static constexpr const char* mouse_touchscreen = "citra_mouse_touchscreen"; +static constexpr const char* touch_touchscreen = "citra_touch_touchscreen"; +static constexpr const char* render_touchscreen = "citra_render_touchscreen"; +static constexpr const char* motion_enabled = "citra_motion_enabled"; +static constexpr const char* motion_sensitivity = "citra_motion_sensitivity"; +} // namespace input + +} // namespace config + +// clang-format off +static constexpr retro_core_option_v2_category option_categories[] = { + { + config::category::cpu, + "CPU", + "Settings related to CPU emulation performance and accuracy." + }, + { + config::category::system, + "System", + "Nintendo 3DS system configuration and region settings." + }, + { + config::category::audio, + "Audio", + "Audio emulation and microphone settings." + }, + { + config::category::graphics, + "Graphics", + "Graphics API, rendering, and visual enhancement settings." + }, + { + config::category::layout, + "Layout", + "Screen layout and display positioning options." + }, + { + config::category::storage, + "Storage", + "Save data and virtual SD card settings." + }, + { + config::category::input, + "Input", + "Controller and touchscreen input configuration." + }, + { nullptr, nullptr, nullptr } +}; + +// ============================================================================ +// Option Definitions +// ============================================================================ + +static constexpr retro_core_option_v2_definition option_definitions[] = { + // CPU Category + { + config::cpu::use_cpu_jit, + "Enable CPU JIT", + "CPU JIT", + "Enable Just-In-Time compilation for ARM CPU emulation. " + "Significantly improves performance but may reduce accuracy. " + "Restart required.", + nullptr, + config::category::cpu, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::cpu::cpu_clock_percentage, + "CPU Clock Speed", + "CPU Clock Speed", + "Adjust the emulated 3DS CPU clock speed as a percentage of normal speed. " + "Higher values may improve performance in some games but can cause issues. " + "Lower values can help with games that run too fast.", + nullptr, + config::category::cpu, + { + { "25", "25%" }, { "50", "50%" }, { "75", "75%" }, { "100", "100% (Default)" }, + { "125", "125%" }, { "150", "150%" }, { "175", "175%" }, { "200", "200%" }, + { "225", "225%" }, { "250", "250%" }, { "275", "275%" }, { "300", "300%" }, + { "325", "325%" }, { "350", "350%" }, { "375", "375%" }, { "400", "400%" }, + { nullptr, nullptr } + }, + "100" + }, + + // System Category + { + config::system::is_new_3ds, + "3DS System Model", + "System Model", + "Select whether to emulate the original 3DS or New 3DS. " + "New 3DS has additional CPU power and memory, required for some games. " + "Restart required.", + nullptr, + config::category::system, + { + { "Old 3DS", "Original 3DS" }, + { "New 3DS", "New 3DS" }, + { nullptr, nullptr } + }, + "Old 3DS" + }, + { + config::system::region, + "3DS System Region", + "System Region", + "Set the 3DS system region. Auto-select will choose based on the game. " + "Some games are region-locked and require matching regions.", + nullptr, + config::category::system, + { + { "Auto", "Auto" }, + { "Japan", "Japan" }, + { "USA", "USA" }, + { "Europe", "Europe" }, + { "Australia", "Australia" }, + { "China", "China" }, + { "Korea", "Korea" }, + { "Taiwan", "Taiwan" }, + { nullptr, nullptr } + }, + "Auto" + }, + { + config::system::language, + "3DS System Language", + "System Language", + "Set the system language for the emulated 3DS. " + "This affects in-game text language when supported.", + nullptr, + config::category::system, + { + { "English", "English" }, + { "Japanese", "Japanese" }, + { "French", "French" }, + { "Spanish", "Spanish" }, + { "German", "German" }, + { "Italian", "Italian" }, + { "Dutch", "Dutch" }, + { "Portuguese", "Portuguese" }, + { "Russian", "Russian" }, + { "Korean", "Korean" }, + { "Traditional Chinese", "Traditional Chinese" }, + { "Simplified Chinese", "Simplified Chinese" }, + { nullptr, nullptr } + }, + "english" + }, + + // Audio Category + { + config::audio::audio_emulation, + "Audio Emulation", + "Audio Emulation", + "Select audio emulation method. HLE is faster, LLE is more accurate.", + nullptr, + config::category::audio, + { + { "hle", "HLE (Fast)" }, + { "lle", "LLE (Accurate)" }, + { "lle_multithread", "LLE Multithreaded" }, + { nullptr, nullptr } + }, + "hle" + }, + { + config::audio::input_type, + "Microphone Input Type", + "Microphone Input", + "Select how microphone input is handled for games that support it.", + nullptr, + config::category::audio, + { + { "auto", "Auto" }, + { "none", "None" }, + { "static_noise", "Static Noise" }, + { "frontend", "Frontend" }, + { nullptr, nullptr } + }, + "auto" + }, + + // Graphics Category + { + config::graphics::graphics_api, + "Graphics API", + "Graphics API", + "Select the graphics rendering API. Auto will choose the best available option. " + "Restart required.", + nullptr, + config::category::graphics, + { + { "Auto", "Auto" }, +#ifdef ENABLE_VULKAN + { "Vulkan", "Vulkan" }, +#endif +#ifdef ENABLE_OPENGL + { "OpenGL", "OpenGL" }, +#endif + { "Software", "Software" }, + { nullptr, nullptr } + }, + "auto" + }, + { + config::graphics::use_hw_shader, + "Enable Hardware Shaders", + "Hardware Shaders", + "Use GPU hardware to accelerate shader processing. " + "Significantly improves performance but may reduce accuracy.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::use_shader_jit, + "Enable Shader JIT", + "Shader JIT", + "Use Just-In-Time compilation for shaders. " + "Improves performance but may cause graphical issues in some games.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::shaders_accurate_mul, + "Accurate Shader Multiplication", + "Accurate Multiplication", + "Use accurate multiplication in shaders. " + "More accurate but can reduce performance. Only works with hardware shaders.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::use_disk_shader_cache, + "Hardware Shader Cache", + "Shader Cache", + "Save compiled shaders to disk to reduce loading times on subsequent runs.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::graphics::resolution_factor, + "Internal Resolution", + "Internal Resolution", + "Render the 3DS screens at a higher resolution. " + "Higher values improve visual quality but significantly impact performance.", + nullptr, + config::category::graphics, + { + { "1", "1x (Native 400x240)" }, + { "2", "2x (800x480)" }, + { "3", "3x (1200x720)" }, + { "4", "4x (1600x960)" }, + { "5", "5x (2000x1200)" }, + { "6", "6x (2400x1440)" }, + { "7", "7x (2800x1680)" }, + { "8", "8x (3200x1920)" }, + { "9", "9x (3600x2160)" }, + { "10", "10x (4000x2400)" }, + { nullptr, nullptr } + }, + "1" + }, + { + config::graphics::texture_filter, + "Texture Filter", + "Texture Filter", + "Apply texture filtering to enhance visual quality. " + "Some filters may significantly impact performance.", + nullptr, + config::category::graphics, + { + { "none", "None" }, + { "Anime4K Ultrafast", "Anime4K Ultrafast" }, + { "Bicubic", "Bicubic" }, + { "ScaleForce", "ScaleForce" }, + { "xBRZ", "xBRZ" }, + { "MMPX", "MMPX" }, + { nullptr, nullptr } + }, + "none" + }, + { + config::graphics::texture_sampling, + "Texture Sampling", + "Texture Sampling", + "Control how textures are sampled and filtered.", + nullptr, + config::category::graphics, + { + { "GameControlled", "Game Controlled" }, + { "NearestNeighbor", "Nearest Neighbor" }, + { "Linear", "Linear" }, + { nullptr, nullptr } + }, + "GameControlled" + }, + { + config::graphics::custom_textures, + "Custom Textures", + "Custom Textures", + "Enable loading of custom texture packs to replace original game textures.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::graphics::dump_textures, + "Dump Game Textures", + "Dump Textures", + "Save original game textures to disk for creating custom texture packs. " + "May impact performance.", + nullptr, + config::category::graphics, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + + // Layout Category + { + config::layout::layout_option, + "Screen Layout", + "Screen Layout", + "Choose how the 3DS screens are arranged in the display.", + nullptr, + config::category::layout, + { + { "default", "Default Top-Bottom" }, + { "single_screen", "Single Screen Only" }, + { "large_screen", "Large Screen, Small Screen" }, + { "side_by_side", "Side by Side" }, + { nullptr, nullptr } + }, + "default" + }, + { + config::layout::swap_screen, + "Prominent 3DS Screen", + "Prominent Screen", + "Choose which screen is displayed prominently in single screen or large screen layouts.", + nullptr, + config::category::layout, + { + { "Top", "Top Screen" }, + { "Bottom", "Bottom Screen" }, + { nullptr, nullptr } + }, + "Top" + }, + { + config::layout::toggle_swap_screen, + "Screen Swap Mode", + "Swap Mode", + "How screen swapping behaves when using the screen swap hotkey.", + nullptr, + config::category::layout, + { + { "Toggle", "Toggle" }, + { "Hold", "Hold" }, + { nullptr, nullptr } + }, + "Toggle" + }, + + // Storage Category + { + config::storage::use_virtual_sd, + "Enable Virtual SD Card", + "Virtual SD Card", + "Enable virtual SD card support for homebrew and some commercial games.", + nullptr, + config::category::storage, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::storage::use_libretro_save_path, + "Save Data Location", + "Save Location", + "Choose where save data and system files are stored.", + nullptr, + config::category::storage, + { + { "LibRetro Default", "LibRetro Default" }, + { "Azahar Default", "Azahar Default" }, + { nullptr, nullptr } + }, + "LibRetro Default" + }, + + // Input Category + { + config::input::analog_function, + "Right Analog Function", + "Right Analog Function", + "Configure what the right analog stick controls.", + nullptr, + config::category::input, + { + { "c_stick_and_touchscreen", "C-Stick and Touchscreen Pointer" }, + { "touchscreen_pointer", "Touchscreen Pointer" }, + { "c_stick", "C-Stick" }, + { nullptr, nullptr } + }, + "c_stick_and_touchscreen" + }, + { + config::input::deadzone, + "Analog Deadzone", + "Analog Deadzone", + "Set the deadzone percentage for analog input to reduce drift.", + nullptr, + config::category::input, + { + { "0", "0%" }, { "5", "5%" }, { "10", "10%" }, { "15", "15%" }, + { "20", "20%" }, { "25", "25%" }, { "30", "30%" }, { "35", "35%" }, + { nullptr, nullptr } + }, + "15" + }, + { + config::input::mouse_touchscreen, + "Mouse Touchscreen Support", + "Mouse Touchscreen", + "Enable mouse input for touchscreen interactions.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::input::touch_touchscreen, + "Touch Device Support", + "Touch Support", + "Enable touch device input for touchscreen interactions.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::input::render_touchscreen, + "Show Touch Interactions", + "Show Touch", + "Visually indicate touchscreen interactions on screen.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::disabled + }, + { + config::input::motion_enabled, + "Gyroscope/Accelerometer Support", + "Motion Support", + "Enable gyroscope and accelerometer input for games that support motion controls.", + nullptr, + config::category::input, + { + { config::enabled, "Enabled" }, + { config::disabled, "Disabled" }, + { nullptr, nullptr } + }, + config::enabled + }, + { + config::input::motion_sensitivity, + "Motion Sensitivity", + "Motion Sensitivity", + "Adjust sensitivity of motion controls (gyroscope/accelerometer).", + nullptr, + config::category::input, + { + { "0.1", "10%" }, + { "0.25", "25%" }, + { "0.5", "50%" }, + { "0.75", "75%" }, + { "1.0", "100%" }, + { "1.25", "125%" }, + { "1.5", "150%" }, + { "2.0", "200%" }, + { nullptr, nullptr } + }, + "1.0" + }, + + // Terminator + { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, { { nullptr, nullptr } }, nullptr } +}; +// clang-format on + +static const retro_core_options_v2 options_v2 = { + const_cast(option_categories), + const_cast(option_definitions)}; + +void RegisterCoreOptions(void) { + // Try v2 first, then fallback to v1 and v0 if needed + unsigned version = 0; + if (!LibRetro::GetCoreOptionsVersion(&version)) { + version = 0; + } + + LOG_INFO(Frontend, "Frontend reports core options version: {}", version); + + if (version >= 2) { + if (LibRetro::SetCoreOptionsV2(&options_v2)) { + LOG_INFO(Frontend, "V2 core options set successfully"); + return; + } + } + + LOG_WARNING(Frontend, "V2 core options not supported, trying V1"); + + // Count number of options + unsigned num_options = 0; + while (option_definitions[num_options].key != nullptr) { + num_options++; + } + + if (version >= 1) { + // Create V1 options array + std::vector options_v1(num_options + 1); + + // Copy parameters from V2 to V1 + for (unsigned i = 0; i < num_options; i++) { + const auto& v2_option = option_definitions[i]; + auto& v1_option = options_v1[i]; + + v1_option.key = v2_option.key; + v1_option.desc = v2_option.desc; + v1_option.info = v2_option.info; + v1_option.default_value = v2_option.default_value; + std::memcpy(v1_option.values, v2_option.values, sizeof(v1_option.values)); + } + + // Null terminator + std::memset(&options_v1.back(), 0, sizeof(retro_core_option_definition)); + + if (LibRetro::SetCoreOptionsV1(options_v1.data())) { + LOG_INFO(Frontend, "V1 core options set successfully"); + return; + } + } + + LOG_WARNING(Frontend, "V1 core options not supported, trying V0"); + + // Create V0 variables array + std::vector variables(num_options + 1); + std::vector values_buffer(num_options); + + for (unsigned i = 0; i < num_options; i++) { + const auto& option = option_definitions[i]; + std::string desc = option.desc ? option.desc : ""; + std::string default_value = option.default_value ? option.default_value : ""; + + values_buffer[i] = ""; + + if (!desc.empty()) { + // Count number of values + size_t num_values = 0; + size_t default_index = 0; + + while (option.values[num_values].value != nullptr) { + if (!default_value.empty() && + std::string(option.values[num_values].value) == default_value) { + default_index = num_values; + } + num_values++; + } + + // Build values string: "Description; default_value|other_value1|other_value2" + if (num_values > 0) { + values_buffer[i] = desc + "; " + option.values[default_index].value; + + // Add remaining values + for (size_t j = 0; j < num_values; j++) { + if (j != default_index) { + values_buffer[i] += "|" + std::string(option.values[j].value); + } + } + } + } + + variables[i].key = option.key; + variables[i].value = values_buffer[i].c_str(); + } + + // Null terminator + std::memset(&variables.back(), 0, sizeof(retro_variable)); + + // Set V0 variables + if (LibRetro::SetVariables(variables.data())) { + LOG_INFO(Frontend, "V0 core options set successfully"); + } else { + LOG_ERROR(Frontend, "Failed to set core options with any version"); + } +} + +static void ParseCpuOptions(void) { + Settings::values.use_cpu_jit = + LibRetro::FetchVariable(config::cpu::use_cpu_jit, config::enabled) == config::enabled; +#if defined(IOS) + if (!LibRetro::CanUseJIT()) + Settings::values.use_cpu_jit = false; +#endif + + auto cpu_clock = LibRetro::FetchVariable(config::cpu::cpu_clock_percentage, "100"); + Settings::values.cpu_clock_percentage = std::stoi(cpu_clock); +} + +static int GetRegionValue(const std::string& name) { + if (name == "Japan") + return 0; + if (name == "USA") + return 1; + if (name == "Europe") + return 2; + if (name == "Australia") + return 3; + if (name == "China") + return 4; + if (name == "Korea") + return 5; + if (name == "Taiwan") + return 6; + return -1; // Auto +} + +static Service::CFG::SystemLanguage GetLanguageValue(const std::string& name) { + if (name == "Japanese") + return Service::CFG::LANGUAGE_JP; + if (name == "French") + return Service::CFG::LANGUAGE_FR; + if (name == "Spanish") + return Service::CFG::LANGUAGE_ES; + if (name == "German") + return Service::CFG::LANGUAGE_DE; + if (name == "Italian") + return Service::CFG::LANGUAGE_IT; + if (name == "Dutch") + return Service::CFG::LANGUAGE_NL; + if (name == "Portuguese") + return Service::CFG::LANGUAGE_PT; + if (name == "Russian") + return Service::CFG::LANGUAGE_RU; + if (name == "Korean") + return Service::CFG::LANGUAGE_KO; + if (name == "Traditional Chinese") + return Service::CFG::LANGUAGE_TW; + if (name == "Simplified Chinese") + return Service::CFG::LANGUAGE_ZH; + return Service::CFG::LANGUAGE_EN; // English default +} + +static void ParseSystemOptions(void) { + Settings::values.is_new_3ds = + LibRetro::FetchVariable(config::system::is_new_3ds, "Old 3DS") == "New 3DS"; + + Settings::values.region_value = + GetRegionValue(LibRetro::FetchVariable("citra_region_value", "Auto")); + + LibRetro::settings.language_value = + GetLanguageValue(LibRetro::FetchVariable(config::system::language, "English")); +} + +static Settings::AudioEmulation GetAudioEmulation(const std::string& name) { + if (name == "lle") + return Settings::AudioEmulation::LLE; + if (name == "lle_multithread") + return Settings::AudioEmulation::LLEMultithreaded; + return Settings::AudioEmulation::HLE; // Default +} + +static void ParseAudioOptions(void) { + Settings::values.audio_emulation = + GetAudioEmulation(LibRetro::FetchVariable(config::audio::audio_emulation, "hle")); + + auto input_type = LibRetro::FetchVariable(config::audio::input_type, "auto"); + if (input_type == "none") { + Settings::values.input_type = AudioCore::InputType::Null; + } else if (input_type == "static_noise") { + Settings::values.input_type = AudioCore::InputType::Static; + } else if (input_type == "frontend") { + Settings::values.input_type = AudioCore::InputType::LibRetro; + } else { + Settings::values.input_type = AudioCore::InputType::Auto; + } +} + +static Settings::TextureFilter GetTextureFilter(const std::string& name) { + if (name == "Anime4K Ultrafast") + return Settings::TextureFilter::Anime4K; + if (name == "Bicubic") + return Settings::TextureFilter::Bicubic; + if (name == "ScaleForce") + return Settings::TextureFilter::ScaleForce; + if (name == "xBRZ freescale") + return Settings::TextureFilter::xBRZ; + if (name == "MMPX") + return Settings::TextureFilter::MMPX; + + return Settings::TextureFilter::NoFilter; +} + +static Settings::TextureSampling GetTextureSampling(const std::string& name) { + if (name == "NearestNeighbor") + return Settings::TextureSampling::NearestNeighbor; + if (name == "Linear") + return Settings::TextureSampling::Linear; + + return Settings::TextureSampling::GameControlled; +} + +static Settings::GraphicsAPI GetGraphicsAPI(const std::string& name) { + if (name == "Software") + return Settings::GraphicsAPI::Software; +#ifdef ENABLE_VULKAN + if (name == "Vulkan") + return Settings::GraphicsAPI::Vulkan; +#endif +#ifdef ENABLE_OPENGL + if (name == "OpenGL") + return Settings::GraphicsAPI::OpenGL; +#endif + // Auto selection + return LibRetro::GetPreferredRenderer(); +} + +static void ParseGraphicsOptions(void) { + Settings::values.graphics_api = + GetGraphicsAPI(LibRetro::FetchVariable(config::graphics::graphics_api, "auto")); + + Settings::values.use_hw_shader = LibRetro::FetchVariable(config::graphics::use_hw_shader, + config::enabled) == config::enabled; + + Settings::values.use_shader_jit = LibRetro::FetchVariable(config::graphics::use_shader_jit, + config::enabled) == config::enabled; +#if defined(IOS) + if (!LibRetro::CanUseJIT()) + Settings::values.use_shader_jit = false; +#endif + + Settings::values.shaders_accurate_mul = + LibRetro::FetchVariable(config::graphics::shaders_accurate_mul, config::enabled) == + config::enabled; + + Settings::values.use_disk_shader_cache = + LibRetro::FetchVariable(config::graphics::use_disk_shader_cache, config::enabled) == + config::enabled; + + auto resolution = LibRetro::FetchVariable(config::graphics::resolution_factor, "1"); + Settings::values.resolution_factor = std::stoi(resolution); + + Settings::values.texture_filter = + GetTextureFilter(LibRetro::FetchVariable(config::graphics::texture_filter, "none")); + + Settings::values.texture_sampling = GetTextureSampling( + LibRetro::FetchVariable(config::graphics::texture_sampling, "GameControlled")); + + Settings::values.custom_textures = LibRetro::FetchVariable(config::graphics::custom_textures, + config::disabled) == config::enabled; + + Settings::values.dump_textures = LibRetro::FetchVariable(config::graphics::dump_textures, + config::disabled) == config::enabled; +} + +static Settings::LayoutOption GetLayoutOption(const std::string& name) { + if (name == "single_screen" || name == "Single Screen Only") + return Settings::LayoutOption::SingleScreen; + if (name == "large_screen" || name == "Large Screen, Small Screen") + return Settings::LayoutOption::LargeScreen; + if (name == "side_by_side" || name == "Side by Side") + return Settings::LayoutOption::SideScreen; + return Settings::LayoutOption::Default; +} + +static void ParseLayoutOptions(void) { + Settings::values.layout_option = + GetLayoutOption(LibRetro::FetchVariable(config::layout::layout_option, "default")); + + Settings::values.swap_screen = + LibRetro::FetchVariable(config::layout::swap_screen, "Top") == "Bottom"; + + LibRetro::settings.toggle_swap_screen = + LibRetro::FetchVariable(config::layout::toggle_swap_screen, "Toggle") == "Toggle"; +} + +static void ParseStorageOptions(void) { + Settings::values.use_virtual_sd = LibRetro::FetchVariable(config::storage::use_virtual_sd, + config::enabled) == config::enabled; + + // Configure the file storage location + auto use_libretro_saves = LibRetro::FetchVariable(config::storage::use_libretro_save_path, + "LibRetro Default") == "LibRetro Default"; + + if (use_libretro_saves) { + auto target_dir = LibRetro::GetSaveDir(); + if (target_dir.empty()) { + LOG_INFO(Frontend, "No save dir provided; trying system dir..."); + target_dir = LibRetro::GetSystemDir(); + } + + if (!target_dir.empty()) { + if (!target_dir.ends_with("/")) + target_dir += "/"; + + target_dir += "Azahar/"; + + // Ensure that this new dir exists + if (!FileUtil::CreateDir(target_dir)) { + LOG_ERROR(Frontend, "Failed to create \"{}\". Using Azahar's default paths.", + target_dir); + } else { + FileUtil::SetUserPath(target_dir); + const auto& target_dir_result = FileUtil::GetUserPath(FileUtil::UserPath::UserDir); + LOG_INFO(Frontend, "User dir set to \"{}\".", target_dir_result); + } + } + } +} + +static LibRetro::CStickFunction GetAnalogFunction(const std::string& name) { + if (name == "c_stick" || name == "C-Stick") + return LibRetro::CStickFunction::CStick; + if (name == "touchscreen_pointer" || name == "Touchscreen Pointer") + return LibRetro::CStickFunction::Touchscreen; + return LibRetro::CStickFunction::Both; // Default +} + +static void ParseInputOptions(void) { + LibRetro::settings.analog_function = GetAnalogFunction( + LibRetro::FetchVariable(config::input::analog_function, "c_stick_and_touchscreen")); + + if (LibRetro::settings.analog_function != LibRetro::CStickFunction::Touchscreen) { + Settings::values.current_input_profile.analogs[1] = "axis:1,joystick:0,engine:libretro"; + } else { + Settings::values.current_input_profile.analogs[1] = ""; + } + + auto deadzone = LibRetro::FetchVariable(config::input::deadzone, "15"); + LibRetro::settings.deadzone = static_cast(std::stoi(deadzone)) / 100.0f; + + LibRetro::settings.mouse_touchscreen = + LibRetro::FetchVariable(config::input::mouse_touchscreen, config::enabled) == + config::enabled; + + LibRetro::settings.touch_touchscreen = + LibRetro::FetchVariable(config::input::touch_touchscreen, config::enabled) == + config::enabled; + + LibRetro::settings.render_touchscreen = + LibRetro::FetchVariable(config::input::render_touchscreen, config::disabled) == + config::enabled; + LibRetro::settings.motion_enabled = + LibRetro::FetchVariable(config::input::motion_enabled, config::enabled) == config::enabled; + auto motion_sens = LibRetro::FetchVariable(config::input::motion_sensitivity, "1.0"); + LibRetro::settings.motion_sensitivity = std::stof(motion_sens); + + // Configure motion device based on user settings + if (LibRetro::settings.motion_enabled) { + Settings::values.current_input_profile.motion_device = + "port:0,sensitivity:" + std::to_string(LibRetro::settings.motion_sensitivity) + + ",engine:libretro"; + } else { + Settings::values.current_input_profile.motion_device = "engine:motion_emu"; + } +} + +void ParseCoreOptions(void) { + // Override default values that aren't user-selectable and aren't correct for the core + Settings::values.enable_audio_stretching = false; + Settings::values.frame_limit = 0; +#if defined(USING_GLES) + Settings::values.use_gles = true; +#else + Settings::values.use_gles = false; +#endif + Settings::values.filter_mode = false; + + ParseCpuOptions(); + ParseSystemOptions(); + ParseAudioOptions(); + ParseGraphicsOptions(); + ParseLayoutOptions(); + ParseStorageOptions(); + ParseInputOptions(); +} + +} // namespace LibRetro diff --git a/src/citra_libretro/core_settings.h b/src/citra_libretro/core_settings.h new file mode 100644 index 000000000..78bcff236 --- /dev/null +++ b/src/citra_libretro/core_settings.h @@ -0,0 +1,41 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "core/hle/service/cfg/cfg.h" + +namespace LibRetro { + +enum CStickFunction { Both, CStick, Touchscreen }; + +struct CoreSettings { + + std::string file_path; + + float deadzone = 1.f; + + LibRetro::CStickFunction analog_function; + + bool mouse_touchscreen; + + Service::CFG::SystemLanguage language_value; + + bool touch_touchscreen; + + bool render_touchscreen; + + bool toggle_swap_screen; + + bool motion_enabled; + + float motion_sensitivity; + +} extern settings; + +void RegisterCoreOptions(void); +void ParseCoreOptions(void); + +} // namespace LibRetro diff --git a/src/citra_libretro/emu_window/libretro_window.cpp b/src/citra_libretro/emu_window/libretro_window.cpp new file mode 100644 index 000000000..10d49d883 --- /dev/null +++ b/src/citra_libretro/emu_window/libretro_window.cpp @@ -0,0 +1,342 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#ifdef ENABLE_OPENGL +#include +#endif +#include + +#include "audio_core/audio_types.h" +#include "citra_libretro/citra_libretro.h" +#include "citra_libretro/environment.h" +#include "citra_libretro/input/input_factory.h" +#include "common/settings.h" +#include "core/3ds.h" +#ifdef ENABLE_OPENGL +#include "video_core/renderer_opengl/gl_state.h" +#endif +#include "video_core/gpu.h" +#include "video_core/renderer_software/renderer_software.h" + +#ifdef ENABLE_OPENGL +/// LibRetro expects a "default" GL state. +void ResetGLState() { + // Reset internal state. + OpenGL::OpenGLState state{}; + state.Apply(); + + // Clean up global state. + if (!Settings::values.use_gles) { + glLogicOp(GL_COPY); + } + + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LESS); + glDepthMask(GL_TRUE); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + + glDisable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 0, 0xFFFFFFFF); + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ZERO); + glBlendEquation(GL_FUNC_ADD); + glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ONE, GL_ZERO); + glBlendColor(0, 0, 0, 0); + + glDisable(GL_COLOR_LOGIC_OP); + + glDisable(GL_DITHER); + + glDisable(GL_CULL_FACE); + glCullFace(GL_BACK); + + glActiveTexture(GL_TEXTURE0); +} +#endif + +EmuWindow_LibRetro::EmuWindow_LibRetro() { + strict_context_required = true; + window_info.type = Frontend::WindowSystemType::LibRetro; +} + +EmuWindow_LibRetro::~EmuWindow_LibRetro() {} + +void EmuWindow_LibRetro::SwapBuffers() { + if (suppressPresentation) + return; + submittedFrame = true; + + switch (Settings::values.graphics_api.GetValue()) { + case Settings::GraphicsAPI::OpenGL: { +#ifdef ENABLE_OPENGL + auto current_state = OpenGL::OpenGLState::GetCurState(); + ResetGLState(); + if (enableEmulatedPointer && tracker) { + tracker->Render(width, height); + } + LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast(width), + static_cast(height), 0); + current_state.Apply(); +#endif + break; + } + case Settings::GraphicsAPI::Vulkan: { +#ifdef ENABLE_VULKAN + if (enableEmulatedPointer && tracker) { + tracker->Render(width, height); + } + LibRetro::UploadVideoFrame(RETRO_HW_FRAME_BUFFER_VALID, static_cast(width), + static_cast(height), 0); +#endif + break; + } + case Settings::GraphicsAPI::Software: { + retro_framebuffer fb; + u8* data; + size_t pitch; + bool did_malloc = false; + if (LibRetro::GetSoftwareFramebuffer(&fb, width, height)) { + data = static_cast(fb.data); + pitch = fb.pitch; + } else { + pitch = static_cast(width) * 4; + data = static_cast(calloc(1, pitch * height)); + did_malloc = true; + } + + std::memset(data, 0, pitch * height); + + auto& system = Core::System::GetInstance(); + const auto& renderer = static_cast(system.GPU().Renderer()); + const auto& layout = GetFramebufferLayout(); + + // Blit a single screen from ScreenInfo (column-major RGBA) to the + // output buffer (row-major XRGB8888), rotating and scaling as needed. + // The 3DS framebuffer is portrait-oriented; ScreenInfo stores pixels + // column-major so the transpose gives us the landscape orientation: + // display (dx, dy) -> ScreenInfo (x=dy, y=dx) + auto blit_screen = [&](VideoCore::ScreenId screen_id, const Common::Rectangle& rect) { + const auto& info = renderer.Screen(screen_id); + if (info.pixels.empty()) + return; + + const u32 rect_w = rect.GetWidth(); + const u32 rect_h = rect.GetHeight(); + if (rect_w == 0 || rect_h == 0) + return; + + // Landscape display dimensions (transposed from portrait storage) + const u32 native_w = info.height; + const u32 native_h = info.width; + + for (u32 oy = 0; oy < rect_h; oy++) { + for (u32 ox = 0; ox < rect_w; ox++) { + const u32 dx = ox * native_w / rect_w; + const u32 dy = oy * native_h / rect_h; + + const u32 src_off = (dy * info.height + dx) * 4; + if (src_off + 3 >= info.pixels.size()) + continue; + + const u8* src = info.pixels.data() + src_off; + const size_t dst_off = static_cast(rect.top + oy) * pitch + + static_cast(rect.left + ox) * 4; + + // RGBA -> XRGB8888 (little-endian: B, G, R, 0) + data[dst_off + 0] = src[2]; + data[dst_off + 1] = src[1]; + data[dst_off + 2] = src[0]; + data[dst_off + 3] = 0; + } + } + }; + + if (layout.top_screen_enabled) { + blit_screen(VideoCore::ScreenId::TopLeft, layout.top_screen); + } + if (layout.bottom_screen_enabled) { + blit_screen(VideoCore::ScreenId::Bottom, layout.bottom_screen); + } + + // Software cursor rendering with framebuffer access + if (enableEmulatedPointer && tracker) { + tracker->Render(width, height, data); + } + + LibRetro::UploadVideoFrame(data, static_cast(width), + static_cast(height), pitch); + if (did_malloc) + free(data); + break; + } + } +} + +void EmuWindow_LibRetro::SetupFramebuffer() { + if (Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::OpenGL) + return; + +#ifdef ENABLE_OPENGL + // TODO: Expose interface in renderer_opengl to configure this in it's internal state + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast(LibRetro::GetFramebuffer())); + + // glClear can be a slow path - skip clearing if we don't need to. + if (doCleanFrame) { + glClear(GL_COLOR_BUFFER_BIT); + + doCleanFrame = false; + } +#endif +} + +void EmuWindow_LibRetro::PollEvents() { + // The software renderer doesn't call render_window.SwapBuffers() — standalone + // frontends (Qt/SDL) use separate presentation threads that pull from screen_infos + // instead. In libretro there's no such thread, so we present here: PollEvents is + // called from EndFrame() during each VBlank, right after PrepareRenderTarget has + // filled the screen pixel buffers. + if (Settings::values.graphics_api.GetValue() == Settings::GraphicsAPI::Software) { + SwapBuffers(); + } + + LibRetro::PollInput(); + + // TODO: Poll for right click for motion emu + + if (enableEmulatedPointer && tracker) { + tracker->Update(width, height, GetFramebufferLayout()); + + if (tracker->IsPressed()) { + auto mousePos = tracker->GetPressedPosition(); + + if (hasTouched) { + TouchMoved(mousePos.first, mousePos.second); + } else { + TouchPressed(mousePos.first, mousePos.second); + hasTouched = true; + } + } else if (hasTouched) { + hasTouched = false; + TouchReleased(); + } + } +} + +void EmuWindow_LibRetro::MakeCurrent() { + // They don't get any say in the matter - GL context is always current! +} + +void EmuWindow_LibRetro::DoneCurrent() { + // They don't get any say in the matter - GL context is always current! +} + +void EmuWindow_LibRetro::OnMinimalClientAreaChangeRequest(std::pair _minimal_size) {} + +LayoutGeometry ComputeLayoutGeometry() { + unsigned baseX; + unsigned baseY; + bool emulated_pointer = true; + + float scaling = Settings::values.resolution_factor.GetValue(); + bool swapped = Settings::values.swap_screen.GetValue(); + + switch (Settings::values.layout_option.GetValue()) { + case Settings::LayoutOption::SingleScreen: + if (swapped) { // Bottom screen visible + baseX = Core::kScreenBottomWidth; + baseY = Core::kScreenBottomHeight; + } else { // Top screen visible + baseX = Core::kScreenTopWidth; + baseY = Core::kScreenTopHeight; + emulated_pointer = false; + } + baseX *= scaling; + baseY *= scaling; + break; + case Settings::LayoutOption::LargeScreen: + if (swapped) { // Bottom screen biggest + baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth / 4; + baseY = Core::kScreenBottomHeight; + } else { // Top screen biggest + baseX = Core::kScreenTopWidth + Core::kScreenBottomWidth / 4; + baseY = Core::kScreenTopHeight; + } + + if (scaling < 4) { + // Unfortunately, to get this aspect ratio correct (and have non-blurry 1x scaling), + // we have to have a pretty large buffer for the minimum ratio. + baseX *= 4; + baseY *= 4; + } else { + baseX *= scaling; + baseY *= scaling; + } + break; + case Settings::LayoutOption::SideScreen: + baseX = Core::kScreenBottomWidth + Core::kScreenTopWidth; + baseY = Core::kScreenTopHeight; + baseX *= scaling; + baseY *= scaling; + break; + case Settings::LayoutOption::Default: + default: + baseX = Core::kScreenTopWidth; + baseY = Core::kScreenTopHeight + Core::kScreenBottomHeight; + baseX *= scaling; + baseY *= scaling; + break; + } + + return {baseX, baseY, emulated_pointer}; +} + +void EmuWindow_LibRetro::UpdateLayout() { + auto geom = ComputeLayoutGeometry(); + unsigned baseX = geom.width; + unsigned baseY = geom.height; + enableEmulatedPointer = geom.emulated_pointer; + + // Update Libretro with our status + struct retro_system_av_info info{}; + info.timing.fps = 60.0; + info.timing.sample_rate = AudioCore::native_sample_rate; + info.geometry.aspect_ratio = (float)baseX / (float)baseY; + info.geometry.base_width = baseX; + info.geometry.base_height = baseY; + info.geometry.max_width = baseX; + info.geometry.max_height = baseY; + if (!LibRetro::SetGeometry(&info)) { + LOG_CRITICAL(Frontend, "Failed to update 3DS layout in frontend!"); + } + + width = baseX; + height = baseY; + + UpdateCurrentFramebufferLayout(baseX, baseY); + + doCleanFrame = true; +} + +bool EmuWindow_LibRetro::NeedsClearing() const { + // We manage this ourselves. + return false; +} + +bool EmuWindow_LibRetro::HasSubmittedFrame() { + bool state = submittedFrame; + submittedFrame = false; + return state; +} + +void EmuWindow_LibRetro::CreateContext() { + tracker = std::make_unique(); + + doCleanFrame = true; +} + +void EmuWindow_LibRetro::DestroyContext() { + tracker = nullptr; +} diff --git a/src/citra_libretro/emu_window/libretro_window.h b/src/citra_libretro/emu_window/libretro_window.h new file mode 100644 index 000000000..235d14319 --- /dev/null +++ b/src/citra_libretro/emu_window/libretro_window.h @@ -0,0 +1,79 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "citra_libretro/input/mouse_tracker.h" +#include "core/frontend/emu_window.h" + +struct LayoutGeometry { + unsigned width; + unsigned height; + bool emulated_pointer; +}; + +/// Compute framebuffer dimensions from current layout/scaling/swap settings. +LayoutGeometry ComputeLayoutGeometry(); + +void ResetGLState(); + +class EmuWindow_LibRetro : public Frontend::EmuWindow { +public: + EmuWindow_LibRetro(); + ~EmuWindow_LibRetro(); + + /// Swap buffers to display the next frame + void SwapBuffers() override; + + /// Polls window events + void PollEvents() override; + + /// Makes the graphics context current for the caller thread + void MakeCurrent() override; + + /// Releases the GL context from the caller thread + void DoneCurrent() override; + + void SetupFramebuffer() override; + + /// Prepares the window for rendering + void UpdateLayout(); + + /// States whether a frame has been submitted. Resets after call. + bool HasSubmittedFrame(); + + /// Flags that the framebuffer should be cleared. + bool NeedsClearing() const override; + + /// Creates state for a currently running OpenGL context. + void CreateContext(); + + /// Destroys a currently running OpenGL context. + void DestroyContext(); + + /// When true, SwapBuffers() is suppressed (used during savestate drain loops) + bool suppressPresentation = false; + +private: + /// Called when a configuration change affects the minimal size of the window + void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; + + int width; + int height; + + bool submittedFrame = false; + + // Hack to ensure stuff runs on the main thread + bool doCleanFrame = false; + + // For tracking LibRetro state + bool hasTouched = false; + + // For tracking mouse cursor + std::unique_ptr tracker = nullptr; + + bool enableEmulatedPointer = false; +}; diff --git a/src/citra_libretro/environment.cpp b/src/citra_libretro/environment.cpp new file mode 100644 index 000000000..04c78356f --- /dev/null +++ b/src/citra_libretro/environment.cpp @@ -0,0 +1,281 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include + +#include "audio_core/audio_types.h" +#include "audio_core/libretro_sink.h" +#include "common/scm_rev.h" +#include "core/3ds.h" +#include "emu_window/libretro_window.h" +#include "environment.h" + +#ifdef HAVE_LIBRETRO_VFS +#include "streams/file_stream.h" +#endif + +using namespace LibRetro; + +namespace LibRetro { + +namespace { + +static retro_video_refresh_t video_cb; +static retro_audio_sample_batch_t audio_batch_cb; +static retro_environment_t environ_cb; +static retro_input_poll_t input_poll_cb; +static retro_input_state_t input_state_cb; + +} // namespace + +bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height) { + fb->data = nullptr; + fb->width = width; + fb->height = height; + fb->pitch = 0; + fb->format = RETRO_PIXEL_FORMAT_XRGB8888; + fb->access_flags = RETRO_MEMORY_ACCESS_WRITE; + fb->memory_flags = 0; + return environ_cb(RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER, fb); +} + +void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch) { + return video_cb(data, width, height, pitch); +} + +bool SetHWSharedContext() { + return environ_cb(RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT, NULL); +} + +void PollInput() { + return input_poll_cb(); +} + +bool GetSensorInterface(struct retro_sensor_interface* sensor_interface) { + return environ_cb(RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE, sensor_interface); +} + +bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface) { + return environ_cb(RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE, mic_interface); +} + +Settings::GraphicsAPI GetPreferredRenderer() { + // try and maintain the current driver + retro_hw_context_type context_type = RETRO_HW_CONTEXT_OPENGL; + environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &context_type); + switch (context_type) { +#ifdef ENABLE_OPENGL + case RETRO_HW_CONTEXT_OPENGL: + case RETRO_HW_CONTEXT_OPENGL_CORE: + case RETRO_HW_CONTEXT_OPENGLES2: + case RETRO_HW_CONTEXT_OPENGLES3: + case RETRO_HW_CONTEXT_OPENGLES_VERSION: + return Settings::GraphicsAPI::OpenGL; +#endif +#ifdef ENABLE_VULKAN + case RETRO_HW_CONTEXT_VULKAN: + return Settings::GraphicsAPI::Vulkan; +#endif + default: + break; + } + // we can't maintain the current driver, need to switch +#if defined(ENABLE_VULKAN) + return Settings::GraphicsAPI::Vulkan; +#elif defined(ENABLE_OPENGL) + return Settings::GraphicsAPI::OpenGL; +#else + return Settings::GraphicsAPI::Software; +#endif +} + +bool SetVariables(const retro_variable vars[]) { + return environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)vars); +} + +bool SetCoreOptionsV2(const retro_core_options_v2* options) { + return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, (void*)options); +} + +bool SetCoreOptionsV1(const retro_core_option_definition* options) { + return environ_cb(RETRO_ENVIRONMENT_SET_CORE_OPTIONS, (void*)options); +} + +bool GetCoreOptionsVersion(unsigned* version) { + return environ_cb(RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION, version); +} + +bool SetMemoryMaps(const retro_memory_map* map) { + return environ_cb(RETRO_ENVIRONMENT_SET_MEMORY_MAPS, (void*)map); +} + +bool SetControllerInfo(const retro_controller_info info[]) { + return environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)info); +} + +bool SetPixelFormat(const retro_pixel_format fmt) { + return environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, (void*)&fmt); +} + +bool SetHWRenderer(retro_hw_render_callback* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, cb); +} + +bool GetHWRenderInterface(void** interface) { + return environ_cb(RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE, interface) && !!*interface; +} + +bool SetHWRenderContextNegotiationInterface(void** interface) { + return environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE, interface) && + !!*interface; +} + +bool SetAudioCallback(retro_audio_callback* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK, cb); +} + +bool SetFrameTimeCallback(retro_frame_time_callback* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK, cb); +} + +bool SetGeometry(retro_system_av_info* cb) { + return environ_cb(RETRO_ENVIRONMENT_SET_GEOMETRY, cb); +} + +bool SetInputDescriptors(const retro_input_descriptor desc[]) { + return environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, (void*)desc); +} + +bool HasUpdatedConfig() { + bool updated = false; + return environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated) && updated; +} + +bool Shutdown() { + return environ_cb(RETRO_ENVIRONMENT_SHUTDOWN, NULL); +} + +/// Displays the specified message to the screen. +bool DisplayMessage(const char* sg) { + retro_message msg; + msg.msg = sg; + msg.frames = 60 * 10; + return environ_cb(RETRO_ENVIRONMENT_SET_MESSAGE, &msg); +} + +bool SetSerializationQuirks(uint64_t quirks) { + return environ_cb(RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS, &quirks); +} + +std::string FetchVariable(std::string key, std::string def) { + struct retro_variable var = {nullptr}; + var.key = key.c_str(); + if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { + // Fetching variable failed. + LOG_ERROR(Frontend, "Fetching variable {} failed.", key); + return def; + } + return std::string(var.value); +} + +std::string GetSaveDir() { + char* var = nullptr; + if (!environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &var) || var == nullptr) { + // Fetching variable failed. + LOG_ERROR(Frontend, "No save directory provided by LibRetro."); + return std::string(); + } + return std::string(var); +} + +std::string GetSystemDir() { + char* var = nullptr; + if (!environ_cb(RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY, &var) || var == nullptr) { + // Fetching variable failed. + LOG_ERROR(Frontend, "No system directory provided by LibRetro."); + return std::string(); + } + return std::string(var); +} + +retro_log_printf_t GetLoggingBackend() { + retro_log_callback callback{}; + if (!environ_cb(RETRO_ENVIRONMENT_GET_LOG_INTERFACE, &callback)) { + return nullptr; + } + return callback.log; +} + +int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id) { + return input_state_cb(port, device, index, id); +} + +#ifdef HAVE_LIBRETRO_VFS +void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info) { + if (environ_cb(RETRO_ENVIRONMENT_GET_VFS_INTERFACE, vfs_iface_info)) + filestream_vfs_init(vfs_iface_info); +} +#endif + +#ifdef IOS +bool CanUseJIT() { + bool can_jit = false; + return environ_cb(RETRO_ENVIRONMENT_GET_JIT_CAPABLE, &can_jit) && can_jit; +} +#endif + +}; // namespace LibRetro + +void retro_get_system_info(struct retro_system_info* info) { + memset(info, 0, sizeof(*info)); + info->library_name = "Azahar"; + info->library_version = Common::g_build_fullname; + info->need_fullpath = true; + info->valid_extensions = "3ds|3dsx|z3dsx|elf|axf|cci|zcci|cxi|zcxi|app"; +} + +void LibRetro::SubmitAudio(const int16_t* data, size_t frames) { + audio_batch_cb(data, frames); +} + +void retro_set_audio_sample(retro_audio_sample_t cb) { + // We don't need single audio sample callbacks. +} + +void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { + LibRetro::audio_batch_cb = cb; +} + +void retro_set_input_poll(retro_input_poll_t cb) { + LibRetro::input_poll_cb = cb; +} + +void retro_set_video_refresh(retro_video_refresh_t cb) { + LibRetro::video_cb = cb; +} +void retro_set_environment(retro_environment_t cb) { + LibRetro::environ_cb = cb; + LibRetro::OnConfigureEnvironment(); +} + +void retro_set_controller_port_device(unsigned port, unsigned device) {} + +void retro_set_input_state(retro_input_state_t cb) { + input_state_cb = cb; +} + +void retro_get_system_av_info(struct retro_system_av_info* info) { + info->timing.fps = 60.0; + info->timing.sample_rate = AudioCore::native_sample_rate; + + // Compute geometry from current settings so the frontend allocates the + // correct framebuffer on first use. + auto geom = ComputeLayoutGeometry(); + info->geometry.base_width = geom.width; + info->geometry.base_height = geom.height; + // Max must cover the largest possible layout (SideScreen at 10x = 7200). + info->geometry.max_width = (Core::kScreenBottomWidth + Core::kScreenTopWidth) * 10; + info->geometry.max_height = (Core::kScreenTopHeight + Core::kScreenBottomHeight) * 10; + info->geometry.aspect_ratio = (float)geom.width / (float)geom.height; +} diff --git a/src/citra_libretro/environment.h b/src/citra_libretro/environment.h new file mode 100644 index 000000000..2b84d8b85 --- /dev/null +++ b/src/citra_libretro/environment.h @@ -0,0 +1,129 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include "common/logging/backend.h" +#include "common/logging/filter.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/core.h" +#include "libretro.h" + +namespace LibRetro { + +/// May fetch a framebuffer that can be rendered into for software rendering +/// @see RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER +/// @see retro_framebuffer +/// @see retro_video_refresh_t +bool GetSoftwareFramebuffer(retro_framebuffer* fb, int width, int height); + +/// Calls back to LibRetro to upload a particular video frame. +/// @see retro_video_refresh_t +void UploadVideoFrame(const void* data, unsigned width, unsigned height, size_t pitch); + +/// Calls back to LibRetro to poll input. +/// @see retro_input_poll_t +void PollInput(); + +/// Gets the sensor interface for motion input +bool GetSensorInterface(struct retro_sensor_interface* sensor_interface); + +/// Gets the microphone interface for audio input +bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface); + +/// Sets the environmental variables used for settings. +bool SetVariables(const retro_variable vars[]); + +/// Sets the core options using the v2 interface with categories. +bool SetCoreOptionsV2(const retro_core_options_v2* options); + +/// Sets the core options using the v1 interface. +bool SetCoreOptionsV1(const retro_core_option_definition* options); + +/// Gets the core options version supported by the frontend. +bool GetCoreOptionsVersion(unsigned* version); + +bool SetHWSharedContext(void); + +/// Returns the LibRetro save directory, or a empty string if one doesn't exist. +std::string GetSaveDir(); + +/// Returns the LibRetro system directory, or a empty string if one doesn't exist. +std::string GetSystemDir(); + +/// Fetches a variable by key name. +std::string FetchVariable(std::string key, std::string def); + +/// Returns a logging backend, or null if the frontend refuses to provide one. +retro_log_printf_t GetLoggingBackend(); + +/// Returns graphics api based on global frontend setting +Settings::GraphicsAPI GetPreferredRenderer(); + +/// Displays information about the kinds of controllers that this Citra recreates. +bool SetControllerInfo(const retro_controller_info info[]); + +/// Sets the memory maps for the core. +bool SetMemoryMaps(const retro_memory_map* map); + +/// Sets the framebuffer pixel format. +bool SetPixelFormat(const retro_pixel_format fmt); + +/// Sets the H/W rendering context. +bool SetHWRenderer(retro_hw_render_callback* cb); + +/// Gets the H/W rendering interface. +bool GetHWRenderInterface(void** interface); + +/// Sets the H/W rendering context negotiation interface. +bool SetHWRenderContextNegotiationInterface(void** interface); + +/// Sets the async audio callback. +bool SetAudioCallback(retro_audio_callback* cb); + +/// Sets the frame time callback. +bool SetFrameTimeCallback(retro_frame_time_callback* cb); + +/// Set the size of the new screen buffer. +bool SetGeometry(retro_system_av_info* cb); + +/// Tells LibRetro what input buttons are labelled on the 3DS. +bool SetInputDescriptors(const retro_input_descriptor desc[]); + +/// Returns the current status of a input. +int16_t CheckInput(unsigned port, unsigned device, unsigned index, unsigned id); + +/// Called when the emulator environment is ready to be configured. +void OnConfigureEnvironment(); + +/// Submits audio frames to LibRetro. +/// @see retro_audio_sample_batch_t +void SubmitAudio(const int16_t* data, size_t frames); + +/// Checks to see if the frontend configuration has been updated. +bool HasUpdatedConfig(); + +/// Returns the current framebuffer. +uintptr_t GetFramebuffer(); + +/// Tells the frontend that we are done. +bool Shutdown(); + +/// Displays the specified message to the screen. +bool DisplayMessage(const char* sg); + +/// Sets serialization quirks for the core. +bool SetSerializationQuirks(uint64_t quirks); + +#ifdef HAVE_LIBRETRO_VFS +void SetVFSCallback(struct retro_vfs_interface_info* vfs_iface_info); +#endif + +#ifdef IOS +bool CanUseJIT(); +#endif + +} // namespace LibRetro diff --git a/src/citra_libretro/input/input_factory.cpp b/src/citra_libretro/input/input_factory.cpp new file mode 100644 index 000000000..3a5ae9665 --- /dev/null +++ b/src/citra_libretro/input/input_factory.cpp @@ -0,0 +1,216 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include + +#include "common/math_util.h" +#include "common/vector_math.h" +#include "core/frontend/input.h" + +#include "citra_libretro/environment.h" +#include "citra_libretro/input/input_factory.h" + +namespace LibRetro { + +namespace Input { + +class LibRetroButtonFactory; +class LibRetroAxisFactory; +class LibRetroMotionFactory; + +class LibRetroButton final : public ::Input::ButtonDevice { +public: + explicit LibRetroButton(int joystick_, int button_) : joystick(joystick_), button(button_) {} + + bool GetStatus() const override { + return CheckInput((unsigned int)joystick, RETRO_DEVICE_JOYPAD, 0, (unsigned int)button) > 0; + } + +private: + int joystick; + int button; +}; + +/// A button device factory that creates button devices from LibRetro joystick +class LibRetroButtonFactory final : public ::Input::Factory<::Input::ButtonDevice> { +public: + /** + * Creates a button device from a joystick button + * @param params contains parameters for creating the device: + * - "joystick": the index of the joystick to bind + * - "button": the index of the button to bind + */ + std::unique_ptr<::Input::ButtonDevice> Create(const Common::ParamPackage& params) override { + const int joystick_index = params.Get("joystick", 0); + + const int button = params.Get("button", 0); + return std::make_unique(joystick_index, button); + } +}; + +/// A axis device factory that creates axis devices from LibRetro joystick +class LibRetroAxis final : public ::Input::AnalogDevice { +public: + explicit LibRetroAxis(int joystick_, int button_) : joystick(joystick_), button(button_) {} + + std::tuple GetStatus() const override { + auto axis_x = + (float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 0); + auto axis_y = + (float)CheckInput((unsigned int)joystick, RETRO_DEVICE_ANALOG, (unsigned int)button, 1); + return std::make_tuple(axis_x / INT16_MAX, -axis_y / INT16_MAX); + } + +private: + int joystick; + int button; +}; + +/// A axis device factory that creates axis devices from SDL joystick +class LibRetroAxisFactory final : public ::Input::Factory<::Input::AnalogDevice> { +public: + /** + * Creates a button device from a joystick button + * @param params contains parameters for creating the device: + * - "joystick": the index of the joystick to bind + * - "button"(optional): the index of the button to bind + * - "hat"(optional): the index of the hat to bind as direction buttons + * - "axis"(optional): the index of the axis to bind + * - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", + * "down", "left" or "right" + * - "threshould"(only used for axis): a float value in (-1.0, 1.0) which the button is + * triggered if the axis value crosses + * - "direction"(only used for axis): "+" means the button is triggered when the axis value + * is greater than the threshold; "-" means the button is triggered when the axis value + * is smaller than the threshold + */ + std::unique_ptr<::Input::AnalogDevice> Create(const Common::ParamPackage& params) override { + const int joystick_index = params.Get("joystick", 0); + + const int button = params.Get("axis", 0); + return std::make_unique(joystick_index, button); + } +}; + +/// Static sensor interface callbacks for LibRetro motion input +static retro_sensor_get_input_t sensor_get_input_callback = nullptr; +static retro_set_sensor_state_t sensor_set_state_callback = nullptr; +static bool gyro_enabled = false; +static bool accel_enabled = false; + +/// LibRetro motion device that implements 3DS gyroscope and accelerometer input +class LibRetroMotion final : public ::Input::MotionDevice { +public: + explicit LibRetroMotion(int port_, float sensitivity_) + : port(port_), sensitivity(sensitivity_) { + InitSensors(); + } + + std::tuple, Common::Vec3> GetStatus() const override { + Common::Vec3 accel = {0.0f, 0.0f, -1.0f}; // Default gravity pointing down + Common::Vec3 gyro = {0.0f, 0.0f, 0.0f}; // Default no rotation + + if (sensor_get_input_callback) { + if (accel_enabled) { + // Get accelerometer data (in g units) + // LibRetro coordinate system matches 3DS: X=LEFT, Y=OUT, Z=UP + accel.x = + sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_X) * sensitivity; + accel.y = + sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Y) * sensitivity; + accel.z = + sensor_get_input_callback(port, RETRO_SENSOR_ACCELEROMETER_Z) * sensitivity; + } + + if (gyro_enabled) { + // Get gyroscope data (convert to degrees/sec) + // LibRetro gives radians/sec, 3DS expects degrees/sec + constexpr float RAD_TO_DEG = 180.0f / 3.14159265f; + gyro.x = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_X) * RAD_TO_DEG * + sensitivity; + gyro.y = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Y) * RAD_TO_DEG * + sensitivity; + gyro.z = sensor_get_input_callback(port, RETRO_SENSOR_GYROSCOPE_Z) * RAD_TO_DEG * + sensitivity; + } + } + + return std::make_tuple(accel, gyro); + } + +private: + int port; + float sensitivity; + + void InitSensors() const { + // Initialize sensors if not already done + if (!sensor_get_input_callback || !sensor_set_state_callback) { + struct retro_sensor_interface sensor_interface; + if (LibRetro::GetSensorInterface(&sensor_interface)) { + sensor_get_input_callback = sensor_interface.get_sensor_input; + sensor_set_state_callback = sensor_interface.set_sensor_state; + } + } + + // Enable sensors at 60Hz rate (matching 3DS update frequency) + const unsigned int event_rate = 60; + + if (sensor_set_state_callback) { + if (!accel_enabled && + sensor_set_state_callback(port, RETRO_SENSOR_ACCELEROMETER_ENABLE, event_rate)) { + accel_enabled = true; + } + if (!gyro_enabled && + sensor_set_state_callback(port, RETRO_SENSOR_GYROSCOPE_ENABLE, event_rate)) { + gyro_enabled = true; + } + } + } +}; + +/// Motion device factory that creates motion devices from LibRetro sensor interface +class LibRetroMotionFactory final : public ::Input::Factory<::Input::MotionDevice> { +public: + /** + * Creates a motion device from LibRetro sensor interface + * @param params contains parameters for creating the device: + * - "port": the controller port to read motion from (default 0) + * - "sensitivity": motion sensitivity multiplier (default 1.0) + */ + std::unique_ptr<::Input::MotionDevice> Create(const Common::ParamPackage& params) override { + const int port = params.Get("port", 0); + const float sensitivity = params.Get("sensitivity", 1.0f); + return std::make_unique(port, sensitivity); + } +}; + +void Init() { + using namespace ::Input; + RegisterFactory("libretro", std::make_shared()); + RegisterFactory("libretro", std::make_shared()); + RegisterFactory("libretro", std::make_shared()); +} + +void Shutdown() { + using namespace ::Input; + UnregisterFactory("libretro"); + UnregisterFactory("libretro"); + UnregisterFactory("libretro"); + + // Disable sensors on shutdown + if (sensor_set_state_callback) { + sensor_set_state_callback(0, RETRO_SENSOR_ACCELEROMETER_DISABLE, 60); + sensor_set_state_callback(0, RETRO_SENSOR_GYROSCOPE_DISABLE, 60); + sensor_get_input_callback = nullptr; + sensor_set_state_callback = nullptr; + accel_enabled = false; + gyro_enabled = false; + } +} + +} // namespace Input +} // namespace LibRetro diff --git a/src/citra_libretro/input/input_factory.h b/src/citra_libretro/input/input_factory.h new file mode 100644 index 000000000..74ec5fb74 --- /dev/null +++ b/src/citra_libretro/input/input_factory.h @@ -0,0 +1,20 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/frontend/input.h" + +namespace LibRetro { + +namespace Input { + +/// Initializes and registers LibRetro device factories +void Init(); + +/// Unresisters LibRetro device factories and shut them down. +void Shutdown(); + +} // namespace Input +} // namespace LibRetro diff --git a/src/citra_libretro/input/mouse_tracker.cpp b/src/citra_libretro/input/mouse_tracker.cpp new file mode 100644 index 000000000..6829aad2d --- /dev/null +++ b/src/citra_libretro/input/mouse_tracker.cpp @@ -0,0 +1,440 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include + +#include "citra_libretro/core_settings.h" +#include "citra_libretro/environment.h" +#include "citra_libretro/input/mouse_tracker.h" +#include "common/settings.h" +#include "core/frontend/framebuffer_layout.h" + +#ifdef ENABLE_OPENGL +#include +#include "video_core/shader/generator/glsl_shader_gen.h" +#endif + +#ifdef ENABLE_VULKAN +#include "core/core.h" +#include "video_core/gpu.h" +#include "video_core/renderer_vulkan/renderer_vulkan.h" +#endif + +namespace LibRetro { + +namespace Input { + +/// Shared cursor coordinate calculation +struct CursorCoordinates { + float centerX, centerY; + float renderWidth, renderHeight; + float boundingLeft, boundingTop, boundingRight, boundingBottom; + float verticalLeft, verticalRight, verticalTop, verticalBottom; + float horizontalLeft, horizontalRight, horizontalTop, horizontalBottom; + + CursorCoordinates(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout) { + // Convert to normalized device coordinates + centerX = (projectedX / bufferWidth) * 2 - 1; + centerY = (projectedY / bufferHeight) * 2 - 1; + + renderWidth = renderRatio / bufferWidth; + renderHeight = renderRatio / bufferHeight; + + boundingLeft = (layout.bottom_screen.left / (float)bufferWidth) * 2 - 1; + boundingTop = (layout.bottom_screen.top / (float)bufferHeight) * 2 - 1; + boundingRight = (layout.bottom_screen.right / (float)bufferWidth) * 2 - 1; + boundingBottom = (layout.bottom_screen.bottom / (float)bufferHeight) * 2 - 1; + + // Calculate cursor dimensions + verticalLeft = std::fmax(centerX - renderWidth / 5, boundingLeft); + verticalRight = std::fmin(centerX + renderWidth / 5, boundingRight); + verticalTop = -std::fmax(centerY - renderHeight, boundingTop); + verticalBottom = -std::fmin(centerY + renderHeight, boundingBottom); + + horizontalLeft = std::fmax(centerX - renderWidth, boundingLeft); + horizontalRight = std::fmin(centerX + renderWidth, boundingRight); + horizontalTop = -std::fmax(centerY - renderHeight / 5, boundingTop); + horizontalBottom = -std::fmin(centerY + renderHeight / 5, boundingBottom); + } +}; + +/// Helper function to check if coordinates are within the touchscreen area +/// (uses the same logic as EmuWindow::IsWithinTouchscreen) +static bool IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x, + unsigned framebuffer_y) { + // Note: LibRetro doesn't support SeparateWindows, so we can skip that check + + Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); + + if (render_3d_mode == Settings::StereoRenderOption::SideBySide || + render_3d_mode == Settings::StereoRenderOption::SideBySideFull) { + return (framebuffer_y >= layout.bottom_screen.top && + framebuffer_y < layout.bottom_screen.bottom && + ((framebuffer_x >= layout.bottom_screen.left / 2 && + framebuffer_x < layout.bottom_screen.right / 2) || + (framebuffer_x >= (layout.bottom_screen.left / 2) + (layout.width / 2) && + framebuffer_x < (layout.bottom_screen.right / 2) + (layout.width / 2)))); + } else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) { + return (framebuffer_y >= layout.bottom_screen.top && + framebuffer_y < layout.bottom_screen.bottom && + ((framebuffer_x >= layout.bottom_screen.left && + framebuffer_x < layout.bottom_screen.right) || + (framebuffer_x >= layout.cardboard.bottom_screen_right_eye + (layout.width / 2) && + framebuffer_x < layout.cardboard.bottom_screen_right_eye + + layout.bottom_screen.GetWidth() + (layout.width / 2)))); + } else { + return (framebuffer_y >= layout.bottom_screen.top && + framebuffer_y < layout.bottom_screen.bottom && + framebuffer_x >= layout.bottom_screen.left && + framebuffer_x < layout.bottom_screen.right); + } +} + +MouseTracker::MouseTracker() { + // Create renderer-specific cursor renderer based on current graphics API + cursor_renderer = nullptr; + switch (Settings::values.graphics_api.GetValue()) { + case Settings::GraphicsAPI::OpenGL: +#ifdef ENABLE_OPENGL + cursor_renderer = std::make_unique(); +#endif + break; + case Settings::GraphicsAPI::Vulkan: +#ifdef ENABLE_VULKAN + cursor_renderer = std::make_unique(); +#endif + break; + case Settings::GraphicsAPI::Software: + cursor_renderer = std::make_unique(); + break; + } +} + +MouseTracker::~MouseTracker() = default; + +void MouseTracker::OnMouseMove(int deltaX, int deltaY) { + x += deltaX; + y += deltaY; +} + +void MouseTracker::Restrict(int minX, int minY, int maxX, int maxY) { + x = std::clamp(x, minX, maxX); + y = std::clamp(y, minY, maxY); +} + +void MouseTracker::Update(int bufferWidth, int bufferHeight, + const Layout::FramebufferLayout& layout) { + bool state = false; + + if (LibRetro::settings.mouse_touchscreen) { + // Check mouse input + state |= LibRetro::CheckInput(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); + + // Read in and convert pointer values to absolute values on the canvas + auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + auto newX = static_cast((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); + auto newY = static_cast((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight); + + // Use mouse pointer movement + if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) { + lastMouseX = newX; + lastMouseY = newY; + + // Use layout system to validate and map coordinates + if (IsWithinTouchscreen(layout, newX, newY)) { + x = std::clamp(newX, static_cast(layout.bottom_screen.left), + static_cast(layout.bottom_screen.right)) - + layout.bottom_screen.left; + y = std::clamp(newY, static_cast(layout.bottom_screen.top), + static_cast(layout.bottom_screen.bottom)) - + layout.bottom_screen.top; + } + } + } + + if (LibRetro::settings.touch_touchscreen) { + // Check touchscreen input + state |= LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED); + + // Read in and convert pointer values to absolute values on the canvas + auto pointerX = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + auto pointerY = LibRetro::CheckInput(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + auto newX = static_cast((pointerX + 0x7fff) / (float)(0x7fff * 2) * bufferWidth); + auto newY = static_cast((pointerY + 0x7fff) / (float)(0x7fff * 2) * bufferHeight); + + // Use mouse pointer movement + if ((pointerX != 0 || pointerY != 0) && (newX != lastMouseX || newY != lastMouseY)) { + lastMouseX = newX; + lastMouseY = newY; + + // Use layout system to validate and map coordinates + if (IsWithinTouchscreen(layout, newX, newY)) { + x = std::clamp(newX, static_cast(layout.bottom_screen.left), + static_cast(layout.bottom_screen.right)) - + layout.bottom_screen.left; + y = std::clamp(newY, static_cast(layout.bottom_screen.top), + static_cast(layout.bottom_screen.bottom)) - + layout.bottom_screen.top; + } + } + } + + if (LibRetro::settings.analog_function != LibRetro::CStickFunction::CStick) { + // Check right analog input + state |= LibRetro::CheckInput(0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R3); + + // TODO: Provide config option for ratios here + auto widthSpeed = (layout.bottom_screen.GetWidth() / 20.0); + auto heightSpeed = (layout.bottom_screen.GetHeight() / 20.0); + + // Use controller movement + float controllerX = + ((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, + RETRO_DEVICE_ID_ANALOG_X) / + INT16_MAX); + float controllerY = + ((float)LibRetro::CheckInput(0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_RIGHT, + RETRO_DEVICE_ID_ANALOG_Y) / + INT16_MAX); + + // Deadzone the controller inputs + float smoothedX = std::abs(controllerX); + float smoothedY = std::abs(controllerY); + + if (smoothedX < LibRetro::settings.deadzone) { + controllerX = 0; + } + if (smoothedY < LibRetro::settings.deadzone) { + controllerY = 0; + } + + OnMouseMove(static_cast(controllerX * widthSpeed), + static_cast(controllerY * heightSpeed)); + } + + Restrict(0, 0, layout.bottom_screen.GetWidth(), layout.bottom_screen.GetHeight()); + + // Make the coordinates 0 -> 1 + projectedX = (float)x / layout.bottom_screen.GetWidth(); + projectedY = (float)y / layout.bottom_screen.GetHeight(); + + // Ensure that the projected position doesn't overlap outside the bottom screen framebuffer. + // TODO: Provide config option + renderRatio = (float)layout.bottom_screen.GetHeight() / 30; + + // Map the mouse coord to the bottom screen's position + projectedX = layout.bottom_screen.left + projectedX * layout.bottom_screen.GetWidth(); + projectedY = layout.bottom_screen.top + projectedY * layout.bottom_screen.GetHeight(); + + isPressed = state; + + this->framebuffer_layout = layout; +} + +void MouseTracker::Render(int bufferWidth, int bufferHeight, void* framebuffer_data) { + if (!LibRetro::settings.render_touchscreen) { + return; + } + + // Delegate to renderer-specific implementation + if (cursor_renderer) { + cursor_renderer->Render(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, + framebuffer_layout, framebuffer_data); + } +} + +#ifdef ENABLE_OPENGL +// OpenGL-specific cursor renderer implementation +OpenGLCursorRenderer::OpenGLCursorRenderer() { + // Could potentially also use Citra's built-in shaders, if they can be + // wrangled to cooperate. + + std::string vertex; + if (Settings::values.use_gles) { + vertex += fragment_shader_precision_OES; + } + + vertex += R"( + in vec2 position; + + void main() + { + gl_Position = vec4(position, 0.0, 1.0); + } + )"; + + std::string fragment; + if (Settings::values.use_gles) { + fragment += fragment_shader_precision_OES; + } + fragment += R"( + out vec4 color; + + void main() + { + color = vec4(1.0, 1.0, 1.0, 1.0); + } + )"; + + vao.Create(); + vbo.Create(); + + glBindVertexArray(vao.handle); + glBindBuffer(GL_ARRAY_BUFFER, vbo.handle); + + shader.Create(vertex.c_str(), fragment.c_str()); + + auto positionVariable = (GLuint)glGetAttribLocation(shader.handle, "position"); + glEnableVertexAttribArray(positionVariable); + + glVertexAttribPointer(positionVariable, 2, GL_FLOAT, GL_FALSE, 0, 0); +} + +OpenGLCursorRenderer::~OpenGLCursorRenderer() { + shader.Release(); + vao.Release(); + vbo.Release(); +} + +void OpenGLCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, + float projectedY, float renderRatio, + const Layout::FramebufferLayout& layout, void* framebuffer_data) { + // Use shared coordinate calculation + CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, + layout); + + glUseProgram(shader.handle); + + glBindVertexArray(vao.handle); + + // clang-format off + GLfloat cursor[] = { + // | in the cursor + coords.verticalLeft, coords.verticalTop, + coords.verticalRight, coords.verticalTop, + coords.verticalRight, coords.verticalBottom, + + coords.verticalLeft, coords.verticalTop, + coords.verticalRight, coords.verticalBottom, + coords.verticalLeft, coords.verticalBottom, + + // - in the cursor + coords.horizontalLeft, coords.horizontalTop, + coords.horizontalRight, coords.horizontalTop, + coords.horizontalRight, coords.horizontalBottom, + + coords.horizontalLeft, coords.horizontalTop, + coords.horizontalRight, coords.horizontalBottom, + coords.horizontalLeft, coords.horizontalBottom + }; + // clang-format on + + glEnable(GL_BLEND); + glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ONE_MINUS_SRC_COLOR); + + glBindBuffer(GL_ARRAY_BUFFER, vbo.handle); + glBufferData(GL_ARRAY_BUFFER, sizeof(cursor), cursor, GL_STATIC_DRAW); + + glDrawArrays(GL_TRIANGLES, 0, 12); + + glBindVertexArray(0); + glUseProgram(0); + glDisable(GL_BLEND); +} +#endif + +#ifdef ENABLE_VULKAN +// Vulkan-specific cursor renderer implementation +VulkanCursorRenderer::VulkanCursorRenderer() { + // Vulkan cursor rendering will be integrated into the main rendering pipeline +} + +VulkanCursorRenderer::~VulkanCursorRenderer() = default; + +void VulkanCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, + float projectedY, float renderRatio, + const Layout::FramebufferLayout& layout, void* framebuffer_data) { + // Use shared coordinate calculation + CursorCoordinates coords(bufferWidth, bufferHeight, projectedX, projectedY, renderRatio, + layout); + + // TODO: Implement actual Vulkan cursor drawing using the renderer's command buffer + // This would involve: + // 1. Creating a simple vertex buffer with cursor geometry using coords + // 2. Using a basic shader pipeline + // 3. Recording draw commands into the current command buffer + // 4. Using blend mode similar to OpenGL (ONE_MINUS_DST_COLOR, ONE_MINUS_SRC_COLOR) + + // For now, this is a placeholder - the cursor won't be visible in Vulkan mode + // but the touchscreen input will still work +} +#endif + +// Software-specific cursor renderer implementation +SoftwareCursorRenderer::SoftwareCursorRenderer() { + // Software renderer initialization +} + +SoftwareCursorRenderer::~SoftwareCursorRenderer() = default; + +void SoftwareCursorRenderer::Render(int bufferWidth, int bufferHeight, float projectedX, + float projectedY, float renderRatio, + const Layout::FramebufferLayout& layout, + void* framebuffer_data) { + if (!framebuffer_data) { + return; // No framebuffer data available + } + + // Convert coordinates to screen space + int centerX = static_cast(projectedX); + int centerY = static_cast(projectedY); + int radius = static_cast(renderRatio); + + // Calculate cursor dimensions within bounds + int verticalLeft = std::max(centerX - radius / 5, static_cast(layout.bottom_screen.left)); + int verticalRight = + std::min(centerX + radius / 5, static_cast(layout.bottom_screen.right)); + int verticalTop = std::max(centerY - radius, static_cast(layout.bottom_screen.top)); + int verticalBottom = std::min(centerY + radius, static_cast(layout.bottom_screen.bottom)); + + int horizontalLeft = std::max(centerX - radius, static_cast(layout.bottom_screen.left)); + int horizontalRight = std::min(centerX + radius, static_cast(layout.bottom_screen.right)); + int horizontalTop = std::max(centerY - radius / 5, static_cast(layout.bottom_screen.top)); + int horizontalBottom = + std::min(centerY + radius / 5, static_cast(layout.bottom_screen.bottom)); + + // Draw cursor directly to framebuffer (assuming RGBA8888 format) + uint32_t* pixels = static_cast(framebuffer_data); + const uint32_t cursorColor = 0xFFFFFFFF; // White cursor + + // Draw vertical line of cursor + for (int y = verticalTop; y < verticalBottom; ++y) { + for (int x = verticalLeft; x < verticalRight; ++x) { + if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) { + int pixelIndex = y * bufferWidth + x; + // XOR blend for visibility on any background + pixels[pixelIndex] ^= cursorColor; + } + } + } + + // Draw horizontal line of cursor + for (int y = horizontalTop; y < horizontalBottom; ++y) { + for (int x = horizontalLeft; x < horizontalRight; ++x) { + if (x >= 0 && x < bufferWidth && y >= 0 && y < bufferHeight) { + int pixelIndex = y * bufferWidth + x; + // XOR blend for visibility on any background + pixels[pixelIndex] ^= cursorColor; + } + } + } +} + +} // namespace Input + +} // namespace LibRetro diff --git a/src/citra_libretro/input/mouse_tracker.h b/src/citra_libretro/input/mouse_tracker.h new file mode 100644 index 000000000..3f4435792 --- /dev/null +++ b/src/citra_libretro/input/mouse_tracker.h @@ -0,0 +1,111 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "common/math_util.h" +#include "core/frontend/framebuffer_layout.h" + +#ifdef ENABLE_OPENGL +#include "video_core/renderer_opengl/gl_resource_manager.h" +#endif + +namespace LibRetro { + +namespace Input { + +class CursorRenderer { +public: + virtual ~CursorRenderer() = default; + virtual void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) = 0; +}; + +/// The mouse tracker provides a mechanism to handle relative mouse/joypad input +/// for a touch-screen device. +class MouseTracker { +public: + MouseTracker(); + ~MouseTracker(); + + /// Called whenever a mouse moves. + void OnMouseMove(int xDelta, int yDelta); + + /// Restricts the mouse cursor to a specified rectangle. + void Restrict(int minX, int minY, int maxX, int maxY); + + /// Updates the tracker. + void Update(int bufferWidth, int bufferHeight, const Layout::FramebufferLayout& layout); + + /// Renders the cursor to the screen (delegates to renderer-specific implementation). + void Render(int bufferWidth, int bufferHeight, void* framebuffer_data = nullptr); + + /// If the touchscreen is being pressed. + bool IsPressed() { + return isPressed; + } + + /// Get the pressed position, relative to the framebuffer. + std::pair GetPressedPosition() { + return {static_cast(projectedX), + static_cast(projectedY)}; + } + +private: + int x; + int y; + + float lastMouseX; + float lastMouseY; + + float projectedX; + float projectedY; + float renderRatio; + + bool isPressed; + + Layout::FramebufferLayout framebuffer_layout; + std::unique_ptr cursor_renderer; +}; + +#ifdef ENABLE_OPENGL +class OpenGLCursorRenderer : public CursorRenderer { +public: + OpenGLCursorRenderer(); + ~OpenGLCursorRenderer(); + void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) override; + +private: + OpenGL::OGLProgram shader; + OpenGL::OGLVertexArray vao; + OpenGL::OGLBuffer vbo; +}; +#endif + +#ifdef ENABLE_VULKAN +class VulkanCursorRenderer : public CursorRenderer { +public: + VulkanCursorRenderer(); + ~VulkanCursorRenderer(); + void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) override; +}; +#endif + +class SoftwareCursorRenderer : public CursorRenderer { +public: + SoftwareCursorRenderer(); + ~SoftwareCursorRenderer(); + void Render(int bufferWidth, int bufferHeight, float projectedX, float projectedY, + float renderRatio, const Layout::FramebufferLayout& layout, + void* framebuffer_data = nullptr) override; +}; + +} // namespace Input + +} // namespace LibRetro diff --git a/src/citra_libretro/libretro.osx.def b/src/citra_libretro/libretro.osx.def new file mode 100644 index 000000000..53a556a60 --- /dev/null +++ b/src/citra_libretro/libretro.osx.def @@ -0,0 +1,27 @@ +#LIBRARY "libretro" +#EXPORTS +_retro_set_environment +_retro_set_video_refresh +_retro_set_audio_sample +_retro_set_audio_sample_batch +_retro_set_input_poll +_retro_set_input_state +_retro_init +_retro_deinit +_retro_api_version +_retro_get_system_info +_retro_get_system_av_info +_retro_set_controller_port_device +_retro_reset +_retro_run +_retro_serialize_size +_retro_serialize +_retro_unserialize +_retro_cheat_reset +_retro_cheat_set +_retro_load_game +_retro_load_game_special +_retro_unload_game +_retro_get_region +_retro_get_memory_data +_retro_get_memory_size diff --git a/src/citra_libretro/libretro_vk.cpp b/src/citra_libretro/libretro_vk.cpp new file mode 100644 index 000000000..eed09d5bc --- /dev/null +++ b/src/citra_libretro/libretro_vk.cpp @@ -0,0 +1,860 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include +#include + +#include "citra_libretro/environment.h" +#include "citra_libretro/libretro_vk.h" +#include "common/assert.h" +#include "common/logging/log.h" +#include "common/settings.h" +#include "core/frontend/emu_window.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" + +#include + +static const struct retro_hw_render_interface_vulkan* vulkan_intf; + +namespace LibRetro { + +const VkApplicationInfo* GetVulkanApplicationInfo() { + static VkApplicationInfo app_info{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app_info.pApplicationName = "Azahar"; + app_info.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + app_info.pEngineName = "Azahar"; + app_info.engineVersion = VK_MAKE_VERSION(1, 0, 0); + // Request Vulkan 1.1 for better compatibility (especially on Android) + // Extensions can be used for features beyond 1.1 + app_info.apiVersion = VK_API_VERSION_1_1; + return &app_info; +} + +void AddExtensionIfAvailable(std::vector& enabled_exts, + const std::vector& available_exts, + const char* ext_name) { + // Check if already in the list + for (const char* ext : enabled_exts) { + if (ext && !strcmp(ext, ext_name)) { + return; // Already enabled + } + } + + // Check if available + for (const auto& ext : available_exts) { + if (!strcmp(ext.extensionName, ext_name)) { + enabled_exts.push_back(ext_name); + LOG_INFO(Render_Vulkan, "Enabling Vulkan extension: {}", ext_name); + return; + } + } + + LOG_DEBUG(Render_Vulkan, "Vulkan extension {} not available", ext_name); +} + +bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance, + VkPhysicalDevice gpu, VkSurfaceKHR surface, + PFN_vkGetInstanceProcAddr get_instance_proc_addr, + const char** required_device_extensions, + unsigned num_required_device_extensions, + const char** required_device_layers, unsigned num_required_device_layers, + const VkPhysicalDeviceFeatures* required_features) { + + LOG_INFO(Render_Vulkan, "CreateDevice callback invoked - negotiating Vulkan device creation"); + + // Get available extensions for this physical device + uint32_t ext_count = 0; + PFN_vkEnumerateDeviceExtensionProperties vkEnumerateDeviceExtensionProperties = + (PFN_vkEnumerateDeviceExtensionProperties)get_instance_proc_addr( + instance, "vkEnumerateDeviceExtensionProperties"); + + vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, nullptr); + std::vector available_exts(ext_count); + if (ext_count > 0) { + vkEnumerateDeviceExtensionProperties(gpu, nullptr, &ext_count, available_exts.data()); + } + + // Start with frontend's required extensions + std::vector enabled_exts; + enabled_exts.reserve(num_required_device_extensions + 10); + for (unsigned i = 0; i < num_required_device_extensions; i++) { + if (required_device_extensions[i]) { + enabled_exts.push_back(required_device_extensions[i]); + } + } + + // Add extensions we want (if available) + AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_SWAPCHAIN_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, + VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, + VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); + AddExtensionIfAvailable(enabled_exts, available_exts, VK_EXT_TOOLING_INFO_EXTENSION_NAME); + + // These are beneficial but blacklisted on some platforms due to driver bugs + // For now, let the Instance class handle these decisions + // AddExtensionIfAvailable(enabled_exts, available_exts, + // VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME); + // AddExtensionIfAvailable(enabled_exts, available_exts, + // VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME); + + // Merge frontend's required features with our baseline + VkPhysicalDeviceFeatures merged_features{}; + if (required_features) { + // Copy all frontend requirements + for (unsigned i = 0; i < sizeof(VkPhysicalDeviceFeatures) / sizeof(VkBool32); i++) { + if (reinterpret_cast(required_features)[i]) { + reinterpret_cast(&merged_features)[i] = VK_TRUE; + } + } + } + + // Request features we need (these will be OR'd with frontend requirements) + // The Instance class will validate these against actual device capabilities + merged_features.geometryShader = VK_TRUE; // Used for certain rendering effects + merged_features.logicOp = VK_TRUE; // Used for blending modes + merged_features.samplerAnisotropy = VK_TRUE; // Used for texture filtering + + // Find queue family with graphics support + PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties = + (PFN_vkGetPhysicalDeviceQueueFamilyProperties)get_instance_proc_addr( + instance, "vkGetPhysicalDeviceQueueFamilyProperties"); + + uint32_t queue_family_count = 0; + vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, nullptr); + std::vector queue_families(queue_family_count); + vkGetPhysicalDeviceQueueFamilyProperties(gpu, &queue_family_count, queue_families.data()); + + uint32_t graphics_queue_family = VK_QUEUE_FAMILY_IGNORED; + for (uint32_t i = 0; i < queue_family_count; i++) { + if (queue_families[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) { + graphics_queue_family = i; + break; + } + } + + if (graphics_queue_family == VK_QUEUE_FAMILY_IGNORED) { + LOG_CRITICAL(Render_Vulkan, "No graphics queue family found!"); + return false; + } + + // Create device + const float queue_priority = 1.0f; + VkDeviceQueueCreateInfo queue_info{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue_info.queueFamilyIndex = graphics_queue_family; + queue_info.queueCount = 1; + queue_info.pQueuePriorities = &queue_priority; + + VkDeviceCreateInfo device_info{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device_info.queueCreateInfoCount = 1; + device_info.pQueueCreateInfos = &queue_info; + device_info.enabledExtensionCount = static_cast(enabled_exts.size()); + device_info.ppEnabledExtensionNames = enabled_exts.data(); + device_info.enabledLayerCount = num_required_device_layers; + device_info.ppEnabledLayerNames = required_device_layers; + device_info.pEnabledFeatures = &merged_features; + + PFN_vkCreateDevice vkCreateDevice = + (PFN_vkCreateDevice)get_instance_proc_addr(instance, "vkCreateDevice"); + + VkDevice device = VK_NULL_HANDLE; + VkResult result = vkCreateDevice(gpu, &device_info, nullptr, &device); + if (result != VK_SUCCESS) { + LOG_CRITICAL(Render_Vulkan, "vkCreateDevice failed: {}", static_cast(result)); + return false; + } + + // Get the queue + PFN_vkGetDeviceQueue vkGetDeviceQueue = + (PFN_vkGetDeviceQueue)get_instance_proc_addr(instance, "vkGetDeviceQueue"); + + VkQueue queue = VK_NULL_HANDLE; + vkGetDeviceQueue(device, graphics_queue_family, 0, &queue); + + // Fill in the context for the frontend + context->gpu = gpu; + context->device = device; + context->queue = queue; + context->queue_family_index = graphics_queue_family; + context->presentation_queue = queue; // Same queue for LibRetro + context->presentation_queue_family_index = graphics_queue_family; + + LOG_INFO(Render_Vulkan, + "Vulkan device created successfully via negotiation interface (GPU: {}, Queue " + "Family: {})", + static_cast(gpu), graphics_queue_family); + + return true; +} + +void VulkanResetContext() { + LibRetro::GetHWRenderInterface((void**)&vulkan_intf); + + // Initialize dispatcher with LibRetro's function pointers + VULKAN_HPP_DEFAULT_DISPATCHER.init(vulkan_intf->get_instance_proc_addr); + + vk::Instance vk_instance{vulkan_intf->instance}; + VULKAN_HPP_DEFAULT_DISPATCHER.init(vk_instance); +} + +} // namespace LibRetro + +namespace Vulkan { + +std::shared_ptr OpenLibrary( + [[maybe_unused]] Frontend::GraphicsContext* context) { + // the frontend takes care of this, we'll get the instance later + return std::make_shared(); +} + +vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window) { + // LibRetro cores don't use surfaces - we render to our own output texture + // This function should not be called in LibRetro mode + LOG_WARNING(Render_Vulkan, "CreateSurface called in LibRetro mode - this should not happen"); + return VK_NULL_HANDLE; +} + +vk::UniqueInstance CreateInstance([[maybe_unused]] const Common::DynamicLibrary& library, + [[maybe_unused]] Frontend::WindowSystemType window_type, + [[maybe_unused]] bool enable_validation, + [[maybe_unused]] bool dump_command_buffers) { + // LibRetro cores don't create instances - frontend handles this + LOG_WARNING(Render_Vulkan, "CreateInstance called in LibRetro mode - this should not happen"); + return vk::UniqueInstance{}; +} + +DebugCallback CreateDebugCallback(vk::Instance instance, bool& debug_utils_supported) { + // LibRetro handles debugging, return empty callback + debug_utils_supported = false; + return {}; +} + +LibRetroVKInstance::LibRetroVKInstance(Frontend::EmuWindow& window, + [[maybe_unused]] u32 physical_device_index) + : Instance(Instance::NoInit{}) { + // Ensure LibRetro interface is available + if (!vulkan_intf) { + LOG_CRITICAL(Render_Vulkan, "LibRetro Vulkan interface not initialized!"); + throw std::runtime_error("LibRetro Vulkan interface not available"); + } + + // Initialize basic Vulkan objects from LibRetro + physical_device = vulkan_intf->gpu; + if (!physical_device) { + LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid physical device!"); + throw std::runtime_error("Invalid physical device from LibRetro"); + } + + // Get device properties and features + properties = physical_device.getProperties(); + + const std::vector extensions = physical_device.enumerateDeviceExtensionProperties(); + available_extensions.reserve(extensions.size()); + for (const auto& extension : extensions) { + available_extensions.emplace_back(extension.extensionName.data()); + } + + // Get queues from LibRetro + graphics_queue = vulkan_intf->queue; + queue_family_index = vulkan_intf->queue_index; + present_queue = graphics_queue; // Same queue for LibRetro + + if (!graphics_queue) { + LOG_CRITICAL(Render_Vulkan, "LibRetro provided invalid graphics queue!"); + throw std::runtime_error("Invalid graphics queue from LibRetro"); + } + + // Initialize Vulkan HPP dispatcher with LibRetro's device + VULKAN_HPP_DEFAULT_DISPATCHER.init(vk::Device{vulkan_intf->device}); + + // Now run device capability detection with dispatcher initialized + CreateDevice(); + + // LibRetro-specific: Validate function pointers are actually available + // LibRetro's device may not have loaded all extension functions even if extensions are + // available + if (extended_dynamic_state) { + if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetCullModeEXT || + !VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthTestEnableEXT || + !VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetDepthWriteEnableEXT || + !VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdSetFrontFaceEXT) { + LOG_WARNING(Render_Vulkan, "Extended dynamic state function pointers not available in " + "LibRetro context, disabling"); + extended_dynamic_state = false; + } + } + + if (timeline_semaphores) { + if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkGetSemaphoreCounterValueKHR) { + LOG_WARNING(Render_Vulkan, "Timeline semaphore function pointers not available in " + "LibRetro context, disabling"); + timeline_semaphores = false; + } + } + + // Initialize subsystems + CreateAllocator(); + CreateFormatTable(); + CollectToolingInfo(); + CreateCustomFormatTable(); + CreateAttribTable(); + + LOG_INFO(Render_Vulkan, "LibRetro Vulkan Instance initialized successfully"); + LOG_INFO(Render_Vulkan, "Device: {} ({})", properties.deviceName.data(), GetVendorName()); + LOG_INFO(Render_Vulkan, "Driver: {}", GetDriverVersionName()); +} + +vk::Instance LibRetroVKInstance::GetInstance() const { + return vk::Instance{vulkan_intf->instance}; +} + +vk::Device LibRetroVKInstance::GetDevice() const { + return vk::Device{vulkan_intf->device}; +} + +// ============================================================================ +// PresentWindow Implementation (LibRetro version) +// ============================================================================ + +PresentWindow::PresentWindow(Frontend::EmuWindow& emu_window_, const Instance& instance_, + Scheduler& scheduler_, [[maybe_unused]] bool low_refresh_rate) + : emu_window{emu_window_}, instance{instance_}, scheduler{scheduler_}, + graphics_queue{instance.GetGraphicsQueue()} { + const vk::Device device = instance.GetDevice(); + + LOG_INFO(Render_Vulkan, "Initializing LibRetro PresentWindow"); + + // Create command pool for frame operations + const vk::CommandPoolCreateInfo pool_info = { + .flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer | + vk::CommandPoolCreateFlagBits::eTransient, + .queueFamilyIndex = instance.GetGraphicsQueueFamilyIndex(), + }; + command_pool = device.createCommandPool(pool_info); + + // Create render pass for LibRetro output + present_renderpass = CreateRenderpass(); + + // Start with initial dimensions from layout + const auto& layout = emu_window.GetFramebufferLayout(); + CreateOutputTexture(layout.width, layout.height); + CreateFrameResources(); + + LOG_INFO(Render_Vulkan, "LibRetro PresentWindow initialized with {}x{}", layout.width, + layout.height); +} + +PresentWindow::~PresentWindow() { + const vk::Device device = instance.GetDevice(); + + LOG_DEBUG(Render_Vulkan, "Destroying LibRetro PresentWindow"); + + // Wait for any pending operations + WaitPresent(); + device.waitIdle(); + + // Destroy frame resources + DestroyFrameResources(); + + // Destroy output texture + DestroyOutputTexture(); + + // Destroy Vulkan objects + if (command_pool) { + device.destroyCommandPool(command_pool); + } + if (present_renderpass) { + device.destroyRenderPass(present_renderpass); + } +} + +void PresentWindow::CreateOutputTexture(u32 width, u32 height) { + if (width == 0 || height == 0) { + LOG_ERROR(Render_Vulkan, "Invalid output texture dimensions: {}x{}", width, height); + return; + } + + // Destroy existing texture if dimensions changed + if (output_image && (output_width != width || output_height != height)) { + DestroyOutputTexture(); + } + + // Skip if already created with correct dimensions + if (output_image && output_width == width && output_height == height) { + return; + } + + const vk::Device device = instance.GetDevice(); + output_width = width; + output_height = height; + + // Create output image with LibRetro requirements + const vk::ImageCreateInfo image_info = { + .imageType = vk::ImageType::e2D, + .format = output_format, + .extent = {width, height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = vk::SampleCountFlagBits::e1, + .tiling = vk::ImageTiling::eOptimal, + .usage = vk::ImageUsageFlagBits::eColorAttachment | // For rendering + vk::ImageUsageFlagBits::eTransferSrc | // Required by LibRetro + vk::ImageUsageFlagBits::eSampled | // Required by LibRetro + vk::ImageUsageFlagBits::eTransferDst, // For clearing + .sharingMode = vk::SharingMode::eExclusive, + .initialLayout = vk::ImageLayout::eUndefined, + }; + + // Create image with VMA - using budget-aware allocation like standalone version + VmaAllocationCreateInfo alloc_info = {}; + alloc_info.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + alloc_info.flags = VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT; + + VkImage vk_image; + const VkResult result = vmaCreateImage(instance.GetAllocator(), + reinterpret_cast(&image_info), + &alloc_info, &vk_image, &output_allocation, nullptr); + + if (result != VK_SUCCESS) { + LOG_CRITICAL(Render_Vulkan, "Failed to create output image: {}", static_cast(result)); + throw std::runtime_error("Failed to create LibRetro output texture"); + } + + output_image = vk::Image{vk_image}; + + // Create image view + output_view_create_info = { + .image = output_image, + .viewType = vk::ImageViewType::e2D, + .format = output_format, + .components = + { + .r = vk::ComponentSwizzle::eIdentity, + .g = vk::ComponentSwizzle::eIdentity, + .b = vk::ComponentSwizzle::eIdentity, + .a = vk::ComponentSwizzle::eIdentity, + }, + .subresourceRange = + { + .aspectMask = vk::ImageAspectFlagBits::eColor, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + output_image_view = device.createImageView(output_view_create_info); + + LOG_DEBUG(Render_Vulkan, "Created LibRetro output texture: {}x{}", width, height); +} + +void PresentWindow::DestroyOutputTexture() { + if (!output_image) { + return; + } + + const vk::Device device = instance.GetDevice(); + + if (output_image_view) { + device.destroyImageView(output_image_view); + output_image_view = nullptr; + } + + if (output_allocation) { + vmaDestroyImage(instance.GetAllocator(), static_cast(output_image), + output_allocation); + output_allocation = {}; + } + + output_image = nullptr; + output_width = 0; + output_height = 0; +} + +vk::RenderPass PresentWindow::CreateRenderpass() { + const vk::AttachmentDescription color_attachment = { + .format = output_format, + .samples = vk::SampleCountFlagBits::e1, + .loadOp = vk::AttachmentLoadOp::eClear, + .storeOp = vk::AttachmentStoreOp::eStore, + .stencilLoadOp = vk::AttachmentLoadOp::eDontCare, + .stencilStoreOp = vk::AttachmentStoreOp::eDontCare, + .initialLayout = vk::ImageLayout::eUndefined, + .finalLayout = vk::ImageLayout::eShaderReadOnlyOptimal, // Ready for LibRetro + }; + + const vk::AttachmentReference color_ref = { + .attachment = 0, + .layout = vk::ImageLayout::eColorAttachmentOptimal, + }; + + const vk::SubpassDescription subpass = { + .pipelineBindPoint = vk::PipelineBindPoint::eGraphics, + .colorAttachmentCount = 1, + .pColorAttachments = &color_ref, + }; + + const vk::SubpassDependency dependency = { + .srcSubpass = VK_SUBPASS_EXTERNAL, + .dstSubpass = 0, + .srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput, + .srcAccessMask = {}, + .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, + }; + + const vk::RenderPassCreateInfo renderpass_info = { + .attachmentCount = 1, + .pAttachments = &color_attachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 1, + .pDependencies = &dependency, + }; + + return instance.GetDevice().createRenderPass(renderpass_info); +} + +void PresentWindow::CreateFrameResources() { + const vk::Device device = instance.GetDevice(); + const u32 frame_count = 2; // Double buffering for LibRetro + + // Destroy existing frames + DestroyFrameResources(); + + // Create frame pool + frame_pool.resize(frame_count); + + // Allocate command buffers + const vk::CommandBufferAllocateInfo alloc_info = { + .commandPool = command_pool, + .level = vk::CommandBufferLevel::ePrimary, + .commandBufferCount = frame_count, + }; + const std::vector command_buffers = device.allocateCommandBuffers(alloc_info); + + // Initialize frames + for (u32 i = 0; i < frame_count; i++) { + Frame& frame = frame_pool[i]; + frame.width = output_width; + frame.height = output_height; + frame.image = output_image; // All frames use the same output texture + frame.image_view = output_image_view; + frame.allocation = {}; // VMA allocation handled separately + frame.cmdbuf = command_buffers[i]; + frame.render_ready = device.createSemaphore({}); + frame.present_done = device.createFence({.flags = vk::FenceCreateFlagBits::eSignaled}); + + // Create framebuffer for this frame + const vk::FramebufferCreateInfo fb_info = { + .renderPass = present_renderpass, + .attachmentCount = 1, + .pAttachments = &output_image_view, + .width = output_width, + .height = output_height, + .layers = 1, + }; + frame.framebuffer = device.createFramebuffer(fb_info); + } + + LOG_DEBUG(Render_Vulkan, "Created {} frame resources for LibRetro", frame_count); +} + +void PresentWindow::DestroyFrameResources() { + if (frame_pool.empty()) { + return; + } + + const vk::Device device = instance.GetDevice(); + + for (auto& frame : frame_pool) { + if (frame.framebuffer) { + device.destroyFramebuffer(frame.framebuffer); + } + if (frame.render_ready) { + device.destroySemaphore(frame.render_ready); + } + if (frame.present_done) { + device.destroyFence(frame.present_done); + } + } + + frame_pool.clear(); + current_frame_index = 0; +} + +Frame* PresentWindow::GetRenderFrame() { + if (frame_pool.empty()) { + LOG_ERROR(Render_Vulkan, "No frames available in LibRetro PresentWindow"); + return nullptr; + } + + // RetroArch may not call context_reset during fullscreen toggle, leaving us + // with a stale interface pointer that can crash + const struct retro_hw_render_interface_vulkan* current_intf = nullptr; + if (!LibRetro::GetHWRenderInterface((void**)¤t_intf) || !current_intf) { + LOG_ERROR(Render_Vulkan, "Failed to get current Vulkan interface"); + return &frame_pool[current_frame_index]; + } + + // Update global interface if it changed + if (current_intf != vulkan_intf) { + LOG_INFO(Render_Vulkan, "Vulkan interface changed during runtime from {} to {}", + static_cast(vulkan_intf), static_cast(current_intf)); + vulkan_intf = current_intf; + } + + // LibRetro synchronization: Use LibRetro's wait mechanism instead of fences + if (vulkan_intf && vulkan_intf->wait_sync_index && vulkan_intf->handle) { + vulkan_intf->wait_sync_index(vulkan_intf->handle); + } + + // Use LibRetro's sync index for frame selection if available + u32 frame_index = current_frame_index; + if (vulkan_intf && vulkan_intf->get_sync_index && vulkan_intf->handle) { + LOG_TRACE(Render_Vulkan, "Calling get_sync_index with handle: {}", + static_cast(vulkan_intf->handle)); + + const u32 sync_index = vulkan_intf->get_sync_index(vulkan_intf->handle); + frame_index = sync_index % frame_pool.size(); + LOG_TRACE(Render_Vulkan, "LibRetro sync index: {}, using frame: {}", sync_index, + frame_index); + } + + return &frame_pool[frame_index]; +} + +void PresentWindow::RecreateFrame(Frame* frame, u32 width, u32 height) { + if (!frame) { + LOG_ERROR(Render_Vulkan, "Invalid frame for recreation"); + return; + } + + if (frame->width == width && frame->height == height) { + return; // No change needed + } + + LOG_DEBUG(Render_Vulkan, "Recreating LibRetro frame: {}x{} -> {}x{}", frame->width, + frame->height, width, height); + + // Wait for frame to be idle + const vk::Device device = instance.GetDevice(); + [[maybe_unused]] const vk::Result wait_result = + device.waitForFences(frame->present_done, VK_TRUE, UINT64_MAX); + + // Recreate output texture with new dimensions + CreateOutputTexture(width, height); + + // Recreate frame resources + CreateFrameResources(); + + LOG_INFO(Render_Vulkan, "LibRetro frame recreated for {}x{}", width, height); +} + +void PresentWindow::Present(Frame* frame) { + if (!frame) { + LOG_ERROR(Render_Vulkan, "Cannot present null frame"); + return; + } + + if (!vulkan_intf) { + LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for presentation"); + return; + } + + // CRITICAL: Use persistent struct to avoid stack lifetime issues! + // RetroArch may cache this pointer for frame duping during pause + persistent_libretro_image.image_view = static_cast(frame->image_view); + persistent_libretro_image.image_layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + persistent_libretro_image.create_info = + static_cast(output_view_create_info); + + vulkan_intf->set_image(vulkan_intf->handle, &persistent_libretro_image, 0, nullptr, + instance.GetGraphicsQueueFamilyIndex()); + + // Call EmuWindow SwapBuffers to trigger LibRetro video frame submission + emu_window.SwapBuffers(); + + // LibRetro manages frame indices via sync_index, so we don't manually increment + // current_frame_index = (current_frame_index + 1) % frame_pool.size(); + + LOG_TRACE(Render_Vulkan, "Frame presented to LibRetro: {}x{}", frame->width, frame->height); +} + +void PresentWindow::WaitPresent() { + if (frame_pool.empty()) { + return; + } + + const vk::Device device = instance.GetDevice(); + + // Wait for all frames to complete + std::vector fences; + fences.reserve(frame_pool.size()); + + for (const auto& frame : frame_pool) { + fences.push_back(frame.present_done); + } + + if (!fences.empty()) { + [[maybe_unused]] const vk::Result wait_result = + device.waitForFences(fences, VK_TRUE, UINT64_MAX); + } +} + +void PresentWindow::NotifySurfaceChanged() { + // LibRetro doesn't use surfaces, so this is a no-op + LOG_DEBUG(Render_Vulkan, "Surface change notification ignored in LibRetro mode"); +} + +// ============================================================================ +// MasterSemaphoreLibRetro Implementation +// ============================================================================ + +constexpr u64 FENCE_RESERVE = 8; + +MasterSemaphoreLibRetro::MasterSemaphoreLibRetro(const Instance& instance_) : instance{instance_} { + const vk::Device device{instance.GetDevice()}; + // Pre-allocate fence pool + for (u64 i = 0; i < FENCE_RESERVE; i++) { + free_queue.push_back(device.createFence({})); + } + // Start background wait thread + wait_thread = std::jthread([this](std::stop_token token) { WaitThread(token); }); +} + +MasterSemaphoreLibRetro::~MasterSemaphoreLibRetro() { + // wait_thread will be automatically stopped by jthread destructor + // Clean up remaining fences + const vk::Device device{instance.GetDevice()}; + for (const auto& fence : free_queue) { + device.destroyFence(fence); + } +} + +void MasterSemaphoreLibRetro::Refresh() {} + +void MasterSemaphoreLibRetro::Wait(u64 tick) { + std::unique_lock lock{free_mutex}; + free_cv.wait(lock, [this, tick] { return gpu_tick.load(std::memory_order_relaxed) >= tick; }); +} + +void MasterSemaphoreLibRetro::SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait, + vk::Semaphore signal, u64 signal_value) { + if (!vulkan_intf) { + LOG_ERROR(Render_Vulkan, "LibRetro Vulkan interface not available for command submission"); + return; + } + + cmdbuf.end(); + + // Get a fence from the pool + const vk::Fence fence = GetFreeFence(); + + // Strip semaphores - RetroArch handles frame sync, we track resources internally + const vk::SubmitInfo submit_info = { + .waitSemaphoreCount = 0, + .pWaitSemaphores = nullptr, + .pWaitDstStageMask = nullptr, + .commandBufferCount = 1u, + .pCommandBuffers = &cmdbuf, + .signalSemaphoreCount = 0, + .pSignalSemaphores = nullptr, + }; + + // Use LibRetro's queue coordination + if (vulkan_intf->lock_queue) { + vulkan_intf->lock_queue(vulkan_intf->handle); + } + + try { + // Submit with fence for internal resource tracking + vk::Queue queue{vulkan_intf->queue}; + queue.submit(submit_info, fence); + + if (vulkan_intf->unlock_queue) { + vulkan_intf->unlock_queue(vulkan_intf->handle); + } + } catch (vk::DeviceLostError& err) { + if (vulkan_intf->unlock_queue) { + vulkan_intf->unlock_queue(vulkan_intf->handle); + } + UNREACHABLE_MSG("Device lost during submit: {}", err.what()); + } catch (...) { + if (vulkan_intf->unlock_queue) { + vulkan_intf->unlock_queue(vulkan_intf->handle); + } + throw; + } + + // Enqueue fence for wait thread to process + { + std::scoped_lock lock{wait_mutex}; + wait_queue.emplace(fence, signal_value); + wait_cv.notify_one(); + } +} + +void MasterSemaphoreLibRetro::WaitThread(std::stop_token token) { + const vk::Device device{instance.GetDevice()}; + + while (!token.stop_requested()) { + vk::Fence fence; + u64 signal_value; + + // Wait for work + { + std::unique_lock lock{wait_mutex}; + Common::CondvarWait(wait_cv, lock, token, [this] { return !wait_queue.empty(); }); + if (token.stop_requested()) { + return; + } + std::tie(fence, signal_value) = wait_queue.front(); + wait_queue.pop(); + } + + // Wait for fence (blocks only this background thread) + const vk::Result result = device.waitForFences(fence, true, UINT64_MAX); + if (result != vk::Result::eSuccess) { + LOG_ERROR(Render_Vulkan, "Fence wait failed: {}", vk::to_string(result)); + } + + // Reset fence and return to pool + device.resetFences(fence); + + // Update GPU tick - signals main thread's Wait() + gpu_tick.store(signal_value, std::memory_order_release); + + // Return fence to pool + { + std::scoped_lock lock{free_mutex}; + free_queue.push_back(fence); + free_cv.notify_all(); + } + } +} + +vk::Fence MasterSemaphoreLibRetro::GetFreeFence() { + std::scoped_lock lock{free_mutex}; + if (free_queue.empty()) { + // Pool exhausted - create new fence + return instance.GetDevice().createFence({}); + } + + const vk::Fence fence = free_queue.front(); + free_queue.pop_front(); + return fence; +} + +// Factory function for scheduler to create LibRetro MasterSemaphore +std::unique_ptr CreateLibRetroMasterSemaphore(const Instance& instance) { + return std::make_unique(instance); +} + +} // namespace Vulkan diff --git a/src/citra_libretro/libretro_vk.h b/src/citra_libretro/libretro_vk.h new file mode 100644 index 000000000..c51075e21 --- /dev/null +++ b/src/citra_libretro/libretro_vk.h @@ -0,0 +1,175 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +#include "common/common_types.h" +#include "common/dynamic_library/dynamic_library.h" +#include "video_core/renderer_vulkan/vk_common.h" +#include "video_core/renderer_vulkan/vk_instance.h" +#include "video_core/renderer_vulkan/vk_master_semaphore.h" +#include "video_core/renderer_vulkan/vk_platform.h" + +#include "libretro_vulkan.h" + +VK_DEFINE_HANDLE(VmaAllocation) + +namespace LibRetro { + +extern void VulkanResetContext(); + +/// Returns VkApplicationInfo for negotiation interface +const VkApplicationInfo* GetVulkanApplicationInfo(); + +/// CreateDevice callback for negotiation interface +bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instance, + VkPhysicalDevice gpu, VkSurfaceKHR surface, + PFN_vkGetInstanceProcAddr get_instance_proc_addr, + const char** required_device_extensions, + unsigned num_required_device_extensions, + const char** required_device_layers, unsigned num_required_device_layers, + const VkPhysicalDeviceFeatures* required_features); + +} // namespace LibRetro + +namespace Vulkan { + +class LibRetroVKInstance : public Instance { +public: + explicit LibRetroVKInstance(Frontend::EmuWindow& window, u32 physical_device_index); + + /// Returns the Vulkan instance + vk::Instance GetInstance() const override; + + /// Returns the Vulkan device + vk::Device GetDevice() const override; +}; + +class Scheduler; +class RenderManager; +class MasterSemaphore; + +/// LibRetro-specific MasterSemaphore implementation +class MasterSemaphoreLibRetro : public MasterSemaphore { + using Waitable = std::pair; + +public: + explicit MasterSemaphoreLibRetro(const Instance& instance); + ~MasterSemaphoreLibRetro() override; + + void Refresh() override; + void Wait(u64 tick) override; + void SubmitWork(vk::CommandBuffer cmdbuf, vk::Semaphore wait, vk::Semaphore signal, + u64 signal_value) override; + +private: + void WaitThread(std::stop_token token); + vk::Fence GetFreeFence(); + + const Instance& instance; + std::deque free_queue; + std::queue wait_queue; + std::mutex free_mutex; + std::mutex wait_mutex; + std::condition_variable free_cv; + std::condition_variable_any wait_cv; + std::jthread wait_thread; +}; + +/// Factory function for scheduler to create LibRetro MasterSemaphore +std::unique_ptr CreateLibRetroMasterSemaphore(const Instance& instance); + +struct Frame { + u32 width; + u32 height; + VmaAllocation allocation; + vk::Framebuffer framebuffer; + vk::Image image; + vk::ImageView image_view; + vk::Semaphore render_ready; + vk::Fence present_done; + vk::CommandBuffer cmdbuf; +}; + +/// LibRetro-specific PresentWindow implementation (same interface as desktop version) +class PresentWindow final { +public: + explicit PresentWindow(Frontend::EmuWindow& emu_window, const Instance& instance, + Scheduler& scheduler, bool low_refresh_rate); + ~PresentWindow(); + + /// Waits for all queued frames to finish presenting. + void WaitPresent(); + + /// Returns the last used render frame. + Frame* GetRenderFrame(); + + /// Recreates the render frame to match provided parameters. + void RecreateFrame(Frame* frame, u32 width, u32 height); + + /// Queues the provided frame for presentation. + void Present(Frame* frame); + + /// This is called to notify the rendering backend of a surface change + void NotifySurfaceChanged(); + + [[nodiscard]] vk::RenderPass Renderpass() const noexcept { + return present_renderpass; + } + + u32 ImageCount() const noexcept { + return static_cast(frame_pool.size()); + } + +private: + /// Creates the render pass for LibRetro output + vk::RenderPass CreateRenderpass(); + + /// Creates output texture for LibRetro submission + void CreateOutputTexture(u32 width, u32 height); + + /// Destroys current output texture + void DestroyOutputTexture(); + + /// Creates frame resources + void CreateFrameResources(); + + /// Destroys frame resources + void DestroyFrameResources(); + +private: + Frontend::EmuWindow& emu_window; + const Instance& instance; + [[maybe_unused]] Scheduler& scheduler; + + // LibRetro output texture (replaces swapchain) + vk::Image output_image{}; + vk::ImageView output_image_view{}; + VmaAllocation output_allocation{}; + vk::Format output_format{vk::Format::eR8G8B8A8Unorm}; + vk::ImageViewCreateInfo output_view_create_info{}; + + // Frame management + vk::RenderPass present_renderpass{}; + vk::CommandPool command_pool{}; + std::vector frame_pool; + u32 current_frame_index{0}; + + // Current output dimensions + u32 output_width{0}; + u32 output_height{0}; + + // Vulkan objects + vk::Queue graphics_queue{}; + + // Persistent LibRetro image descriptor, must persist across frames for RetroArch frame duping + // during pause + retro_vulkan_image persistent_libretro_image{}; +}; + +} // namespace Vulkan diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 6bb14fc9e..4d7981d82 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -63,8 +63,6 @@ add_library(citra_common STATIC aarch64/oaknut_abi.h aarch64/oaknut_util.h alignment.h - android_storage.h - android_storage.cpp announce_multiplayer_room.h arch.h archives.h @@ -172,7 +170,7 @@ if (UNIX AND NOT APPLE) target_link_libraries(citra_common PRIVATE gamemode) endif() -if (APPLE) +if (APPLE AND NOT ENABLE_LIBRETRO) target_sources(citra_common PUBLIC apple_authorization.h apple_authorization.cpp @@ -181,6 +179,14 @@ if (APPLE) ) endif() +# Android storage is only used for non-libretro Android builds +if (ANDROID AND NOT ENABLE_LIBRETRO) + target_sources(citra_common PRIVATE + android_storage.cpp + android_storage.h + ) +endif() + if (MSVC) target_compile_options(citra_common PRIVATE /W4 diff --git a/src/common/error.cpp b/src/common/error.cpp index 1d7467f68..def285115 100644 --- a/src/common/error.cpp +++ b/src/common/error.cpp @@ -1,4 +1,8 @@ -// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +// Copyright 2013 Dolphin Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -31,7 +35,7 @@ std::string NativeErrorToString(int e) { #else char err_str[255]; #if defined(__GLIBC__) && (_GNU_SOURCE || (_POSIX_C_SOURCE < 200112L && _XOPEN_SOURCE < 600)) || \ - defined(ANDROID) + (defined(ANDROID) && !defined(HAVE_LIBRETRO)) // Thread safe (GNU-specific) const char* str = strerror_r(e, err_str, sizeof(err_str)); return std::string(str); diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 79c0afaca..e42fcd785 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -43,6 +43,12 @@ #define fseeko _fseeki64 #define ftello _ftelli64 #define fileno _fileno +typedef struct _stat64 file_stat_t; +#define fstat _fstat64 +#elif defined(HAVE_LIBRETRO) +typedef struct _stat64 file_stat_t; +#else +typedef struct stat file_stat_t; #endif #else @@ -56,6 +62,7 @@ #include #include #include +typedef struct stat file_stat_t; #endif #if defined(__APPLE__) @@ -75,7 +82,7 @@ #endif -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) #include "common/android_storage.h" #include "common/string_util.h" #endif @@ -87,6 +94,36 @@ #define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) #endif +#ifdef HAVE_LIBRETRO_VFS +#define SKIP_STDIO_REDEFINES +#include +#include + +#define FILE RFILE +#define FTELL rftell +#define FOPEN rfopen +#define FCLOSE rfclose +#define FSEEK rfseek +#define FREAD rfread +#define FWRITE rfwrite +#define FEOF rfeof +#define FERROR rferror +#define FFLUSH rfflush + +#else + +#define FTELL ftello +#define FOPEN fopen +#define FCLOSE std::fclose +#define FSEEK fseeko +#define FREAD std::fread +#define FWRITE std::fwrite +#define FEOF feof +#define FERROR ferror +#define FFLUSH std::fflush + +#endif + // This namespace has various generic functions related to files and paths. // The code still needs a ton of cleanup. // REMEMBER: strdup considered harmful! @@ -119,7 +156,7 @@ bool Exists(const std::string& filename) { copy += DIR_SEP_CHR; int result = _wstat64(Common::UTF8ToUTF16W(copy).c_str(), &file_info); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) int result = AndroidStorage::FileExists(filename) ? 0 : -1; #else struct stat file_info; @@ -130,7 +167,7 @@ bool Exists(const std::string& filename) { } bool IsDirectory(const std::string& filename) { -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) return AndroidStorage::IsDirectory(filename); #endif @@ -178,7 +215,7 @@ bool Delete(const std::string& filename) { LOG_ERROR(Common_Filesystem, "DeleteFile failed on {}: {}", filename, GetLastErrorMsg()); return false; } -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) if (!AndroidStorage::DeleteDocument(filename)) { LOG_ERROR(Common_Filesystem, "unlink failed on {}", filename); return false; @@ -205,7 +242,7 @@ bool CreateDir(const std::string& path) { } LOG_ERROR(Common_Filesystem, "CreateDirectory failed on {}: {}", path, error); return false; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) std::string directory = path; std::string filename = path; if (Common::EndsWith(path, "/")) { @@ -292,7 +329,7 @@ bool DeleteDir(const std::string& filename) { #ifdef _WIN32 if (::RemoveDirectoryW(Common::UTF8ToUTF16W(filename).c_str())) return true; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) if (AndroidStorage::DeleteDocument(filename)) return true; #else @@ -310,7 +347,7 @@ bool Rename(const std::string& srcFullPath, const std::string& destFullPath) { if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(), Common::UTF8ToUTF16W(destFullPath).c_str()) == 0) return true; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) // srcFullPath and destFullPath are relative to the user directory if (AndroidStorage::GetBuildFlavor() == AndroidStorage::AndroidBuildFlavors::GOOGLEPLAY) { if (AndroidStorage::MoveAndRenameFile(srcFullPath, destFullPath)) @@ -343,36 +380,36 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) { LOG_ERROR(Common_Filesystem, "failed {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) return AndroidStorage::CopyFile(srcFilename, std::string(GetParentPath(destFilename)), std::string(GetFilename(destFilename))); #else // Open input file - FILE* input = fopen(srcFilename.c_str(), "rb"); + FILE* input = FOPEN(srcFilename.c_str(), "rb"); if (!input) { LOG_ERROR(Common_Filesystem, "opening input failed {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; } - SCOPE_EXIT({ fclose(input); }); + SCOPE_EXIT({ FCLOSE(input); }); // open output file - FILE* output = fopen(destFilename.c_str(), "wb"); + FILE* output = FOPEN(destFilename.c_str(), "wb"); if (!output) { LOG_ERROR(Common_Filesystem, "opening output failed {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; } - SCOPE_EXIT({ fclose(output); }); + SCOPE_EXIT({ FCLOSE(output); }); // copy loop std::array buffer; - while (!feof(input)) { + while (!FEOF(input)) { // read input - std::size_t rnum = fread(buffer.data(), sizeof(char), buffer.size(), input); + std::size_t rnum = FREAD(buffer.data(), sizeof(char), buffer.size(), input); if (rnum != buffer.size()) { - if (ferror(input) != 0) { + if (FERROR(input) != 0) { LOG_ERROR(Common_Filesystem, "failed reading from source, {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); return false; @@ -380,7 +417,7 @@ bool Copy(const std::string& srcFilename, const std::string& destFilename) { } // write output - std::size_t wnum = fwrite(buffer.data(), sizeof(char), rnum, output); + std::size_t wnum = FWRITE(buffer.data(), sizeof(char), rnum, output); if (wnum != rnum) { LOG_ERROR(Common_Filesystem, "failed writing to output, {} --> {}: {}", srcFilename, destFilename, GetLastErrorMsg()); @@ -408,7 +445,7 @@ u64 GetSize(const std::string& filename) { #ifdef _WIN32 struct _stat64 buf; if (_wstat64(Common::UTF8ToUTF16W(filename).c_str(), &buf) == 0) -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) u64 result = AndroidStorage::GetSize(filename); LOG_TRACE(Common_Filesystem, "{}: {}", filename, result); return result; @@ -425,7 +462,7 @@ u64 GetSize(const std::string& filename) { } u64 GetSize(const int fd) { - struct stat buf; + file_stat_t buf; if (fstat(fd, &buf) != 0) { LOG_ERROR(Common_Filesystem, "GetSize: stat failed {}: {}", fd, GetLastErrorMsg()); return 0; @@ -435,13 +472,13 @@ u64 GetSize(const int fd) { u64 GetSize(FILE* f) { // can't use off_t here because it can be 32-bit - u64 pos = ftello(f); - if (fseeko(f, 0, SEEK_END) != 0) { + u64 pos = FTELL(f); + if (FSEEK(f, 0, SEEK_END) != 0) { LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg()); return 0; } - u64 size = ftello(f); - if ((size != pos) && (fseeko(f, pos, SEEK_SET) != 0)) { + u64 size = FTELL(f); + if ((size != pos) && (FSEEK(f, pos, SEEK_SET) != 0)) { LOG_ERROR(Common_Filesystem, "GetSize: seek failed {}: {}", fmt::ptr(f), GetLastErrorMsg()); return 0; } @@ -481,7 +518,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, // windows loop do { const std::string virtual_name(Common::UTF16ToUTF8(ffd.cFileName)); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) // android loop auto result = AndroidStorage::GetFilesName(directory); for (auto virtual_name : result) { @@ -508,7 +545,7 @@ bool ForeachDirectoryEntry(u64* num_entries_out, const std::string& directory, #ifdef _WIN32 } while (FindNextFileW(handle_find, &ffd) != 0); FindClose(handle_find); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) } #else } @@ -605,7 +642,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path, if (!FileUtil::Exists(dest_path)) FileUtil::CreateFullPath(dest_path); -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) auto result = AndroidStorage::GetFilesName(source_path); for (auto virtualName : result) { #else @@ -635,7 +672,7 @@ void CopyDir([[maybe_unused]] const std::string& source_path, FileUtil::Copy(source, dest); } -#ifndef ANDROID +#if !(defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS)) closedir(dirp); #endif // ANDROID #endif // _WIN32 @@ -811,7 +848,7 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) user_path = "/"; g_paths.emplace(UserPath::ConfigDir, user_path + CONFIG_DIR DIR_SEP); g_paths.emplace(UserPath::CacheDir, user_path + CACHE_DIR DIR_SEP); @@ -1070,7 +1107,7 @@ std::string_view RemoveTrailingSlash(std::string_view path) { std::string SanitizePath(std::string_view path_, DirectorySeparator directory_separator) { std::string path(path_); -#ifdef ANDROID +#if defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) return std::string(RemoveTrailingSlash(path)); #endif char type1 = directory_separator == DirectorySeparator::BackwardSlash ? '/' : '\\'; @@ -1137,7 +1174,7 @@ bool IOFile::Open() { Common::UTF8ToUTF16W(openmode).c_str(), flags); m_good = m_file != nullptr; -#elif ANDROID +#elif defined(ANDROID) && !defined(HAVE_LIBRETRO_VFS) // Check whether filepath is startsWith content AndroidStorage::AndroidOpenMode android_open_mode = AndroidStorage::ParseOpenmode(openmode); if (android_open_mode == AndroidStorage::AndroidOpenMode::WRITE || @@ -1168,7 +1205,7 @@ bool IOFile::Open() { m_good = m_file != nullptr; #else - m_file = std::fopen(filename.c_str(), openmode.c_str()); + m_file = FOPEN(filename.c_str(), openmode.c_str()); m_good = m_file != nullptr; #endif @@ -1176,7 +1213,7 @@ bool IOFile::Open() { } bool IOFile::Close() { - if (!IsOpen() || 0 != std::fclose(m_file)) + if (!IsOpen() || 0 != FCLOSE(m_file)) m_good = false; m_file = nullptr; @@ -1191,7 +1228,7 @@ u64 IOFile::GetSize() const { } bool IOFile::SeekImpl(s64 off, int origin) { - if (!IsOpen() || 0 != fseeko(m_file, off, origin)) + if (!IsOpen() || 0 != FSEEK(m_file, off, origin)) m_good = false; return m_good; @@ -1199,13 +1236,13 @@ bool IOFile::SeekImpl(s64 off, int origin) { u64 IOFile::TellImpl() const { if (IsOpen()) - return ftello(m_file); + return FTELL(m_file); return std::numeric_limits::max(); } bool IOFile::Flush() { - if (!IsOpen() || 0 != std::fflush(m_file)) + if (!IsOpen() || 0 != FFLUSH(m_file)) m_good = false; return m_good; @@ -1223,7 +1260,7 @@ std::size_t IOFile::ReadImpl(void* data, std::size_t length, std::size_t data_si DEBUG_ASSERT(data != nullptr); - return std::fread(data, data_size, length, m_file); + return FREAD(data, data_size, length, m_file); } #ifdef _WIN32 @@ -1266,7 +1303,16 @@ std::size_t IOFile::ReadAtImpl(void* data, std::size_t byte_count, std::size_t o DEBUG_ASSERT(data != nullptr); +#ifdef HAVE_LIBRETRO_VFS + std::scoped_lock lock(m_file_pos_mutex); + int64_t pos = filestream_tell(m_file); + FSEEK(m_file, offset, RETRO_VFS_SEEK_POSITION_START); + int64_t rv = FREAD(data, 1, byte_count, m_file); + FSEEK(m_file, pos, RETRO_VFS_SEEK_POSITION_START); + return rv; +#else return pread(fileno(m_file), data, byte_count, offset); +#endif } std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t data_size) { @@ -1281,12 +1327,18 @@ std::size_t IOFile::WriteImpl(const void* data, std::size_t length, std::size_t DEBUG_ASSERT(data != nullptr); +#if defined(HAVE_LIBRETRO_VFS) + return rfwrite(data, data_size, length, m_file) / data_size; +#else return std::fwrite(data, data_size, length, m_file); +#endif } bool IOFile::Resize(u64 size) { if (!IsOpen() || 0 != -#ifdef _WIN32 +#if defined(HAVE_LIBRETRO_VFS) + filestream_truncate(m_file, size) +#elif defined(_WIN32) // ector: _chsize sucks, not 64-bit safe // F|RES: changed to _chsize_s. i think it is 64-bit safe _chsize_s(_fileno(m_file), size) diff --git a/src/common/file_util.h b/src/common/file_util.h index 98d232dcf..58cc4f7aa 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,14 @@ #include "common/string_util.h" #endif +#ifdef HAVE_LIBRETRO_VFS +#define SKIP_STDIO_REDEFINES +#include +#define CORE_FILE RFILE +#else +#define CORE_FILE std::FILE +#endif + namespace FileUtil { // User paths for GetUserPath @@ -120,7 +129,7 @@ private: [[nodiscard]] u64 GetSize(int fd); // Overloaded GetSize, accepts FILE* -[[nodiscard]] u64 GetSize(FILE* f); +[[nodiscard]] u64 GetSize(CORE_FILE* f); // Returns true if successful, or path already exists. bool CreateDir(const std::string& filename); @@ -423,7 +432,11 @@ public: return m_good; } [[nodiscard]] virtual int GetFd() const { -#ifdef ANDROID +#ifdef HAVE_LIBRETRO_VFS + if (m_file == nullptr) + return -1; + return fileno(filestream_get_vfs_handle(m_file)->fp); +#elif defined(ANDROID) return m_fd; #else if (m_file == nullptr) @@ -448,7 +461,12 @@ public: // clear error state virtual void Clear() { m_good = true; + +#ifdef HAVE_LIBRETRO_VFS + filestream_rewind(m_file); +#else std::clearerr(m_file); +#endif } virtual bool IsCrypto() { @@ -476,9 +494,16 @@ protected: virtual u64 TellImpl() const; private: - std::FILE* m_file = nullptr; + CORE_FILE* m_file = nullptr; int m_fd = -1; bool m_good = true; +#ifdef HAVE_LIBRETRO_VFS + // pread() doesn't touch the file position, so it's safe alongside + // concurrent fread/fwrite. Libretro VFS has no pread equivalent, so + // ReadAtImpl emulates it with seek+read+seek, which would corrupt the + // file position for concurrent Read/Write operations. + mutable std::mutex m_file_pos_mutex; +#endif std::string filename; std::string openmode; @@ -547,4 +572,4 @@ void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmod } BOOST_CLASS_EXPORT_KEY(FileUtil::IOFile) -BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile) \ No newline at end of file +BOOST_CLASS_EXPORT_KEY(FileUtil::CryptoIOFile) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index 36fcfeccf..e1b8621bd 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -55,6 +55,61 @@ public: virtual void Close() = 0; }; +#ifdef HAVE_LIBRETRO +/** + * LibRetro backend + */ +class LibRetroBackend : public Backend { +public: + explicit LibRetroBackend() {} + explicit LibRetroBackend(retro_log_printf_t callback) : callback(callback) {} + + ~LibRetroBackend() override = default; + + void Write(const Entry& entry) override { + if (callback == nullptr) { + return; + } + retro_log_level log_level; + + switch (entry.log_level) { + case Common::Log::Level::Trace: + log_level = retro_log_level::RETRO_LOG_DEBUG; + break; + case Common::Log::Level::Debug: + log_level = retro_log_level::RETRO_LOG_DEBUG; + break; + case Common::Log::Level::Info: + log_level = retro_log_level::RETRO_LOG_INFO; + break; + case Common::Log::Level::Warning: + log_level = retro_log_level::RETRO_LOG_WARN; + break; + case Common::Log::Level::Error: + log_level = retro_log_level::RETRO_LOG_ERROR; + break; + case Common::Log::Level::Critical: + log_level = retro_log_level::RETRO_LOG_ERROR; + break; + default: + log_level = retro_log_level::RETRO_LOG_DUMMY; + } + + auto str = FormatLogMessage(entry).append(1, '\n'); + callback(log_level, str.c_str()); + } + + void Flush() override {} + + void Close() override {} + + void EnableForStacktrace() override {} + +private: + retro_log_printf_t callback = nullptr; +}; +#endif + /** * Backend that writes to stderr and with color */ @@ -218,7 +273,19 @@ public: } return *instance; } - +#ifdef HAVE_LIBRETRO + static void Initialize(retro_log_printf_t callback) { + if (instance) { + LOG_WARNING(Log, "Reinitializing logging backend"); + return; + } + initialization_in_progress_suppress_logging = true; + Filter filter; + filter.ParseFilterString(Settings::values.log_filter.GetValue()); + instance = std::unique_ptr(new Impl(callback, filter), Deleter); + initialization_in_progress_suppress_logging = false; + } +#endif static void Initialize(std::string_view log_file) { if (instance) { LOG_WARNING(Log, "Reinitializing logging backend"); @@ -310,6 +377,10 @@ public: } private: +#ifdef HAVE_LIBRETRO + Impl(retro_log_printf_t callback, const Filter& filter_) + : filter{filter_}, file_backend{""}, libretro_backend{callback} {} +#endif Impl(const std::string& file_backend_filename, const Filter& filter_) : filter{filter_}, file_backend{file_backend_filename} { #ifdef CITRA_LINUX_GCC_BACKTRACE @@ -412,12 +483,16 @@ private: } void ForEachBackend(auto lambda) { +#ifdef HAVE_LIBRETRO + lambda(static_cast(libretro_backend)); +#else lambda(static_cast(debugger_backend)); lambda(static_cast(color_console_backend)); lambda(static_cast(file_backend)); #ifdef ANDROID lambda(static_cast(lc_backend)); -#endif +#endif // ANDROID +#endif // HAVE_LIBRETRO } static void Deleter(Impl* ptr) { @@ -464,6 +539,9 @@ private: #ifdef ANDROID LogcatBackend lc_backend{}; #endif +#ifdef HAVE_LIBRETRO + LibRetroBackend libretro_backend; +#endif MPSCQueue message_queue{}; std::chrono::steady_clock::time_point time_origin{std::chrono::steady_clock::now()}; @@ -478,6 +556,13 @@ private: }; } // namespace +#ifdef HAVE_LIBRETRO +void LibRetroStart(retro_log_printf_t callback) { + Impl::Initialize(callback); + Impl::Start(); +} +#endif + void Initialize(std::string_view log_file) { Impl::Initialize(log_file.empty() ? LOG_FILE : log_file); } diff --git a/src/common/logging/backend.h b/src/common/logging/backend.h index e16a129b1..c87bff2dc 100644 --- a/src/common/logging/backend.h +++ b/src/common/logging/backend.h @@ -1,4 +1,4 @@ -// Copyright 2014 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -6,6 +6,9 @@ #include #include "common/logging/filter.h" +#ifdef HAVE_LIBRETRO +#include "libretro.h" +#endif namespace Common::Log { @@ -13,6 +16,9 @@ class Filter; /// Initializes the logging system. This should be the first thing called in main. void Initialize(std::string_view log_file = ""); +#ifdef HAVE_LIBRETRO +void LibRetroStart(retro_log_printf_t callback); +#endif void Start(); diff --git a/src/common/zstd_compression.cpp b/src/common/zstd_compression.cpp index 3257fa2d8..20840e3ac 100644 --- a/src/common/zstd_compression.cpp +++ b/src/common/zstd_compression.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include diff --git a/src/core/core.h b/src/core/core.h index 085a7b3b0..d493e8491 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -362,6 +362,10 @@ public: void LoadState(u32 slot); + std::vector SaveStateBuffer() const; + + bool LoadStateBuffer(std::vector buffer); + /// Self delete ncch bool SetSelfDelete(const std::string& file) { if (m_filepath == file) { diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index d5b7803cc..1f0fec7b7 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -27,6 +27,7 @@ enum class WindowSystemType : u8 { MacOS, X11, Wayland, + LibRetro, }; struct Frame; @@ -258,6 +259,16 @@ public: return is_secondary; } + /** + * Requests for a frontend to setup a framebuffer. + */ + virtual void SetupFramebuffer() {} + + /// Flags that the framebuffer should be cleared. + virtual bool NeedsClearing() const { + return true; + } + Settings::StereoRenderOption get3DMode() const; protected: diff --git a/src/core/hle/service/cfg/cfg.cpp b/src/core/hle/service/cfg/cfg.cpp index 070612250..8a34237c6 100644 --- a/src/core/hle/service/cfg/cfg.cpp +++ b/src/core/hle/service/cfg/cfg.cpp @@ -32,6 +32,9 @@ #include "core/hle/service/cfg/cfg_u.h" #include "core/hw/unique_data.h" #include "core/loader/loader.h" +#ifdef HAVE_LIBRETRO +#include "citra_libretro/core_settings.h" +#endif SERVICE_CONSTRUCT_IMPL(Service::CFG::Module) SERIALIZE_EXPORT_IMPL(Service::CFG::Module) @@ -1058,6 +1061,11 @@ void Module::UpdatePreferredRegionCode() { if (preferred_region_chosen || !system.IsPoweredOn()) { return; } +#ifdef HAVE_LIBRETRO + // Apply language set in core options first + SetSystemLanguage(LibRetro::settings.language_value); +#endif + preferred_region_chosen = true; const auto preferred_regions = system.GetAppLoader().GetPreferredRegions(); diff --git a/src/core/hle/service/mic/mic_u.cpp b/src/core/hle/service/mic/mic_u.cpp index d15c077bd..f6fee7784 100644 --- a/src/core/hle/service/mic/mic_u.cpp +++ b/src/core/hle/service/mic/mic_u.cpp @@ -214,7 +214,6 @@ struct MIC_U::Impl { LOG_CRITICAL(Service_MIC, "Application started sampling again before stopping sampling"); mic->StopSampling(); - mic.reset(); } u8 sample_size = encoding == Encoding::PCM8Signed || encoding == Encoding::PCM8 ? 8 : 16; @@ -225,7 +224,9 @@ struct MIC_U::Impl { state.looped_buffer = audio_buffer_loop; state.size = audio_buffer_size; - CreateMic(); + if (!mic) { + CreateMic(); + } StartSampling(); timing.ScheduleEvent(GetBufferUpdatePeriod(state.sample_rate), buffer_write_event); @@ -259,7 +260,6 @@ struct MIC_U::Impl { timing.RemoveEvent(buffer_write_event); if (mic) { mic->StopSampling(); - mic.reset(); } LOG_TRACE(Service_MIC, "called"); } diff --git a/src/core/hle/service/soc/soc_u.cpp b/src/core/hle/service/soc/soc_u.cpp index 23bfb5aec..d4ae261bf 100644 --- a/src/core/hle/service/soc/soc_u.cpp +++ b/src/core/hle/service/soc/soc_u.cpp @@ -2308,7 +2308,9 @@ std::optional SOC_U::GetDefaultInterfaceInfo() { break; } } -#else +#elif !(defined(ANDROID) && defined(HAVE_LIBRETRO)) + // Libretro Android builds target API 21, but getifaddrs() requires API 24+. + // Standalone Android (minSdk 29) and other platforms have getifaddrs(). struct ifaddrs* ifaddr; struct ifaddrs* ifa; if (getifaddrs(&ifaddr) == -1) { diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp index 3390af273..573862d81 100644 --- a/src/core/savestate.cpp +++ b/src/core/savestate.cpp @@ -1,4 +1,4 @@ -// Copyright 2020 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -217,4 +217,77 @@ void System::LoadState(u32 slot) { ia&* this; } +std::vector System::SaveStateBuffer() const { + std::ostringstream sstream{std::ios_base::binary}; + // Serialize + oarchive oa{sstream}; + oa&* this; + + const std::string& str{sstream.str()}; + const auto data = std::span{reinterpret_cast(str.data()), str.size()}; + auto buffer = Common::Compression::CompressDataZSTDDefault(data); + + CSTHeader header{}; + header.filetype = header_magic_bytes; + header.program_id = title_id; + std::string rev_bytes; + CryptoPP::StringSource ss(Common::g_scm_rev, true, + new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes))); + std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision)); + header.time = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + const std::string build_fullname = Common::g_build_fullname; + std::memset(header.build_name.data(), 0, sizeof(header.build_name)); + std::memcpy(header.build_name.data(), build_fullname.c_str(), + std::min(build_fullname.length(), sizeof(header.build_name) - 1)); + + std::vector result((u8*)&header, (u8*)&header + sizeof(header)); + std::copy(buffer.begin(), buffer.end(), std::back_inserter(result)); + + return result; +} + +bool System::LoadStateBuffer(std::vector buffer) { + CSTHeader header; + + if (buffer.size() < sizeof(header)) { + LOG_ERROR(Core, "Save state too small"); + return false; + } + + header = *((CSTHeader*)buffer.data()); + + if (header.filetype != header_magic_bytes) { + LOG_ERROR(Core, "Invalid save state"); + return false; + } + + if (header.program_id != title_id) { + LOG_ERROR(Core, "Save state isn't for the current game"); + return false; + } + std::string revision = fmt::format("{:02x}", fmt::join(header.revision, "")); + if (revision != Common::g_scm_rev) { + LOG_ERROR(Core, + "Save state file created from a different revision (core: {}, savestate: {})", + Common::g_scm_rev, revision); + return false; + } + + std::vector state(buffer.begin() + sizeof(CSTHeader), buffer.end()); + auto decompressed = Common::Compression::DecompressDataZSTD(state); + + std::istringstream sstream{ + std::string{reinterpret_cast(decompressed.data()), decompressed.size()}, + std::ios_base::binary}; + decompressed.clear(); + + // Deserialize + iarchive ia{sstream}; + ia&* this; + + return true; +} + } // namespace Core diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 4ac9368b1..f248ece4b 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -26,6 +26,10 @@ create_target_directory_groups(tests) target_link_libraries(tests PRIVATE citra_common citra_core video_core audio_core) target_link_libraries(tests PRIVATE ${PLATFORM_LIBRARIES} catch2 nihstro-headers Threads::Threads) +if (ENABLE_LIBRETRO) + target_link_libraries(tests PRIVATE $) +endif() + add_test(NAME tests COMMAND tests) if (CITRA_USE_PRECOMPILED_HEADERS) diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 3da82159e..a8ade344d 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -183,10 +183,10 @@ if (ENABLE_VULKAN) renderer_vulkan/vk_instance.h renderer_vulkan/vk_pipeline_cache.cpp renderer_vulkan/vk_pipeline_cache.h - renderer_vulkan/vk_platform.cpp + $<$>:renderer_vulkan/vk_platform.cpp> renderer_vulkan/vk_platform.h - renderer_vulkan/vk_present_window.cpp - renderer_vulkan/vk_present_window.h + $<$>:renderer_vulkan/vk_present_window.cpp> + $<$>:renderer_vulkan/vk_present_window.h> renderer_vulkan/vk_render_manager.cpp renderer_vulkan/vk_render_manager.h renderer_vulkan/vk_shader_disk_cache.cpp @@ -195,8 +195,8 @@ if (ENABLE_VULKAN) renderer_vulkan/vk_shader_util.h renderer_vulkan/vk_stream_buffer.cpp renderer_vulkan/vk_stream_buffer.h - renderer_vulkan/vk_swapchain.cpp - renderer_vulkan/vk_swapchain.h + $<$>:renderer_vulkan/vk_swapchain.cpp> + $<$>:renderer_vulkan/vk_swapchain.h> renderer_vulkan/vk_texture_runtime.cpp renderer_vulkan/vk_texture_runtime.h shader/generator/spv_fs_shader_gen.cpp diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp index 37272d4ad..9a76f4859 100644 --- a/src/video_core/gpu.cpp +++ b/src/video_core/gpu.cpp @@ -9,6 +9,7 @@ #include "core/core_timing.h" #include "core/hle/service/gsp/gsp_gpu.h" #include "core/hle/service/plgldr/plgldr.h" +#include "core/loader/loader.h" #include "video_core/debug_utils/debug_utils.h" #include "video_core/gpu.h" #include "video_core/gpu_debugger.h" @@ -421,6 +422,53 @@ void GPU::VBlankCallback(std::uintptr_t user_data, s64 cycles_late) { impl->timing.ScheduleEvent(FRAME_TICKS - cycles_late, impl->vblank_event); } +void GPU::RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window) { + // Reset the renderer (this will destroy OpenGL resources) + impl->renderer.reset(); + + // Create a new renderer + impl->renderer = + VideoCore::CreateRenderer(emu_window, secondary_window, impl->pica, impl->system); + impl->rasterizer = impl->renderer->Rasterizer(); + + // Rebind the rasterizer to the PICA GPU + impl->pica.BindRasterizer(impl->rasterizer); + + // Update the sw_blitter with the new rasterizer + impl->sw_blitter = std::make_unique(impl->memory, impl->rasterizer); + + // Re-apply per-game configuration and reload disk shader cache + u64 program_id{}; + impl->system.GetAppLoader().ReadProgramId(program_id); + ApplyPerProgramSettings(program_id); + if (Settings::values.use_disk_shader_cache) { + impl->renderer->Rasterizer()->LoadDefaultDiskResources(false, nullptr); + } + + // Mark ALL GPU registers as dirty so current state gets uploaded to new renderer + impl->pica.dirty_regs.SetAllDirty(); + + // Also mark shader setups as dirty so uniforms get re-uploaded and + // stale pointers to the old rasterizer's JIT cache are cleared. + impl->pica.vs_setup.uniforms_dirty = true; + impl->pica.vs_setup.cached_shader = nullptr; + impl->pica.gs_setup.uniforms_dirty = true; + impl->pica.gs_setup.cached_shader = nullptr; + + // Mark all cached LUT/table state in pica as dirty + impl->pica.lighting.lut_dirty = impl->pica.lighting.LutAllDirty; + impl->pica.fog.lut_dirty = true; + impl->pica.proctex.table_dirty = impl->pica.proctex.TableAllDirty; +} + +void GPU::ReleaseRenderer() { + // Just reset the renderer to release OpenGL resources + // Don't null out rasterizer pointer as it will become dangling + impl->renderer.reset(); + impl->sw_blitter.reset(); + LOG_INFO(HW_GPU, "Renderer released for context destroy"); +} + template void GPU::serialize(Archive& ar, const u32 file_version) { ar & impl->pica; diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h index 1b326a067..4e32b6d2e 100644 --- a/src/video_core/gpu.h +++ b/src/video_core/gpu.h @@ -96,6 +96,12 @@ public: void ApplyPerProgramSettings(u64 program_ID); + /// Recreates the renderer (for GL context reset in libretro) + void RecreateRenderer(Frontend::EmuWindow& emu_window, Frontend::EmuWindow* secondary_window); + + /// Releases the renderer (for GL context destroy in libretro) + void ReleaseRenderer(); + private: void SubmitCmdList(u32 index); diff --git a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp index 1f9a16793..6969faa88 100644 --- a/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp +++ b/src/video_core/renderer_opengl/gl_shader_disk_cache.cpp @@ -457,6 +457,14 @@ FileUtil::IOFile ShaderDiskCache::AppendTransferableFile() { const auto transferable_path{GetTransferablePath()}; const bool existed = FileUtil::Exists(transferable_path); +#ifdef HAVE_LIBRETRO_VFS + // LibRetro's VFS maps "ab+" to RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, which + // uses "r+b" internally and fails if the file doesn't exist. Pre-create it. + if (!existed) { + FileUtil::CreateEmptyFile(transferable_path); + } +#endif + FileUtil::IOFile file(transferable_path, "ab+"); if (!file.IsOpen()) { LOG_ERROR(Render_OpenGL, "Failed to open transferable cache in path={}", transferable_path); @@ -480,6 +488,14 @@ FileUtil::IOFile ShaderDiskCache::AppendPrecompiledFile(bool write_header) { const auto precompiled_path{GetPrecompiledPath()}; const bool existed = FileUtil::Exists(precompiled_path); +#ifdef HAVE_LIBRETRO_VFS + // LibRetro's VFS maps "ab+" to RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING, which + // uses "r+b" internally and fails if the file doesn't exist. Pre-create it. + if (!existed) { + FileUtil::CreateEmptyFile(precompiled_path); + } +#endif + FileUtil::IOFile file(precompiled_path, "ab+"); if (!file.IsOpen()) { LOG_ERROR(Render_OpenGL, "Failed to open precompiled cache in path={}", precompiled_path); @@ -514,6 +530,13 @@ void ShaderDiskCache::SaveVirtualPrecompiledFile() { const auto precompiled_path{GetPrecompiledPath()}; +#ifdef HAVE_LIBRETRO_VFS + const bool existed = FileUtil::Exists(precompiled_path); + if (!existed) { + FileUtil::CreateEmptyFile(precompiled_path); + } +#endif + precompiled_file.Close(); if (!FileUtil::Delete(GetPrecompiledPath())) { LOG_ERROR(Render_OpenGL, "Failed to invalidate precompiled file={}", GetPrecompiledPath()); diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index 967fd21fa..c8cb6c000 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -94,9 +94,14 @@ void RendererOpenGL::SwapBuffers() { OpenGLState prev_state = OpenGLState::GetCurState(); state.Apply(); + render_window.SetupFramebuffer(); + PrepareRendertarget(); RenderScreenshot(); - +#ifdef HAVE_LIBRETRO + DrawScreens(render_window.GetFramebufferLayout(), false); + render_window.SwapBuffers(); +#else const auto& main_layout = render_window.GetFramebufferLayout(); RenderToMailbox(main_layout, render_window.mailbox, false); @@ -124,6 +129,7 @@ void RendererOpenGL::SwapBuffers() { LOG_DEBUG(Render_OpenGL, "Frame dumper exception caught: {}", exception.what()); } } +#endif system.perf_stats->EndSwap(); EndFrame(); @@ -663,7 +669,9 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f const auto& bottom_screen = layout.bottom_screen; glViewport(0, 0, layout.width, layout.height); - glClear(GL_COLOR_BUFFER_BIT); + if (render_window.NeedsClearing()) { + glClear(GL_COLOR_BUFFER_BIT); + } // Set projection matrix std::array ortho_matrix = diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 59a5a1c94..0b04b0ba7 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -22,7 +22,7 @@ #include -#ifdef __APPLE__ +#if defined(__APPLE__) && !defined(HAVE_LIBRETRO) #include "common/apple_utils.h" #endif @@ -60,11 +60,11 @@ constexpr static std::array PRESENT_BINDINGS namespace { static bool IsLowRefreshRate() { +#if (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO) if (!Settings::values.use_display_refresh_rate_detection) { LOG_INFO(Render_Vulkan, "Refresh rate detection is currently disabled via settings"); return false; } -#if defined(__APPLE__) || defined(ENABLE_SDL2) #ifdef __APPLE__ // Apple's low power mode sometimes limits applications to 30fps without changing the refresh // rate, meaning the above code doesn't catch it. @@ -98,7 +98,7 @@ static bool IsLowRefreshRate() { LOG_INFO(Render_Vulkan, "Refresh rate is above emulated 3DS screen: {}hz. Good.", cur_refresh_rate); } -#endif // defined(__APPLE__) || defined(ENABLE_SDL2) +#endif // (defined(__APPLE__) || defined(ENABLE_SDL2)) && !defined(HAVE_LIBRETRO) // We have no available method of checking refresh rate. Just assume that everything is fine :) return false; diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 29ed8a66e..b275f3189 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -7,8 +7,12 @@ #include "common/common_types.h" #include "common/math_util.h" #include "video_core/renderer_base.h" +#ifdef HAVE_LIBRETRO +#include "citra_libretro/libretro_vk.h" +#else #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_present_window.h" +#endif #include "video_core/renderer_vulkan/vk_rasterizer.h" #include "video_core/renderer_vulkan/vk_render_manager.h" #include "video_core/renderer_vulkan/vk_scheduler.h" @@ -116,7 +120,11 @@ private: Memory::MemorySystem& memory; Pica::PicaCore& pica; +#ifdef HAVE_LIBRETRO + LibRetroVKInstance instance; +#else Instance instance; +#endif Scheduler scheduler; RenderManager renderpass_cache; PresentWindow main_present_window; diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index d41cdfb9e..3f968e4c9 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -464,7 +464,9 @@ bool Instance::CreateDevice() { const bool has_custom_border_color = add_extension(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME, is_qualcomm, "it is broken on most Qualcomm driver versions"); - const bool has_index_type_uint8 = add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME); + const bool has_index_type_uint8 = + add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME, is_moltenvk, + "uint8 index conversion causes memory leaks in MoltenVK"); const bool has_fragment_shader_interlock = add_extension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, is_nvidia, "it is broken on Nvidia drivers"); @@ -481,6 +483,9 @@ bool Instance::CreateDevice() { return false; } +#ifndef HAVE_LIBRETRO + // Find graphics queue family. LibRetro builds skip this since queue_family_index + // is already set by LibRetroVKInstance from the frontend-provided context. bool graphics_queue_found = false; for (std::size_t i = 0; i < family_properties.size(); i++) { const u32 index = static_cast(i); @@ -494,6 +499,7 @@ bool Instance::CreateDevice() { LOG_CRITICAL(Render_Vulkan, "Unable to find graphics and/or present queues."); return false; } +#endif static constexpr std::array queue_priorities = {1.0f}; @@ -612,6 +618,10 @@ bool Instance::CreateDevice() { #undef PROP_GET #undef FEAT_SET +#ifdef HAVE_LIBRETRO + // LibRetro builds: device already created by frontend, just return after feature detection + return true; +#else try { device = physical_device.createDeviceUnique(device_chain.get()); } catch (vk::ExtensionNotPresentError& err) { @@ -626,6 +636,7 @@ bool Instance::CreateDevice() { CreateAllocator(); return true; +#endif } void Instance::CreateAllocator() { @@ -636,9 +647,9 @@ void Instance::CreateAllocator() { const VmaAllocatorCreateInfo allocator_info = { .physicalDevice = physical_device, - .device = *device, + .device = GetDevice(), .pVulkanFunctions = &functions, - .instance = *instance, + .instance = GetInstance(), .vulkanApiVersion = TargetVulkanApiVersion, }; diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index 09984b53f..f6fbc28fb 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -38,9 +38,11 @@ struct FormatTraits { class Instance { public: + struct NoInit {}; explicit Instance(bool validation = false, bool dump_command_buffers = false); explicit Instance(Frontend::EmuWindow& window, u32 physical_device_index); - ~Instance(); + explicit Instance(NoInit) {} // For LibRetro inheritance - does minimal setup + virtual ~Instance(); /// Returns the FormatTraits struct for the provided pixel format const FormatTraits& GetTraits(VideoCore::PixelFormat pixel_format) const; @@ -58,7 +60,7 @@ public: std::string GetDriverVersionName(); /// Returns the Vulkan instance - vk::Instance GetInstance() const { + virtual vk::Instance GetInstance() const { return *instance; } @@ -68,7 +70,7 @@ public: } /// Returns the Vulkan device - vk::Device GetDevice() const { + virtual vk::Device GetDevice() const { return *device; } @@ -254,6 +256,11 @@ public: return features.shaderSampledImageArrayDynamicIndexing; } + /// Returns true if layered rendering (array attachments) is supported + bool IsLayeredRenderingSupported() const { + return layered_rendering_supported; + } + /// Returns the minimum vertex stride alignment u32 GetMinVertexStrideAlignment() const { return min_vertex_stride_alignment; @@ -270,7 +277,7 @@ public: driver_id == vk::DriverIdKHR::eQualcommProprietary; } -private: +protected: /// Returns the optimal supported usage for the requested format [[nodiscard]] FormatTraits DetermineTraits(VideoCore::PixelFormat pixel_format, vk::Format format); @@ -294,7 +301,7 @@ private: // Collects logging gpu info void CollectToolingInfo(); -private: +protected: std::shared_ptr library; vk::UniqueInstance instance; vk::PhysicalDevice physical_device; @@ -328,10 +335,11 @@ private: bool shader_stencil_export{}; bool external_memory_host{}; u64 min_imported_host_pointer_alignment{}; + bool layered_rendering_supported{true}; bool tooling_info{}; bool debug_utils_supported{}; bool has_nsight_graphics{}; bool has_renderdoc{}; }; -} // namespace Vulkan \ No newline at end of file +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_resource_pool.cpp b/src/video_core/renderer_vulkan/vk_resource_pool.cpp index 0021167e4..03b644ea2 100644 --- a/src/video_core/renderer_vulkan/vk_resource_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_resource_pool.cpp @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2020 yuzu Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -159,7 +163,10 @@ void DescriptorHeap::Allocate(std::size_t begin, std::size_t end) { if (result == vk::Result::eSuccess) { break; } - if (result == vk::Result::eErrorOutOfPoolMemory) { + // eErrorFragmentedPool: pool has space but is too fragmented to allocate. + // MoltenVK on iOS/tvOS returns this more frequently than native Vulkan drivers. + if (result == vk::Result::eErrorOutOfPoolMemory || + result == vk::Result::eErrorFragmentedPool) { current_pool++; if (current_pool == pools.size()) { LOG_INFO(Render_Vulkan, "Run out of pools, creating new one!"); diff --git a/src/video_core/renderer_vulkan/vk_scheduler.cpp b/src/video_core/renderer_vulkan/vk_scheduler.cpp index 0099b0ca3..5d50b6c97 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.cpp +++ b/src/video_core/renderer_vulkan/vk_scheduler.cpp @@ -1,4 +1,4 @@ -// Copyright 2019 yuzu Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -8,6 +8,9 @@ #include "common/thread.h" #include "video_core/renderer_vulkan/vk_instance.h" #include "video_core/renderer_vulkan/vk_scheduler.h" +#ifdef HAVE_LIBRETRO +#include "citra_libretro/libretro_vk.h" +#endif MICROPROFILE_DEFINE(Vulkan_WaitForWorker, "Vulkan", "Wait for worker", MP_RGB(255, 192, 192)); MICROPROFILE_DEFINE(Vulkan_Submit, "Vulkan", "Submit Exectution", MP_RGB(255, 192, 255)); @@ -17,11 +20,15 @@ namespace Vulkan { namespace { std::unique_ptr MakeMasterSemaphore(const Instance& instance) { +#ifdef HAVE_LIBRETRO + return CreateLibRetroMasterSemaphore(instance); +#else if (instance.IsTimelineSemaphoreSupported()) { return std::make_unique(instance); } else { return std::make_unique(instance); } +#endif } } // Anonymous namespace diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index fdaa4ef2e..cfcb199a1 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -146,7 +146,10 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, std::string_view debug_name = {}) { - const u32 layers = type == TextureType::CubeMap ? 6 : 1; + // On tvOS/iOS, fall back to 2D textures when layered rendering isn't supported + const bool is_cube_map = + type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); + const u32 layers = is_cube_map ? 6 : 1; const std::array format_list = { vk::Format::eR8G8B8A8Unorm, @@ -192,8 +195,7 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T const vk::Image image{unsafe_image}; const vk::ImageViewCreateInfo view_info = { .image = image, - .viewType = - type == TextureType::CubeMap ? vk::ImageViewType::eCube : vk::ImageViewType::e2D, + .viewType = is_cube_map ? vk::ImageViewType::eCube : vk::ImageViewType::e2D, .format = format, .subresourceRange{ .aspectMask = aspect, From 7bcbf8aba4c4d0c82c0f3bc9265cf305a0a39ecf Mon Sep 17 00:00:00 2001 From: Alexandre Bouvier Date: Fri, 20 Feb 2026 05:32:46 +0100 Subject: [PATCH 10/94] cmake: fix import name --- src/citra_libretro/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/citra_libretro/CMakeLists.txt b/src/citra_libretro/CMakeLists.txt index 8ea9b7b10..d90709f36 100644 --- a/src/citra_libretro/CMakeLists.txt +++ b/src/citra_libretro/CMakeLists.txt @@ -16,7 +16,7 @@ add_library(azahar_libretro_common OBJECT core_settings.h) target_compile_definitions(azahar_libretro_common PRIVATE HAVE_LIBRETRO) -target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro robin_map) +target_link_libraries(azahar_libretro_common PRIVATE citra_common citra_core video_core libretro tsl::robin_map) if(ENABLE_OPENGL) target_link_libraries(azahar_libretro_common PRIVATE glad) endif() @@ -43,7 +43,7 @@ target_compile_definitions(audio_core PRIVATE HAVE_LIBRETRO) target_compile_definitions(input_common PRIVATE HAVE_LIBRETRO) target_link_libraries(azahar_libretro PRIVATE citra_common citra_core) -target_link_libraries(azahar_libretro PRIVATE boost dds-ktx libretro robin_map) +target_link_libraries(azahar_libretro PRIVATE Boost::boost dds-ktx libretro tsl::robin_map) if(ENABLE_VULKAN) target_link_libraries(azahar_libretro PRIVATE sirit vulkan-headers vma) endif() From f3fb0b729e7ca90e9697ca659643e5ef940ce41d Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Thu, 19 Feb 2026 22:48:49 +0000 Subject: [PATCH 11/94] Kill SDL2 frontend Good riddance --- CMakeLists.txt | 6 - src/CMakeLists.txt | 6 +- src/citra_meta/CMakeLists.txt | 4 - src/citra_meta/common_strings.h | 7 - src/citra_meta/main.cpp | 28 +- src/citra_sdl/CMakeLists.txt | 48 -- src/citra_sdl/citra_sdl.cpp | 530 ------------------ src/citra_sdl/citra_sdl.h | 7 - src/citra_sdl/config.cpp | 395 ------------- src/citra_sdl/config.h | 35 -- src/citra_sdl/default_ini.h | 405 ------------- src/citra_sdl/emu_window/emu_window_sdl2.cpp | 252 --------- src/citra_sdl/emu_window/emu_window_sdl2.h | 91 --- .../emu_window/emu_window_sdl2_gl.cpp | 167 ------ src/citra_sdl/emu_window/emu_window_sdl2_gl.h | 39 -- .../emu_window/emu_window_sdl2_sw.cpp | 108 ---- src/citra_sdl/emu_window/emu_window_sdl2_sw.h | 43 -- .../emu_window/emu_window_sdl2_vk.cpp | 90 --- src/citra_sdl/emu_window/emu_window_sdl2_vk.h | 24 - src/citra_sdl/precompiled_headers.h | 7 - src/citra_sdl/resource.h | 16 - 21 files changed, 6 insertions(+), 2302 deletions(-) delete mode 100644 src/citra_sdl/CMakeLists.txt delete mode 100644 src/citra_sdl/citra_sdl.cpp delete mode 100644 src/citra_sdl/citra_sdl.h delete mode 100644 src/citra_sdl/config.cpp delete mode 100644 src/citra_sdl/config.h delete mode 100644 src/citra_sdl/default_ini.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_gl.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_sw.h delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp delete mode 100644 src/citra_sdl/emu_window/emu_window_sdl2_vk.h delete mode 100644 src/citra_sdl/precompiled_headers.h delete mode 100644 src/citra_sdl/resource.h diff --git a/CMakeLists.txt b/CMakeLists.txt index df86bf28c..54eb90e90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,7 +105,6 @@ foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS) endforeach() option(ENABLE_SDL2 "Enable using SDL2" ON) -CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF) option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF) # Set bundled qt as dependent options. @@ -182,9 +181,6 @@ endif() if (ENABLE_SDL2) add_definitions(-DENABLE_SDL2) endif() -if (ENABLE_SDL2_FRONTEND) - add_definitions(-DENABLE_SDL2_FRONTEND) -endif() if(ENABLE_SSE42 AND (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")) message(STATUS "SSE4.2 enabled for x86_64") @@ -555,8 +551,6 @@ if (NOT ANDROID AND NOT IOS) include(BundleTarget) if (ENABLE_QT) qt_bundle_target(citra_meta) - elseif (ENABLE_SDL2_FRONTEND) - bundle_target(citra_meta) endif() if (ENABLE_ROOM_STANDALONE) bundle_target(citra_room_standalone) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b18925867..bd1810d53 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -193,15 +193,11 @@ if (ENABLE_TESTS) add_subdirectory(tests) endif() -if (ENABLE_SDL2_FRONTEND) - add_subdirectory(citra_sdl) -endif() - if (ENABLE_QT) add_subdirectory(citra_qt) endif() -if (ENABLE_QT OR ENABLE_SDL2_FRONTEND) +if (ENABLE_QT) # Or any other hypothetical future frontends add_subdirectory(citra_meta) endif() diff --git a/src/citra_meta/CMakeLists.txt b/src/citra_meta/CMakeLists.txt index 3b6d2b230..d2c367fb4 100644 --- a/src/citra_meta/CMakeLists.txt +++ b/src/citra_meta/CMakeLists.txt @@ -45,10 +45,6 @@ endif() target_link_libraries(citra_meta PRIVATE fmt) -if (ENABLE_SDL2_FRONTEND) - target_link_libraries(citra_meta PRIVATE citra_sdl) -endif() - if (ENABLE_QT) target_link_libraries(citra_meta PRIVATE citra_qt) target_link_libraries(citra_meta PRIVATE Boost::boost Qt6::Widgets) diff --git a/src/citra_meta/common_strings.h b/src/citra_meta/common_strings.h index 4d78f3a05..0276b697f 100644 --- a/src/citra_meta/common_strings.h +++ b/src/citra_meta/common_strings.h @@ -19,13 +19,6 @@ constexpr char help_string[] = "-r, --movie-record [path] Record a TAS movie to the given file path\n" "-a, --movie-record-author [author] Set the author for the recorded TAS movie (to be used " "alongside --movie-record)\n" -#ifdef ENABLE_SDL2_FRONTEND - "-n, --no-gui Use the lightweight SDL frontend instead of the usual Qt " - "frontend\n" - // TODO: Move -m outside of this check when it is implemented in Qt frontend - "-m, --multiplayer [nick:password@address:port] Nickname, password, address and port for " - "multiplayer (currently only usable with SDL frontend)\n" -#endif #ifdef ENABLE_ROOM " --room Utilize dedicated multiplayer room functionality (equivalent to " "the old citra-room executable)\n" diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index 134f2d2dd..f1dba092f 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -7,15 +7,16 @@ #include "common/detached_tasks.h" #include "common/scope_exit.h" +#if !defined(ENABLE_QT) +#error "citra_meta is somehow building with no frontend. This should be impossible!" +#endif + #ifdef ENABLE_QT #include "citra_qt/citra_qt.h" #endif #ifdef ENABLE_ROOM #include "citra_room/citra_room.h" #endif -#ifdef ENABLE_SDL2_FRONTEND -#include "citra_sdl/citra_sdl.h" -#endif #ifdef _WIN32 extern "C" { @@ -95,25 +96,6 @@ int main(int argc, char* argv[]) { #endif #if ENABLE_QT - bool no_gui = false; - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--no-gui") == 0 || strcmp(argv[i], "-n") == 0) { - no_gui = true; - } - } - - if (!no_gui) { - return LaunchQtFrontend(argc, argv); - } + return LaunchQtFrontend(argc, argv); #endif - -#if ENABLE_SDL2_FRONTEND - return LaunchSdlFrontend(argc, argv); -#else - std::cout << "Cannot use SDL frontend as it was disabled at compile time. Exiting." - << std::endl; - return -1; -#endif - - return 0; } diff --git a/src/citra_sdl/CMakeLists.txt b/src/citra_sdl/CMakeLists.txt deleted file mode 100644 index cf3deda2d..000000000 --- a/src/citra_sdl/CMakeLists.txt +++ /dev/null @@ -1,48 +0,0 @@ -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/CMakeModules) - -add_library(citra_sdl STATIC EXCLUDE_FROM_ALL - config.cpp - config.h - default_ini.h - emu_window/emu_window_sdl2.cpp - emu_window/emu_window_sdl2.h - citra_sdl.cpp - precompiled_headers.h - resource.h -) - -if (ENABLE_SOFTWARE_RENDERER) - target_sources(citra_sdl PRIVATE - emu_window/emu_window_sdl2_sw.cpp - emu_window/emu_window_sdl2_sw.h - ) -endif() -if (ENABLE_OPENGL) - target_sources(citra_sdl PRIVATE - emu_window/emu_window_sdl2_gl.cpp - emu_window/emu_window_sdl2_gl.h - ) -endif() -if (ENABLE_VULKAN) - target_sources(citra_sdl PRIVATE - emu_window/emu_window_sdl2_vk.cpp - emu_window/emu_window_sdl2_vk.h - ) -endif() - -create_target_directory_groups(citra_sdl) - -target_link_libraries(citra_sdl PRIVATE citra_common citra_core input_common network) -target_link_libraries(citra_sdl PRIVATE inih) -if (MSVC) - target_link_libraries(citra_sdl PRIVATE getopt) -endif() -target_link_libraries(citra_sdl PRIVATE ${PLATFORM_LIBRARIES} SDL2::SDL2 Threads::Threads) - -if (ENABLE_OPENGL) - target_link_libraries(citra_sdl PRIVATE glad) -endif() - -if (CITRA_USE_PRECOMPILED_HEADERS) - target_precompile_headers(citra_sdl PRIVATE precompiled_headers.h) -endif() diff --git a/src/citra_sdl/citra_sdl.cpp b/src/citra_sdl/citra_sdl.cpp deleted file mode 100644 index 9c0f4470a..000000000 --- a/src/citra_sdl/citra_sdl.cpp +++ /dev/null @@ -1,530 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include - -// This needs to be included before getopt.h because the latter #defines symbols used by it -#include "common/microprofile.h" - -#include "citra_sdl/config.h" -#include "citra_sdl/emu_window/emu_window_sdl2.h" -#ifdef ENABLE_OPENGL -#include "citra_sdl/emu_window/emu_window_sdl2_gl.h" -#endif -#ifdef ENABLE_SOFTWARE_RENDERER -#include "citra_sdl/emu_window/emu_window_sdl2_sw.h" -#endif -#ifdef ENABLE_VULKAN -#include "citra_sdl/emu_window/emu_window_sdl2_vk.h" -#endif -#include "SDL_messagebox.h" -#include "citra_meta/common_strings.h" -#include "common/common_paths.h" -#include "common/file_util.h" -#include "common/logging/backend.h" -#include "common/logging/log.h" -#include "common/scm_rev.h" -#include "common/scope_exit.h" -#include "common/settings.h" -#include "common/string_util.h" -#include "core/core.h" -#include "core/dumping/backend.h" -#include "core/dumping/ffmpeg_backend.h" -#include "core/frontend/applets/default_applets.h" -#include "core/frontend/framebuffer_layout.h" -#include "core/hle/service/am/am.h" -#include "core/hle/service/cfg/cfg.h" -#include "core/movie.h" -#include "input_common/main.h" -#include "network/network.h" -#include "video_core/gpu.h" -#include "video_core/renderer_base.h" - -#ifdef __unix__ -#include "common/linux/gamemode.h" -#endif - -#undef _UNICODE -#include -#ifndef _MSC_VER -#include -#endif - -#ifdef _WIN32 -// windows.h needs to be included before shellapi.h -#include - -#include -#endif - -static void ShowCommandOutput(std::string title, std::string message) { -#ifdef _WIN32 - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION, title.c_str(), message.c_str(), NULL); -#else - std::cout << message << std::endl; -#endif -} - -static void PrintHelp(const char* argv0) { - ShowCommandOutput("Help", fmt::format(Common::help_string, argv0)); -} - -static void OnStateChanged(const Network::RoomMember::State& state) { - switch (state) { - case Network::RoomMember::State::Idle: - LOG_DEBUG(Network, "Network is idle"); - break; - case Network::RoomMember::State::Joining: - LOG_DEBUG(Network, "Connection sequence to room started"); - break; - case Network::RoomMember::State::Joined: - LOG_DEBUG(Network, "Successfully joined to the room"); - break; - case Network::RoomMember::State::Moderator: - LOG_DEBUG(Network, "Successfully joined the room as a moderator"); - break; - default: - break; - } -} - -static void OnNetworkError(const Network::RoomMember::Error& error) { - switch (error) { - case Network::RoomMember::Error::LostConnection: - LOG_DEBUG(Network, "Lost connection to the room"); - break; - case Network::RoomMember::Error::CouldNotConnect: - LOG_ERROR(Network, "Error: Could not connect"); - std::exit(1); - break; - case Network::RoomMember::Error::NameCollision: - LOG_ERROR( - Network, - "You tried to use the same nickname as another user that is connected to the Room"); - std::exit(1); - break; - case Network::RoomMember::Error::MacCollision: - LOG_ERROR(Network, "You tried to use the same MAC-Address as another user that is " - "connected to the Room"); - std::exit(1); - break; - case Network::RoomMember::Error::ConsoleIdCollision: - LOG_ERROR(Network, "Your Console ID conflicted with someone else in the Room"); - std::exit(1); - break; - case Network::RoomMember::Error::WrongPassword: - LOG_ERROR(Network, "Room replied with: Wrong password"); - std::exit(1); - break; - case Network::RoomMember::Error::WrongVersion: - LOG_ERROR(Network, - "You are using a different version than the room you are trying to connect to"); - std::exit(1); - break; - case Network::RoomMember::Error::RoomIsFull: - LOG_ERROR(Network, "The room is full"); - std::exit(1); - break; - case Network::RoomMember::Error::HostKicked: - LOG_ERROR(Network, "You have been kicked by the host"); - break; - case Network::RoomMember::Error::HostBanned: - LOG_ERROR(Network, "You have been banned by the host"); - break; - default: - LOG_ERROR(Network, "Unknown network error: {}", error); - break; - } -} - -static void OnMessageReceived(const Network::ChatEntry& msg) { - std::cout << std::endl << msg.nickname << ": " << msg.message << std::endl << std::endl; -} - -static void OnStatusMessageReceived(const Network::StatusMessageEntry& msg) { - std::string message; - switch (msg.type) { - case Network::IdMemberJoin: - message = fmt::format("{} has joined", msg.nickname); - break; - case Network::IdMemberLeave: - message = fmt::format("{} has left", msg.nickname); - break; - case Network::IdMemberKicked: - message = fmt::format("{} has been kicked", msg.nickname); - break; - case Network::IdMemberBanned: - message = fmt::format("{} has been banned", msg.nickname); - break; - case Network::IdAddressUnbanned: - message = fmt::format("{} has been unbanned", msg.nickname); - break; - } - if (!message.empty()) - std::cout << std::endl << "* " << message << std::endl << std::endl; -} - -/// Application entry point -int LaunchSdlFrontend(int argc, char** argv) { - Common::Log::Initialize(); - Common::Log::SetColorConsoleBackendEnabled(true); - Common::Log::Start(); - SdlConfig config; - int option_index = 0; - bool use_gdbstub = Settings::values.use_gdbstub.GetValue(); - u32 gdb_port = static_cast(Settings::values.gdbstub_port.GetValue()); - std::string movie_record; - std::string movie_record_author; - std::string movie_play; - std::string dump_video; - - char* endarg; -#ifdef _WIN32 - int argc_w; - auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w); - - if (argv_w == nullptr) { - LOG_CRITICAL(Frontend, "Failed to get command line arguments"); - return -1; - } -#endif - std::string filepath; - - bool use_multiplayer = false; - bool fullscreen = false; - std::string nickname{}; - std::string password{}; - std::string address{}; - u16 port = Network::DefaultRoomPort; - - static struct option long_options[] = { - {"dump-video", required_argument, 0, 'd'}, - {"fullscreen", no_argument, 0, 'f'}, - {"gdbport", required_argument, 0, 'g'}, - {"help", no_argument, 0, 'h'}, - {"install", required_argument, 0, 'i'}, - {"movie-play", required_argument, 0, 'p'}, - {"movie-record", required_argument, 0, 'r'}, - {"movie-record-author", required_argument, 0, 'a'}, - {"multiplayer", required_argument, 0, 'm'}, - {"version", no_argument, 0, 'v'}, - {"windowed", no_argument, 0, 'w'}, - {0, 0, 0, 0}, - }; - - while (optind < argc) { - int arg = getopt_long(argc, argv, "d:fg:hi:p:r:a:m:nvw", long_options, &option_index); - if (arg != -1) { - switch (static_cast(arg)) { - case 'd': - dump_video = optarg; - break; - case 'f': - fullscreen = true; - LOG_INFO(Frontend, "Starting in fullscreen mode..."); - break; - case 'g': - errno = 0; - gdb_port = strtoul(optarg, &endarg, 0); - use_gdbstub = true; - if (endarg == optarg) - errno = EINVAL; - if (errno != 0) { - perror("--gdbport"); - return 1; - } - break; - case 'h': - PrintHelp(argv[0]); - return 0; - case 'i': { - const auto cia_progress = [](std::size_t written, std::size_t total) { - LOG_INFO(Frontend, "{:02d}%", (written * 100 / total)); - }; - if (Service::AM::InstallCIA(std::string(optarg), cia_progress) != - Service::AM::InstallStatus::Success) - errno = EINVAL; - if (errno != 0) - return 1; - break; - } - case 'p': - movie_play = optarg; - break; - case 'r': - movie_record = optarg; - break; - case 'a': - movie_record_author = optarg; - break; - case 'm': { - use_multiplayer = true; - const std::string str_arg(optarg); - // regex to check if the format is nickname:password@ip:port - // with optional :password - const std::regex re("^([^:]+)(?::(.+))?@([^:]+)(?::([0-9]+))?$"); - if (!std::regex_match(str_arg, re)) { - std::cout << "Wrong format for option --multiplayer\n"; - PrintHelp(argv[0]); - return 0; - } - - std::smatch match; - std::regex_search(str_arg, match, re); - ASSERT(match.size() == 5); - nickname = match[1]; - password = match[2]; - address = match[3]; - if (!match[4].str().empty()) - port = std::stoi(match[4]); - std::regex nickname_re("^[a-zA-Z0-9._\\- ]+$"); - if (!std::regex_match(nickname, nickname_re)) { - std::cout - << "Nickname is not valid. Must be 4 to 20 alphanumeric characters.\n"; - return 0; - } - if (address.empty()) { - std::cout << "Address to room must not be empty.\n"; - return 0; - } - break; - } - case 'v': - const std::string version_string = - std::string("Azahar ") + Common::g_build_fullname; - ShowCommandOutput("Version", version_string); - return 0; - } - } else { -#ifdef _WIN32 - filepath = Common::UTF16ToUTF8(argv_w[optind]); -#else - filepath = argv[optind]; -#endif - optind++; - } - } - -#ifdef _WIN32 - LocalFree(argv_w); -#endif - - MicroProfileOnThreadCreate("EmuThread"); - SCOPE_EXIT({ MicroProfileShutdown(); }); - - if (filepath.empty()) { - LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified"); - return -1; - } - - if (!movie_record.empty() && !movie_play.empty()) { - LOG_CRITICAL(Frontend, "Cannot both play and record a movie"); - return -1; - } - - auto& system = Core::System::GetInstance(); - auto& movie = system.Movie(); - - if (!movie_record.empty()) { - movie.PrepareForRecording(); - } - if (!movie_play.empty()) { - movie.PrepareForPlayback(movie_play); - } - - // Apply the command line arguments - Settings::values.gdbstub_port = gdb_port; - Settings::values.use_gdbstub = use_gdbstub; - system.ApplySettings(); - - // Register frontend applets - Frontend::RegisterDefaultApplets(system); - - EmuWindow_SDL2::InitializeSDL2(); - - const auto create_emu_window = [&](bool fullscreen, - bool is_secondary) -> std::unique_ptr { - const auto graphics_api = Settings::values.graphics_api.GetValue(); - switch (graphics_api) { -#ifdef ENABLE_OPENGL - case Settings::GraphicsAPI::OpenGL: - return std::make_unique(system, fullscreen, is_secondary); -#endif -#ifdef ENABLE_VULKAN - case Settings::GraphicsAPI::Vulkan: - return std::make_unique(system, fullscreen, is_secondary); -#endif -#ifdef ENABLE_SOFTWARE_RENDERER - case Settings::GraphicsAPI::Software: - return std::make_unique(system, fullscreen, is_secondary); -#endif - default: - LOG_CRITICAL( - Frontend, - "Unknown or unsupported graphics API {}, falling back to available default", - graphics_api); -#ifdef ENABLE_OPENGL - return std::make_unique(system, fullscreen, is_secondary); -#elif ENABLE_VULKAN - return std::make_unique(system, fullscreen, is_secondary); -#elif ENABLE_SOFTWARE_RENDERER - return std::make_unique(system, fullscreen, is_secondary); -#else - // TODO: Add a null renderer backend for this, perhaps. -#error "At least one renderer must be enabled." -#endif - } - }; - - const auto emu_window{create_emu_window(fullscreen, false)}; - const bool use_secondary_window{ - Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows && - Settings::values.graphics_api.GetValue() != Settings::GraphicsAPI::Software}; - const auto secondary_window = use_secondary_window ? create_emu_window(false, true) : nullptr; - - const auto scope = emu_window->Acquire(); - - LOG_INFO(Frontend, "Azahar Version: {} | {}-{}", Common::g_build_fullname, Common::g_scm_branch, - Common::g_scm_desc); - Settings::LogSettings(); - - const Core::System::ResultStatus load_result{ - system.Load(*emu_window, filepath, secondary_window.get())}; - - switch (load_result) { - case Core::System::ResultStatus::ErrorGetLoader: - LOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath); - return -1; - case Core::System::ResultStatus::ErrorLoader: - LOG_CRITICAL(Frontend, "Failed to load ROM!"); - return -1; - case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted: - LOG_CRITICAL(Frontend, - "The application that you are trying to load must be decrypted before " - "being used with Azahar. \n\n For more information on dumping and " - "decrypting applications, please refer to: " - "https://web.archive.org/web/20240304210021/https://citra-emu.org/" - "wiki/dumping-game-cartridges/"); - return -1; - case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat: - LOG_CRITICAL(Frontend, "Error while loading ROM: The ROM format is not supported."); - return -1; - case Core::System::ResultStatus::ErrorNotInitialized: - LOG_CRITICAL(Frontend, "CPUCore not initialized"); - return -1; - case Core::System::ResultStatus::ErrorSystemMode: - LOG_CRITICAL(Frontend, "Failed to determine system mode!"); - return -1; - case Core::System::ResultStatus::Success: - break; // Expected case - default: - LOG_ERROR(Frontend, "Error while loading ROM: {}", system.GetStatusDetails()); - break; - } - - if (use_multiplayer) { - if (auto member = Network::GetRoomMember().lock()) { - member->BindOnChatMessageRecieved(OnMessageReceived); - member->BindOnStatusMessageReceived(OnStatusMessageReceived); - member->BindOnStateChanged(OnStateChanged); - member->BindOnError(OnNetworkError); - LOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port, - nickname); - member->Join(nickname, Service::CFG::GetConsoleIdHash(system), address.c_str(), port, 0, - Network::NoPreferredMac, password); - } else { - LOG_ERROR(Network, "Could not access RoomMember"); - return 0; - } - } - - if (!movie_play.empty()) { - auto metadata = movie.GetMovieMetadata(movie_play); - LOG_INFO(Movie, "Author: {}", metadata.author); - LOG_INFO(Movie, "Rerecord count: {}", metadata.rerecord_count); - LOG_INFO(Movie, "Input count: {}", metadata.input_count); - movie.StartPlayback(movie_play); - } - if (!movie_record.empty()) { - movie.StartRecording(movie_record, movie_record_author); - } - if (!dump_video.empty() && DynamicLibrary::FFmpeg::LoadFFmpeg()) { - auto& renderer = system.GPU().Renderer(); - const auto layout{ - Layout::FrameLayoutFromResolutionScale(renderer.GetResolutionScaleFactor())}; - auto dumper = std::make_shared(renderer); - if (dumper->StartDumping(dump_video, layout)) { - system.RegisterVideoDumper(dumper); - } - } - -#ifdef __unix__ - Common::Linux::StartGamemode(); -#endif - - std::thread main_render_thread([&emu_window] { emu_window->Present(); }); - std::thread secondary_render_thread([&secondary_window] { - if (secondary_window) { - secondary_window->Present(); - } - }); - - u64 program_id{}; - system.GetAppLoader().ReadProgramId(program_id); - system.GPU().ApplyPerProgramSettings(program_id); - - std::atomic_bool stop_run; - system.GPU().Renderer().Rasterizer()->LoadDefaultDiskResources( - stop_run, [](VideoCore::LoadCallbackStage stage, std::size_t value, std::size_t total) { - LOG_DEBUG(Frontend, "Loading stage {} progress {} {}", static_cast(stage), value, - total); - }); - - const auto secondary_is_open = [&secondary_window] { - // if the secondary window isn't created, it shouldn't affect the main loop - return secondary_window ? secondary_window->IsOpen() : true; - }; - while (emu_window->IsOpen() && secondary_is_open()) { - const auto result = system.RunLoop(); - - switch (result) { - case Core::System::ResultStatus::ShutdownRequested: - emu_window->RequestClose(); - break; - case Core::System::ResultStatus::Success: - break; - default: - LOG_ERROR(Frontend, "Error in main run loop: {}", result, system.GetStatusDetails()); - break; - } - } - emu_window->RequestClose(); - if (secondary_window) { - secondary_window->RequestClose(); - } - main_render_thread.join(); - secondary_render_thread.join(); - - movie.Shutdown(); - - auto video_dumper = system.GetVideoDumper(); - if (video_dumper && video_dumper->IsDumping()) { - video_dumper->StopDumping(); - } - - Network::Shutdown(); - InputCommon::Shutdown(); - - system.Shutdown(); - -#ifdef __unix__ - Common::Linux::StopGamemode(); -#endif - - return 0; -} diff --git a/src/citra_sdl/citra_sdl.h b/src/citra_sdl/citra_sdl.h deleted file mode 100644 index 9a1a2b6c3..000000000 --- a/src/citra_sdl/citra_sdl.h +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -int LaunchSdlFrontend(int argc, char** argv); diff --git a/src/citra_sdl/config.cpp b/src/citra_sdl/config.cpp deleted file mode 100644 index a504dd3aa..000000000 --- a/src/citra_sdl/config.cpp +++ /dev/null @@ -1,395 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include "citra_sdl/config.h" -#include "citra_sdl/default_ini.h" -#include "common/file_util.h" -#include "common/logging/backend.h" -#include "common/logging/log.h" -#include "common/settings.h" -#include "core/hle/service/service.h" -#include "input_common/main.h" -#include "input_common/udp/client.h" -#include "network/network_settings.h" - -SdlConfig::SdlConfig() { - // TODO: Don't hardcode the path; let the frontend decide where to put the config files. - sdl2_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "sdl2-config.ini"; - sdl2_config = std::make_unique(sdl2_config_loc); - - Reload(); -} - -SdlConfig::~SdlConfig() = default; - -bool SdlConfig::LoadINI(const std::string& default_contents, bool retry) { - const std::string& location = this->sdl2_config_loc; - if (sdl2_config->ParseError() < 0) { - if (retry) { - LOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location); - FileUtil::CreateFullPath(location); - FileUtil::WriteStringToFile(true, location, default_contents); - sdl2_config = std::make_unique(location); // Reopen file - - return LoadINI(default_contents, false); - } - LOG_ERROR(Config, "Failed."); - return false; - } - LOG_INFO(Config, "Successfully loaded {}", location); - return true; -} - -static const std::array default_buttons = { - SDL_SCANCODE_A, SDL_SCANCODE_S, SDL_SCANCODE_Z, SDL_SCANCODE_X, SDL_SCANCODE_T, SDL_SCANCODE_G, - SDL_SCANCODE_F, SDL_SCANCODE_H, SDL_SCANCODE_Q, SDL_SCANCODE_W, SDL_SCANCODE_M, SDL_SCANCODE_N, - SDL_SCANCODE_O, SDL_SCANCODE_P, SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_B, -}; - -static const std::array, Settings::NativeAnalog::NumAnalogs> default_analogs{{ - { - SDL_SCANCODE_UP, - SDL_SCANCODE_DOWN, - SDL_SCANCODE_LEFT, - SDL_SCANCODE_RIGHT, - SDL_SCANCODE_D, - }, - { - SDL_SCANCODE_I, - SDL_SCANCODE_K, - SDL_SCANCODE_J, - SDL_SCANCODE_L, - SDL_SCANCODE_D, - }, -}}; - -template <> -void SdlConfig::ReadSetting(const std::string& group, Settings::Setting& setting) { - std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault()); - if (setting_value.empty()) { - setting_value = setting.GetDefault(); - } - setting = std::move(setting_value); -} - -template <> -void SdlConfig::ReadSetting(const std::string& group, Settings::Setting& setting) { - setting = sdl2_config->GetBoolean(group, setting.GetLabel(), setting.GetDefault()); -} - -template -void SdlConfig::ReadSetting(const std::string& group, Settings::Setting& setting) { - if constexpr (std::is_floating_point_v) { - setting = static_cast( - sdl2_config->GetReal(group, setting.GetLabel(), setting.GetDefault())); - } else { - setting = static_cast(sdl2_config->GetInteger( - group, setting.GetLabel(), static_cast(setting.GetDefault()))); - } -} - -void SdlConfig::ReadValues() { - // Controls - // TODO: add multiple input profile support - for (int i = 0; i < Settings::NativeButton::NumButtons; ++i) { - std::string default_param = InputCommon::GenerateKeyboardParam(default_buttons[i]); - Settings::values.current_input_profile.buttons[i] = - sdl2_config->GetString("Controls", Settings::NativeButton::mapping[i], default_param); - if (Settings::values.current_input_profile.buttons[i].empty()) - Settings::values.current_input_profile.buttons[i] = default_param; - } - - for (int i = 0; i < Settings::NativeAnalog::NumAnalogs; ++i) { - std::string default_param = InputCommon::GenerateAnalogParamFromKeys( - default_analogs[i][0], default_analogs[i][1], default_analogs[i][2], - default_analogs[i][3], default_analogs[i][4], 0.5f); - Settings::values.current_input_profile.analogs[i] = - sdl2_config->GetString("Controls", Settings::NativeAnalog::mapping[i], default_param); - if (Settings::values.current_input_profile.analogs[i].empty()) - Settings::values.current_input_profile.analogs[i] = default_param; - } - - Settings::values.current_input_profile.motion_device = sdl2_config->GetString( - "Controls", "motion_device", - "engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0"); - Settings::values.current_input_profile.touch_device = - sdl2_config->GetString("Controls", "touch_device", "engine:emu_window"); - Settings::values.current_input_profile.udp_input_address = sdl2_config->GetString( - "Controls", "udp_input_address", InputCommon::CemuhookUDP::DEFAULT_ADDR); - Settings::values.current_input_profile.udp_input_port = - static_cast(sdl2_config->GetInteger("Controls", "udp_input_port", - InputCommon::CemuhookUDP::DEFAULT_PORT)); - ReadSetting("Controls", Settings::values.use_artic_base_controller); - - // Core - ReadSetting("Core", Settings::values.use_cpu_jit); - ReadSetting("Core", Settings::values.cpu_clock_percentage); - - // Renderer - ReadSetting("Renderer", Settings::values.graphics_api); - ReadSetting("Renderer", Settings::values.physical_device); - ReadSetting("Renderer", Settings::values.spirv_shader_gen); - ReadSetting("Renderer", Settings::values.async_shader_compilation); - ReadSetting("Renderer", Settings::values.async_presentation); - ReadSetting("Renderer", Settings::values.use_gles); - ReadSetting("Renderer", Settings::values.use_hw_shader); - ReadSetting("Renderer", Settings::values.shaders_accurate_mul); - ReadSetting("Renderer", Settings::values.use_shader_jit); - ReadSetting("Renderer", Settings::values.resolution_factor); - ReadSetting("Renderer", Settings::values.use_disk_shader_cache); - ReadSetting("Renderer", Settings::values.frame_limit); - ReadSetting("Renderer", Settings::values.use_vsync); - ReadSetting("Renderer", Settings::values.texture_filter); - ReadSetting("Renderer", Settings::values.texture_sampling); - ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); - - ReadSetting("Renderer", Settings::values.mono_render_option); - ReadSetting("Renderer", Settings::values.render_3d); - ReadSetting("Renderer", Settings::values.factor_3d); - ReadSetting("Renderer", Settings::values.pp_shader_name); - ReadSetting("Renderer", Settings::values.anaglyph_shader_name); - ReadSetting("Renderer", Settings::values.filter_mode); - - ReadSetting("Renderer", Settings::values.bg_red); - ReadSetting("Renderer", Settings::values.bg_green); - ReadSetting("Renderer", Settings::values.bg_blue); - ReadSetting("Renderer", Settings::values.disable_right_eye_render); - - // Layout - ReadSetting("Layout", Settings::values.layout_option); - ReadSetting("Layout", Settings::values.swap_screen); - ReadSetting("Layout", Settings::values.upright_screen); - ReadSetting("Layout", Settings::values.large_screen_proportion); - ReadSetting("Layout", Settings::values.custom_top_x); - ReadSetting("Layout", Settings::values.custom_top_y); - ReadSetting("Layout", Settings::values.custom_top_width); - ReadSetting("Layout", Settings::values.custom_top_height); - ReadSetting("Layout", Settings::values.custom_bottom_x); - ReadSetting("Layout", Settings::values.custom_bottom_y); - ReadSetting("Layout", Settings::values.custom_bottom_width); - ReadSetting("Layout", Settings::values.custom_bottom_height); - ReadSetting("Layout", Settings::values.custom_second_layer_opacity); - - ReadSetting("Layout", Settings::values.screen_top_stretch); - ReadSetting("Layout", Settings::values.screen_top_leftright_padding); - ReadSetting("Layout", Settings::values.screen_top_topbottom_padding); - ReadSetting("Layout", Settings::values.screen_bottom_stretch); - ReadSetting("Layout", Settings::values.screen_bottom_leftright_padding); - ReadSetting("Layout", Settings::values.screen_bottom_topbottom_padding); - - ReadSetting("Layout", Settings::values.portrait_layout_option); - ReadSetting("Layout", Settings::values.custom_portrait_top_x); - ReadSetting("Layout", Settings::values.custom_portrait_top_y); - ReadSetting("Layout", Settings::values.custom_portrait_top_width); - ReadSetting("Layout", Settings::values.custom_portrait_top_height); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_x); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_y); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_width); - ReadSetting("Layout", Settings::values.custom_portrait_bottom_height); - - // Utility - ReadSetting("Utility", Settings::values.dump_textures); - ReadSetting("Utility", Settings::values.custom_textures); - ReadSetting("Utility", Settings::values.preload_textures); - ReadSetting("Utility", Settings::values.async_custom_loading); - - // Audio - ReadSetting("Audio", Settings::values.audio_emulation); - ReadSetting("Audio", Settings::values.enable_audio_stretching); - ReadSetting("Audio", Settings::values.enable_realtime_audio); - ReadSetting("Audio", Settings::values.volume); - ReadSetting("Audio", Settings::values.output_type); - ReadSetting("Audio", Settings::values.output_device); - ReadSetting("Audio", Settings::values.input_type); - ReadSetting("Audio", Settings::values.input_device); - - // Data Storage - ReadSetting("Data Storage", Settings::values.use_virtual_sd); - ReadSetting("Data Storage", Settings::values.use_custom_storage); - ReadSetting("Data Storage", Settings::values.compress_cia_installs); - - if (Settings::values.use_custom_storage) { - FileUtil::UpdateUserPath(FileUtil::UserPath::NANDDir, - sdl2_config->GetString("Data Storage", "nand_directory", "")); - FileUtil::UpdateUserPath(FileUtil::UserPath::SDMCDir, - sdl2_config->GetString("Data Storage", "sdmc_directory", "")); - } - - // System - ReadSetting("System", Settings::values.is_new_3ds); - ReadSetting("System", Settings::values.lle_applets); - ReadSetting("System", Settings::values.enable_required_online_lle_modules); - ReadSetting("System", Settings::values.region_value); - ReadSetting("System", Settings::values.init_clock); - { - std::tm t; - t.tm_sec = 1; - t.tm_min = 0; - t.tm_hour = 0; - t.tm_mday = 1; - t.tm_mon = 0; - t.tm_year = 100; - t.tm_isdst = 0; - std::istringstream string_stream( - sdl2_config->GetString("System", "init_time", "2000-01-01 00:00:01")); - string_stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S"); - if (string_stream.fail()) { - LOG_ERROR(Config, "Failed To parse init_time. Using 2000-01-01 00:00:01"); - } - Settings::values.init_time = - std::chrono::duration_cast( - std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch()) - .count(); - } - ReadSetting("System", Settings::values.init_ticks_type); - ReadSetting("System", Settings::values.init_ticks_override); - ReadSetting("System", Settings::values.plugin_loader_enabled); - ReadSetting("System", Settings::values.allow_plugin_loader); - ReadSetting("System", Settings::values.steps_per_hour); - ReadSetting("System", Settings::values.apply_region_free_patch); - - { - constexpr const char* default_init_time_offset = "0 00:00:00"; - - std::string offset_string = - sdl2_config->GetString("System", "init_time_offset", default_init_time_offset); - - std::size_t sep_index = offset_string.find(' '); - - if (sep_index == std::string::npos) { - LOG_ERROR(Config, "Failed to parse init_time_offset. Using 0 00:00:00"); - offset_string = default_init_time_offset; - - sep_index = offset_string.find(' '); - } - - std::string day_string = offset_string.substr(0, sep_index); - long long days = 0; - - try { - days = std::stoll(day_string); - } catch (std::logic_error&) { - LOG_ERROR(Config, "Failed to parse days in init_time_offset. Using 0"); - days = 0; - } - - long long days_in_seconds = days * 86400; - - std::tm t; - t.tm_sec = 0; - t.tm_min = 0; - t.tm_hour = 0; - t.tm_mday = 1; - t.tm_mon = 0; - t.tm_year = 100; - t.tm_isdst = 0; - - std::istringstream string_stream(offset_string.substr(sep_index + 1)); - string_stream >> std::get_time(&t, "%H:%M:%S"); - - if (string_stream.fail()) { - LOG_ERROR(Config, - "Failed to parse hours, minutes and seconds in init_time_offset. 00:00:00"); - } - - auto time_offset = - std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch(); - - auto secs = std::chrono::duration_cast(time_offset).count(); - - Settings::values.init_time_offset = static_cast(secs) + days_in_seconds; - } - - // Camera - using namespace Service::CAM; - Settings::values.camera_name[OuterRightCamera] = - sdl2_config->GetString("Camera", "camera_outer_right_name", "blank"); - Settings::values.camera_config[OuterRightCamera] = - sdl2_config->GetString("Camera", "camera_outer_right_config", ""); - Settings::values.camera_flip[OuterRightCamera] = - sdl2_config->GetInteger("Camera", "camera_outer_right_flip", 0); - Settings::values.camera_name[InnerCamera] = - sdl2_config->GetString("Camera", "camera_inner_name", "blank"); - Settings::values.camera_config[InnerCamera] = - sdl2_config->GetString("Camera", "camera_inner_config", ""); - Settings::values.camera_flip[InnerCamera] = - sdl2_config->GetInteger("Camera", "camera_inner_flip", 0); - Settings::values.camera_name[OuterLeftCamera] = - sdl2_config->GetString("Camera", "camera_outer_left_name", "blank"); - Settings::values.camera_config[OuterLeftCamera] = - sdl2_config->GetString("Camera", "camera_outer_left_config", ""); - Settings::values.camera_flip[OuterLeftCamera] = - sdl2_config->GetInteger("Camera", "camera_outer_left_flip", 0); - - // Miscellaneous - ReadSetting("Miscellaneous", Settings::values.log_filter); - ReadSetting("Miscellaneous", Settings::values.log_regex_filter); - ReadSetting("Miscellaneous", Settings::values.delay_start_for_lle_modules); - ReadSetting("Miscellaneous", Settings::values.deterministic_async_operations); - - // Apply the log_filter setting as the logger has already been initialized - // and doesn't pick up the filter on its own. - Common::Log::Filter filter; - filter.ParseFilterString(Settings::values.log_filter.GetValue()); - Common::Log::SetGlobalFilter(filter); - Common::Log::SetRegexFilter(Settings::values.log_regex_filter.GetValue()); - - // Debugging - Settings::values.record_frame_times = - sdl2_config->GetBoolean("Debugging", "record_frame_times", false); - ReadSetting("Debugging", Settings::values.renderer_debug); - ReadSetting("Debugging", Settings::values.use_gdbstub); - ReadSetting("Debugging", Settings::values.gdbstub_port); - ReadSetting("Debugging", Settings::values.instant_debug_log); - ReadSetting("Debugging", Settings::values.enable_rpc_server); - - for (const auto& service_module : Service::service_module_map) { - bool use_lle = sdl2_config->GetBoolean("Debugging", "LLE\\" + service_module.name, false); - Settings::values.lle_modules.emplace(service_module.name, use_lle); - } - - // Web Service - NetSettings::values.web_api_url = - sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org"); - NetSettings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", ""); - NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", ""); - - // Video Dumping - Settings::values.output_format = - sdl2_config->GetString("Video Dumping", "output_format", "webm"); - Settings::values.format_options = sdl2_config->GetString("Video Dumping", "format_options", ""); - - Settings::values.video_encoder = - sdl2_config->GetString("Video Dumping", "video_encoder", "libvpx-vp9"); - - // Options for variable bit rate live streaming taken from here: - // https://developers.google.com/media/vp9/live-encoding - std::string default_video_options; - if (Settings::values.video_encoder == "libvpx-vp9") { - default_video_options = - "quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1"; - } - Settings::values.video_encoder_options = - sdl2_config->GetString("Video Dumping", "video_encoder_options", default_video_options); - Settings::values.video_bitrate = - sdl2_config->GetInteger("Video Dumping", "video_bitrate", 2500000); - - Settings::values.audio_encoder = - sdl2_config->GetString("Video Dumping", "audio_encoder", "libvorbis"); - Settings::values.audio_encoder_options = - sdl2_config->GetString("Video Dumping", "audio_encoder_options", ""); - Settings::values.audio_bitrate = - sdl2_config->GetInteger("Video Dumping", "audio_bitrate", 64000); -} - -void SdlConfig::Reload() { - LoadINI(DefaultINI::sdl2_config_file); - ReadValues(); -} diff --git a/src/citra_sdl/config.h b/src/citra_sdl/config.h deleted file mode 100644 index d7c3e6c80..000000000 --- a/src/citra_sdl/config.h +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2014 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include -#include "common/settings.h" - -class INIReader; - -class SdlConfig { - std::unique_ptr sdl2_config; - std::string sdl2_config_loc; - - bool LoadINI(const std::string& default_contents = "", bool retry = true); - void ReadValues(); - -public: - SdlConfig(); - ~SdlConfig(); - - void Reload(); - -private: - /** - * Applies a value read from the sdl2_config to a Setting. - * - * @param group The name of the INI group - * @param setting The yuzu setting to modify - */ - template - void ReadSetting(const std::string& group, Settings::Setting& setting); -}; diff --git a/src/citra_sdl/default_ini.h b/src/citra_sdl/default_ini.h deleted file mode 100644 index 4c97dbb89..000000000 --- a/src/citra_sdl/default_ini.h +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright Citra Emulator Project / Azahar Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -namespace DefaultINI { - -const char* sdl2_config_file = R"( -[Controls] -# The input devices and parameters for each 3DS native input -# It should be in the format of "engine:[engine_name],[param1]:[value1],[param2]:[value2]..." -# Escape characters $0 (for ':'), $1 (for ',') and $2 (for '$') can be used in values - -# for button input, the following devices are available: -# - "keyboard" (default) for keyboard input. Required parameters: -# - "code": the code of the key to bind -# - "sdl" for joystick input using SDL. Required parameters: -# - "joystick": the index of the joystick to bind -# - "button"(optional): the index of the button to bind -# - "hat"(optional): the index of the hat to bind as direction buttons -# - "axis"(optional): the index of the axis to bind -# - "direction"(only used for hat): the direction name of the hat to bind. Can be "up", "down", "left" or "right" -# - "threshold"(only used for axis): a float value in (-1.0, 1.0) which the button is -# triggered if the axis value crosses -# - "direction"(only used for axis): "+" means the button is triggered when the axis value -# is greater than the threshold; "-" means the button is triggered when the axis value -# is smaller than the threshold -button_a= -button_b= -button_x= -button_y= -button_up= -button_down= -button_left= -button_right= -button_l= -button_r= -button_start= -button_select= -button_debug= -button_gpio14= -button_zl= -button_zr= -button_home= - -# for analog input, the following devices are available: -# - "analog_from_button" (default) for emulating analog input from direction buttons. Required parameters: -# - "up", "down", "left", "right": sub-devices for each direction. -# Should be in the format as a button input devices using escape characters, for example, "engine$0keyboard$1code$00" -# - "modifier": sub-devices as a modifier. -# - "modifier_scale": a float number representing the applied modifier scale to the analog input. -# Must be in range of 0.0-1.0. Defaults to 0.5 -# - "sdl" for joystick input using SDL. Required parameters: -# - "joystick": the index of the joystick to bind -# - "axis_x": the index of the axis to bind as x-axis (default to 0) -# - "axis_y": the index of the axis to bind as y-axis (default to 1) -circle_pad= -c_stick= - -# for motion input, the following devices are available: -# - "motion_emu" (default) for emulating motion input from mouse input. Required parameters: -# - "update_period": update period in milliseconds (default to 100) -# - "sensitivity": the coefficient converting mouse movement to tilting angle (default to 0.01) -# - "tilt_clamp": the max value of the tilt angle in degrees (default to 90) -# - "cemuhookudp" reads motion input from a udp server that uses cemuhook's udp protocol -motion_device= - -# for touch input, the following devices are available: -# - "emu_window" (default) for emulating touch input from mouse input to the emulation window. No parameters required -# - "cemuhookudp" reads touch input from a udp server that uses cemuhook's udp protocol -# - "min_x", "min_y", "max_x", "max_y": defines the udp device's touch screen coordinate system -touch_device= - -# Most desktop operating systems do not expose a way to poll the motion state of the controllers -# so as a way around it, cemuhook created a udp client/server protocol to broadcast the data directly -# from a controller device to the client program. Citra has a client that can connect and read -# from any cemuhook compatible motion program. - -# IPv4 address of the udp input server (Default "127.0.0.1") -udp_input_address= - -# Port of the udp input server. (Default 26760) -udp_input_port= - -# The pad to request data on. Should be between 0 (Pad 1) and 3 (Pad 4). (Default 0) -udp_pad_index= - -[Core] -# Whether to use the Just-In-Time (JIT) compiler for CPU emulation -# 0: Interpreter (slow), 1 (default): JIT (fast) -use_cpu_jit = - -# Change the Clock Frequency of the emulated 3DS CPU. -# Underclocking can increase the performance of the game at the risk of freezing. -# Overclocking may fix lag that happens on console, but also comes with the risk of freezing. -# Range is any positive integer (but we suspect 25 - 400 is a good idea) Default is 100 -cpu_clock_percentage = - -[Renderer] -# Whether to render using OpenGL or Software -# 0: Software, 1: OpenGL (default), 2: Vulkan -graphics_api = - -# Whether to render using GLES or OpenGL -# 0 (default): OpenGL, 1: GLES -use_gles = - -# Whether to use hardware shaders to emulate 3DS shaders -# 0: Software, 1 (default): Hardware -use_hw_shader = - -# Whether to use accurate multiplication in hardware shaders -# 0: Off (Faster, but causes issues in some games) 1: On (Default. Slower, but correct) -shaders_accurate_mul = - -# Whether to use the Just-In-Time (JIT) compiler for shader emulation -# 0: Interpreter (slow), 1 (default): JIT (fast) -use_shader_jit = - -# Forces VSync on the display thread. Usually doesn't impact performance, but on some drivers it can -# so only turn this off if you notice a speed difference. -# 0: Off, 1 (default): On -use_vsync = - -# Reduce stuttering by storing and loading generated shaders to disk -# 0: Off, 1 (default. On) -use_disk_shader_cache = - -# Resolution scale factor -# 0: Auto (scales resolution to window size), 1: Native 3DS screen resolution, Otherwise a scale -# factor for the 3DS resolution -resolution_factor = - -# Texture filter -# 0: None, 1: Anime4K, 2: Bicubic, 3: Nearest Neighbor, 4: ScaleForce, 5: xBRZ -texture_filter = - -# Limits the speed of the game to run no faster than this value as a percentage of target speed. -# Will not have an effect if unthrottled is enabled. -# 5 - 995: Speed limit as a percentage of target game speed. 0 for unthrottled. 100 (default) -frame_limit = - -# Overrides the frame limiter to use frame_limit_alternate instead of frame_limit. -# 0: Off (default), 1: On -use_frame_limit_alternate = - -# Alternate speed limit to be used instead of frame_limit if use_frame_limit_alternate is enabled -# 5 - 995: Speed limit as a percentage of target game speed. 0 for unthrottled. 200 (default) -frame_limit_alternate = - -# The clear color for the renderer. What shows up on the sides of the bottom screen. -# Must be in range of 0.0-1.0. Defaults to 0.0 for all. -bg_red = -bg_blue = -bg_green = - -# Whether and how Stereoscopic 3D should be rendered -# 0 (default): Off, 1: Side by Side, 2: Reverse Side by Side, 3: Anaglyph, 4: Interlaced, 5: Reverse Interlaced -render_3d = - -# Change 3D Intensity -# 0 - 100: Intensity. 0 (default) -factor_3d = - -# Swap Eyes in 3D -# true or false (default) -swap_eyes_3d = - -# Change Default Eye to Render When in Monoscopic Mode -# 0 (default): Left, 1: Right -mono_render_option = - -# The name of the post processing shader to apply. -# Loaded from shaders if render_3d is off or side by side. -pp_shader_name = - -# The name of the shader to apply when render_3d is anaglyph. -# Loaded from shaders/anaglyph -anaglyph_shader_name = - -# Whether to enable linear filtering or not -# This is required for some shaders to work correctly -# 0: Nearest, 1 (default): Linear -filter_mode = - -[Layout] -# Layout for the screen inside the render window. -# 0 (default): Default Above/Below Screen -# 1: Single Screen Only -# 2: Large Screen Small Screen -# 3: Side by Side -# 4: Separate Windows -# 5: Hybrid Screen -# 6: Custom Layout -layout_option = - -# Screen placement when using Custom layout option -# 0x, 0y is the top left corner of the render window. -custom_top_x = -custom_top_y = -custom_top_width = -custom_top_height = -custom_bottom_x = -custom_bottom_y = -custom_bottom_width = -custom_bottom_height = - -# Opacity of second layer when using custom layout option (bottom screen unless swapped) -custom_second_layer_opacity = - -# Swaps the prominent screen with the other screen. -# For example, if Single Screen is chosen, setting this to 1 will display the bottom screen instead of the top screen. -# 0 (default): Top Screen is prominent, 1: Bottom Screen is prominent -swap_screen = - -# Toggle upright orientation, for book style games. -# 0 (default): Off, 1: On -upright_screen = - -# The proportion between the large and small screens when playing in Large Screen Small Screen layout. -# Must be a real value between 1.0 and 16.0. Default is 4 -large_screen_proportion = - -# Dumps textures as PNG to dump/textures/[Title ID]/. -# 0 (default): Off, 1: On -dump_textures = - -# Reads PNG files from load/textures/[Title ID]/ and replaces textures. -# 0 (default): Off, 1: On -custom_textures = - -# Loads all custom textures into memory before booting. -# 0 (default): Off, 1: On -preload_textures = - -# Loads custom textures asynchronously with background threads. -# 0: Off, 1 (default): On -async_custom_loading = - -[Audio] -# Whether or not to enable DSP LLE -# 0 (default): No, 1: Yes -enable_dsp_lle = - -# Whether or not to run DSP LLE on a different thread -# 0 (default): No, 1: Yes -enable_dsp_lle_thread = - -# Whether or not to enable the audio-stretching post-processing effect. -# This effect adjusts audio speed to match emulation speed and helps prevent audio stutter, -# at the cost of increasing audio latency. -# 0: No, 1 (default): Yes -enable_audio_stretching = - -# Scales audio playback speed to account for drops in emulation framerate -# 0 (default): No, 1: Yes -enable_realtime_audio = - -# Output volume. -# 1.0 (default): 100%, 0.0; mute -volume = - -# Which audio output type to use. -# 0 (default): Auto-select, 1: No audio output, 2: Cubeb (if available), 3: OpenAL (if available), 4: SDL2 (if available) -output_type = - -# Which audio output device to use. -# auto (default): Auto-select -output_device = - -# Which audio input type to use. -# 0 (default): Auto-select, 1: No audio input, 2: Static noise, 3: Cubeb (if available), 4: OpenAL (if available) -input_type = - -# Which audio input device to use. -# auto (default): Auto-select -input_device = - -[Data Storage] -# Whether to create a virtual SD card. -# 1 (default): Yes, 0: No -use_virtual_sd = - -# Whether to use custom storage locations -# 1: Yes, 0 (default): No -use_custom_storage = - -# The path of the virtual SD card directory. -# empty (default) will use the user_path -sdmc_directory = - -# The path of NAND directory. -# empty (default) will use the user_path -nand_directory = - -[System] -# The system model that Citra will try to emulate -# 0: Old 3DS, 1: New 3DS (default) -is_new_3ds = - -# Whether to use LLE system applets, if installed -# 0 (default): No, 1: Yes -lle_applets = - -# The system region that Citra will use during emulation -# -1: Auto-select (default), 0: Japan, 1: USA, 2: Europe, 3: Australia, 4: China, 5: Korea, 6: Taiwan -region_value = - -# The clock to use when citra starts -# 0: System clock (default), 1: fixed time -init_clock = - -# Time used when init_clock is set to fixed_time in the format %Y-%m-%d %H:%M:%S -# set to fixed time. Default 2000-01-01 00:00:01 -# Note: 3DS can only handle times later then Jan 1 2000 -init_time = - -# The system ticks count to use when citra starts -# 0: Random (default), 1: Fixed -init_ticks_type = - -# Tick count to use when init_ticks_type is set to Fixed. -# Defaults to 0. -init_ticks_override = - -# Number of steps per hour reported by the pedometer. Range from 0 to 65,535. -# Defaults to 0. -steps_per_hour = - -[Camera] -# Which camera engine to use for the right outer camera -# blank (default): a dummy camera that always returns black image -camera_outer_right_name = - -# A config string for the right outer camera. Its meaning is defined by the camera engine -camera_outer_right_config = - -# The image flip to apply -# 0: None (default), 1: Horizontal, 2: Vertical, 3: Reverse -camera_outer_right_flip = - -# ... for the left outer camera -camera_outer_left_name = -camera_outer_left_config = -camera_outer_left_flip = - -# ... for the inner camera -camera_inner_name = -camera_inner_config = -camera_inner_flip = - -[Miscellaneous] -# A filter which removes logs below a certain logging level. -# Examples: *:Debug Kernel.SVC:Trace Service.*:Critical -log_filter = *:Info - -[Debugging] -# Record frame time data, can be found in the log directory. Boolean value -record_frame_times = - -# Port for listening to GDB connections. -use_gdbstub=false -gdbstub_port=24689 - -# Whether to enable additional debugging information during emulation -# 0 (default): Off, 1: On -renderer_debug = - -# To LLE a service module add "LLE\=true" - -[WebService] -# URL for Web API -web_api_url = -# Username and token for Citra Web Service -citra_username = -citra_token = - -[Video Dumping] -# Format of the video to output, default: webm -output_format = - -# Options passed to the muxer (optional) -# This is a param package, format: [key1]:[value1],[key2]:[value2],... -format_options = - -# Video encoder used, default: libvpx-vp9 -video_encoder = - -# Options passed to the video codec (optional) -video_encoder_options = - -# Video bitrate, default: 2500000 -video_bitrate = - -# Audio encoder used, default: libvorbis -audio_encoder = - -# Options passed to the audio codec (optional) -audio_encoder_options = - -# Audio bitrate, default: 64000 -audio_bitrate = -)"; -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2.cpp b/src/citra_sdl/emu_window/emu_window_sdl2.cpp deleted file mode 100644 index a84e38669..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2.cpp +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#define SDL_MAIN_HANDLED -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" -#include "common/logging/log.h" -#include "common/scm_rev.h" -#include "core/core.h" -#include "input_common/keyboard.h" -#include "input_common/main.h" -#include "input_common/motion_emu.h" -#include "network/network.h" - -void EmuWindow_SDL2::OnMouseMotion(s32 x, s32 y) { - TouchMoved((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); - InputCommon::GetMotionEmu()->Tilt(x, y); -} - -void EmuWindow_SDL2::OnMouseButton(u32 button, u8 state, s32 x, s32 y) { - if (button == SDL_BUTTON_LEFT) { - if (state == SDL_PRESSED) { - TouchPressed((unsigned)std::max(x, 0), (unsigned)std::max(y, 0)); - } else { - TouchReleased(); - } - } else if (button == SDL_BUTTON_RIGHT) { - if (state == SDL_PRESSED) { - InputCommon::GetMotionEmu()->BeginTilt(x, y); - } else { - InputCommon::GetMotionEmu()->EndTilt(); - } - } -} - -std::pair EmuWindow_SDL2::TouchToPixelPos(float touch_x, float touch_y) const { - int w, h; - SDL_GetWindowSize(render_window, &w, &h); - - touch_x *= w; - touch_y *= h; - - return {static_cast(std::max(std::round(touch_x), 0.0f)), - static_cast(std::max(std::round(touch_y), 0.0f))}; -} - -void EmuWindow_SDL2::OnFingerDown(float x, float y) { - // TODO(NeatNit): keep track of multitouch using the fingerID and a dictionary of some kind - // This isn't critical because the best we can do when we have that is to average them, like the - // 3DS does - - const auto [px, py] = TouchToPixelPos(x, y); - TouchPressed(px, py); -} - -void EmuWindow_SDL2::OnFingerMotion(float x, float y) { - const auto [px, py] = TouchToPixelPos(x, y); - TouchMoved(px, py); -} - -void EmuWindow_SDL2::OnFingerUp() { - TouchReleased(); -} - -void EmuWindow_SDL2::OnKeyEvent(int key, u8 state) { - if (state == SDL_PRESSED) { - InputCommon::GetKeyboard()->PressKey(key); - } else if (state == SDL_RELEASED) { - InputCommon::GetKeyboard()->ReleaseKey(key); - } -} - -bool EmuWindow_SDL2::IsOpen() const { - return is_open; -} - -void EmuWindow_SDL2::RequestClose() { - is_open = false; -} - -void EmuWindow_SDL2::OnResize() { - int width, height; - SDL_GL_GetDrawableSize(render_window, &width, &height); - UpdateCurrentFramebufferLayout(width, height); -} - -void EmuWindow_SDL2::Fullscreen() { - if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN) == 0) { - return; - } - - LOG_ERROR(Frontend, "Fullscreening failed: {}", SDL_GetError()); - - // Try a different fullscreening method - LOG_INFO(Frontend, "Attempting to use borderless fullscreen..."); - if (SDL_SetWindowFullscreen(render_window, SDL_WINDOW_FULLSCREEN_DESKTOP) == 0) { - return; - } - - LOG_ERROR(Frontend, "Borderless fullscreening failed: {}", SDL_GetError()); - - // Fallback algorithm: Maximise window. - // Works on all systems (unless something is seriously wrong), so no fallback for this one. - LOG_INFO(Frontend, "Falling back on a maximised window..."); - SDL_MaximizeWindow(render_window); -} - -EmuWindow_SDL2::EmuWindow_SDL2(Core::System& system_, bool is_secondary) - : EmuWindow(is_secondary), system(system_) {} - -EmuWindow_SDL2::~EmuWindow_SDL2() { - SDL_Quit(); -} - -void EmuWindow_SDL2::InitializeSDL2() { - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0) { - LOG_CRITICAL(Frontend, "Failed to initialize SDL2: {}! Exiting...", SDL_GetError()); - exit(1); - } - - InputCommon::Init(); - Network::Init(); - - SDL_SetMainReady(); -} - -u32 EmuWindow_SDL2::GetEventWindowId(const SDL_Event& event) const { - switch (event.type) { - case SDL_WINDOWEVENT: - return event.window.windowID; - case SDL_KEYDOWN: - case SDL_KEYUP: - return event.key.windowID; - case SDL_MOUSEMOTION: - return event.motion.windowID; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - return event.button.windowID; - case SDL_MOUSEWHEEL: - return event.wheel.windowID; - case SDL_FINGERDOWN: - case SDL_FINGERMOTION: - case SDL_FINGERUP: - return event.tfinger.windowID; - case SDL_TEXTEDITING: - return event.edit.windowID; - case SDL_TEXTEDITING_EXT: - return event.editExt.windowID; - case SDL_TEXTINPUT: - return event.text.windowID; - case SDL_DROPBEGIN: - case SDL_DROPFILE: - case SDL_DROPTEXT: - case SDL_DROPCOMPLETE: - return event.drop.windowID; - case SDL_USEREVENT: - return event.user.windowID; - default: - // Event is not for any particular window, so we can just pretend it's for this one. - return render_window_id; - } -} - -void EmuWindow_SDL2::PollEvents() { - SDL_Event event; - std::vector other_window_events; - - // SDL_PollEvent returns 0 when there are no more events in the event queue - while (SDL_PollEvent(&event)) { - if (GetEventWindowId(event) != render_window_id) { - other_window_events.push_back(event); - continue; - } - - switch (event.type) { - case SDL_WINDOWEVENT: - switch (event.window.event) { - case SDL_WINDOWEVENT_SIZE_CHANGED: - case SDL_WINDOWEVENT_RESIZED: - case SDL_WINDOWEVENT_MAXIMIZED: - case SDL_WINDOWEVENT_RESTORED: - case SDL_WINDOWEVENT_MINIMIZED: - OnResize(); - break; - case SDL_WINDOWEVENT_CLOSE: - RequestClose(); - break; - } - break; - case SDL_KEYDOWN: - case SDL_KEYUP: - OnKeyEvent(static_cast(event.key.keysym.scancode), event.key.state); - break; - case SDL_MOUSEMOTION: - // ignore if it came from touch - if (event.button.which != SDL_TOUCH_MOUSEID) - OnMouseMotion(event.motion.x, event.motion.y); - break; - case SDL_MOUSEBUTTONDOWN: - case SDL_MOUSEBUTTONUP: - // ignore if it came from touch - if (event.button.which != SDL_TOUCH_MOUSEID) { - OnMouseButton(event.button.button, event.button.state, event.button.x, - event.button.y); - } - break; - case SDL_FINGERDOWN: - OnFingerDown(event.tfinger.x, event.tfinger.y); - break; - case SDL_FINGERMOTION: - OnFingerMotion(event.tfinger.x, event.tfinger.y); - break; - case SDL_FINGERUP: - OnFingerUp(); - break; - case SDL_QUIT: - RequestClose(); - break; - default: - break; - } - } - for (auto& e : other_window_events) { - // This is a somewhat hacky workaround to re-emit window events meant for another window - // since SDL_PollEvent() is global but we poll events per window. - SDL_PushEvent(&e); - } - if (!is_secondary) { - UpdateFramerateCounter(); - } -} - -void EmuWindow_SDL2::OnMinimalClientAreaChangeRequest(std::pair minimal_size) { - SDL_SetWindowMinimumSize(render_window, minimal_size.first, minimal_size.second); -} - -void EmuWindow_SDL2::UpdateFramerateCounter() { - const u32 current_time = SDL_GetTicks(); - if (current_time > last_time + 2000) { - const auto results = system.GetAndResetPerfStats(); - const auto title = - fmt::format("Azahar {} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc, results.game_fps, - results.emulation_speed * 100.0f); - SDL_SetWindowTitle(render_window, title.c_str()); - last_time = current_time; - } -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2.h b/src/citra_sdl/emu_window/emu_window_sdl2.h deleted file mode 100644 index 28f86f81a..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2.h +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2016 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "common/common_types.h" -#include "core/frontend/emu_window.h" - -union SDL_Event; -struct SDL_Window; - -namespace Core { -class System; -} - -class EmuWindow_SDL2 : public Frontend::EmuWindow { -public: - explicit EmuWindow_SDL2(Core::System& system_, bool is_secondary); - ~EmuWindow_SDL2(); - - /// Initializes SDL2 - static void InitializeSDL2(); - - /// Presents the most recent frame from the video backend - virtual void Present() {} - - /// Polls window events - void PollEvents() override; - - /// Whether the window is still open, and a close request hasn't yet been sent - bool IsOpen() const; - - /// Close the window. - void RequestClose(); - -protected: - /// Gets the ID of the window an event originated from. - u32 GetEventWindowId(const SDL_Event& event) const; - - /// Called by PollEvents when a key is pressed or released. - void OnKeyEvent(int key, u8 state); - - /// Called by PollEvents when the mouse moves. - void OnMouseMotion(s32 x, s32 y); - - /// Called by PollEvents when a mouse button is pressed or released - void OnMouseButton(u32 button, u8 state, s32 x, s32 y); - - /// Translates pixel position (0..1) to pixel positions - std::pair TouchToPixelPos(float touch_x, float touch_y) const; - - /// Called by PollEvents when a finger starts touching the touchscreen - void OnFingerDown(float x, float y); - - /// Called by PollEvents when a finger moves while touching the touchscreen - void OnFingerMotion(float x, float y); - - /// Called by PollEvents when a finger stops touching the touchscreen - void OnFingerUp(); - - /// Called by PollEvents when any event that may cause the window to be resized occurs - void OnResize(); - - /// Called when user passes the fullscreen parameter flag - void Fullscreen(); - - /// Called when a configuration change affects the minimal size of the window - void OnMinimalClientAreaChangeRequest(std::pair minimal_size) override; - - /// Called when polling to update framerate - void UpdateFramerateCounter(); - - /// Is the window still open? - bool is_open = true; - - /// Internal SDL2 render window - SDL_Window* render_window; - - /// Internal SDL2 window ID - u32 render_window_id{}; - - /// Fake hidden window for the core context - SDL_Window* dummy_window; - - /// Keeps track of how often to update the title bar during gameplay - u32 last_time = 0; - - Core::System& system; -}; diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp b/src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp deleted file mode 100644 index f2fc28dc3..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_gl.cpp +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#define SDL_MAIN_HANDLED -#include -#include -#include "citra_sdl/emu_window/emu_window_sdl2_gl.h" -#include "common/scm_rev.h" -#include "common/settings.h" -#include "core/core.h" -#include "video_core/gpu.h" -#include "video_core/renderer_base.h" - -class SDLGLContext : public Frontend::GraphicsContext { -public: - using SDL_GLContext = void*; - - SDLGLContext() { - window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, - SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL); - context = SDL_GL_CreateContext(window); - } - - ~SDLGLContext() override { - SDL_GL_DeleteContext(context); - SDL_DestroyWindow(window); - } - - void MakeCurrent() override { - SDL_GL_MakeCurrent(window, context); - } - - void DoneCurrent() override { - SDL_GL_MakeCurrent(window, nullptr); - } - -private: - SDL_Window* window; - SDL_GLContext context; -}; - -static SDL_Window* CreateGLWindow(const std::string& window_title, bool gles) { - if (gles) { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); - } else { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); - SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); - } - return SDL_CreateWindow(window_title.c_str(), - SDL_WINDOWPOS_UNDEFINED, // x position - SDL_WINDOWPOS_UNDEFINED, // y position - Core::kScreenTopWidth, - Core::kScreenTopHeight + Core::kScreenBottomHeight, - SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); -} - -EmuWindow_SDL2_GL::EmuWindow_SDL2_GL(Core::System& system_, bool fullscreen, bool is_secondary) - : EmuWindow_SDL2{system_, is_secondary} { - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 0); - // Enable context sharing for the shared context - SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1); - // Enable vsync - SDL_GL_SetSwapInterval(1); - // Enable debug context - if (Settings::values.renderer_debug) { - SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_DEBUG_FLAG); - } - - std::string window_title = fmt::format("Azahar {} | {}-{}", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc); - - // First, try to create a context with the requested type. - render_window = CreateGLWindow(window_title, Settings::values.use_gles.GetValue()); - if (render_window == nullptr) { - // On failure, fall back to context with flipped type. - render_window = CreateGLWindow(window_title, !Settings::values.use_gles.GetValue()); - if (render_window == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 window: {}", SDL_GetError()); - exit(1); - } - } - - strict_context_required = std::strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0; - - dummy_window = SDL_CreateWindow(NULL, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 0, 0, - SDL_WINDOW_HIDDEN | SDL_WINDOW_OPENGL); - - if (fullscreen) { - Fullscreen(); - } - - window_context = SDL_GL_CreateContext(render_window); - core_context = CreateSharedContext(); - last_saved_context = nullptr; - - if (window_context == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 GL context: {}", SDL_GetError()); - exit(1); - } - if (core_context == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create shared SDL2 GL context: {}", SDL_GetError()); - exit(1); - } - - render_window_id = SDL_GetWindowID(render_window); - - int profile_mask = 0; - SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &profile_mask); - auto gl_load_func = - profile_mask == SDL_GL_CONTEXT_PROFILE_ES ? gladLoadGLES2Loader : gladLoadGLLoader; - - if (!gl_load_func(static_cast(SDL_GL_GetProcAddress))) { - LOG_CRITICAL(Frontend, "Failed to initialize GL functions: {}", SDL_GetError()); - exit(1); - } - - OnResize(); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - SDL_PumpEvents(); -} - -EmuWindow_SDL2_GL::~EmuWindow_SDL2_GL() { - core_context.reset(); - SDL_DestroyWindow(render_window); - SDL_GL_DeleteContext(window_context); -} - -std::unique_ptr EmuWindow_SDL2_GL::CreateSharedContext() const { - return std::make_unique(); -} - -void EmuWindow_SDL2_GL::MakeCurrent() { - core_context->MakeCurrent(); -} - -void EmuWindow_SDL2_GL::DoneCurrent() { - core_context->DoneCurrent(); -} - -void EmuWindow_SDL2_GL::SaveContext() { - last_saved_context = SDL_GL_GetCurrentContext(); -} - -void EmuWindow_SDL2_GL::RestoreContext() { - SDL_GL_MakeCurrent(render_window, last_saved_context); -} - -void EmuWindow_SDL2_GL::Present() { - SDL_GL_MakeCurrent(render_window, window_context); - SDL_GL_SetSwapInterval(1); - while (IsOpen()) { - system.GPU().Renderer().TryPresent(100, is_secondary); - SDL_GL_SwapWindow(render_window); - } - SDL_GL_MakeCurrent(render_window, nullptr); -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_gl.h b/src/citra_sdl/emu_window/emu_window_sdl2_gl.h deleted file mode 100644 index 6e9045cba..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_gl.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" - -struct SDL_Window; - -namespace Core { -class System; -} - -class EmuWindow_SDL2_GL : public EmuWindow_SDL2 { -public: - explicit EmuWindow_SDL2_GL(Core::System& system_, bool fullscreen, bool is_secondary); - ~EmuWindow_SDL2_GL(); - - void Present() override; - std::unique_ptr CreateSharedContext() const override; - void MakeCurrent() override; - void DoneCurrent() override; - void SaveContext() override; - void RestoreContext() override; - -private: - using SDL_GLContext = void*; - - /// The OpenGL context associated with the window - SDL_GLContext window_context; - - /// Used by SaveContext and RestoreContext - SDL_GLContext last_saved_context; - - /// The OpenGL context associated with the core - std::unique_ptr core_context; -}; diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp b/src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp deleted file mode 100644 index 8da894fec..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_sw.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#define SDL_MAIN_HANDLED -#include -#include -#include "citra_sdl/emu_window/emu_window_sdl2_sw.h" -#include "common/scm_rev.h" -#include "common/settings.h" -#include "core/core.h" -#include "core/frontend/emu_window.h" -#include "video_core/gpu.h" -#include "video_core/renderer_software/renderer_software.h" - -class DummyContext : public Frontend::GraphicsContext {}; - -EmuWindow_SDL2_SW::EmuWindow_SDL2_SW(Core::System& system_, bool fullscreen, bool is_secondary) - : EmuWindow_SDL2{system_, is_secondary}, system{system_} { - std::string window_title = fmt::format("Azahar {} | {}-{}", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc); - render_window = - SDL_CreateWindow(window_title.c_str(), - SDL_WINDOWPOS_UNDEFINED, // x position - SDL_WINDOWPOS_UNDEFINED, // y position - Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight, - SDL_WINDOW_SHOWN); - - if (render_window == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 window: {}", SDL_GetError()); - exit(1); - } - - window_surface = SDL_GetWindowSurface(render_window); - renderer = SDL_CreateSoftwareRenderer(window_surface); - - if (renderer == nullptr) { - LOG_CRITICAL(Frontend, "Failed to create SDL2 software renderer: {}", SDL_GetError()); - exit(1); - } - - if (fullscreen) { - Fullscreen(); - } - - render_window_id = SDL_GetWindowID(render_window); - - OnResize(); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - SDL_PumpEvents(); -} - -EmuWindow_SDL2_SW::~EmuWindow_SDL2_SW() { - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(render_window); -} - -std::unique_ptr EmuWindow_SDL2_SW::CreateSharedContext() const { - return std::make_unique(); -} - -void EmuWindow_SDL2_SW::Present() { - const auto layout{Layout::DefaultFrameLayout( - Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight, false, false)}; - - using VideoCore::ScreenId; - - while (IsOpen()) { - SDL_SetRenderDrawColor(renderer, - static_cast(Settings::values.bg_red.GetValue() * 255), - static_cast(Settings::values.bg_green.GetValue() * 255), - static_cast(Settings::values.bg_blue.GetValue() * 255), 0xFF); - SDL_RenderClear(renderer); - - const auto draw_screen = [&](ScreenId screen_id) { - const auto dst_rect = - screen_id == ScreenId::TopLeft ? layout.top_screen : layout.bottom_screen; - SDL_Rect sdl_rect{static_cast(dst_rect.left), static_cast(dst_rect.top), - static_cast(dst_rect.GetWidth()), - static_cast(dst_rect.GetHeight())}; - SDL_Surface* screen = LoadFramebuffer(screen_id); - SDL_BlitSurface(screen, nullptr, window_surface, &sdl_rect); - SDL_FreeSurface(screen); - }; - - draw_screen(ScreenId::TopLeft); - draw_screen(ScreenId::Bottom); - - SDL_RenderPresent(renderer); - SDL_UpdateWindowSurface(render_window); - } -} - -SDL_Surface* EmuWindow_SDL2_SW::LoadFramebuffer(VideoCore::ScreenId screen_id) { - const auto& renderer = static_cast(system.GPU().Renderer()); - const auto& info = renderer.Screen(screen_id); - const int width = static_cast(info.width); - const int height = static_cast(info.height); - SDL_Surface* surface = - SDL_CreateRGBSurfaceWithFormat(0, width, height, 0, SDL_PIXELFORMAT_ABGR8888); - SDL_LockSurface(surface); - std::memcpy(surface->pixels, info.pixels.data(), info.pixels.size()); - SDL_UnlockSurface(surface); - return surface; -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_sw.h b/src/citra_sdl/emu_window/emu_window_sdl2_sw.h deleted file mode 100644 index 12f446e33..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_sw.h +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" - -struct SDL_Renderer; -struct SDL_Surface; - -namespace VideoCore { -enum class ScreenId : u32; -} - -namespace Core { -class System; -} - -class EmuWindow_SDL2_SW : public EmuWindow_SDL2 { -public: - explicit EmuWindow_SDL2_SW(Core::System& system, bool fullscreen, bool is_secondary); - ~EmuWindow_SDL2_SW(); - - void Present() override; - std::unique_ptr CreateSharedContext() const override; - void MakeCurrent() override {} - void DoneCurrent() override {} - -private: - /// Loads a framebuffer to an SDL surface - SDL_Surface* LoadFramebuffer(VideoCore::ScreenId screen_id); - - /// The system class. - Core::System& system; - - /// The SDL software renderer - SDL_Renderer* renderer; - - /// The window surface - SDL_Surface* window_surface; -}; diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp b/src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp deleted file mode 100644 index b002c15bb..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_vk.cpp +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#include -#include -#include -#include -#include -#include -#include "citra_sdl/emu_window/emu_window_sdl2_vk.h" -#include "common/logging/log.h" -#include "common/scm_rev.h" -#include "core/frontend/emu_window.h" - -class DummyContext : public Frontend::GraphicsContext {}; - -EmuWindow_SDL2_VK::EmuWindow_SDL2_VK(Core::System& system, bool fullscreen, bool is_secondary) - : EmuWindow_SDL2{system, is_secondary} { - const std::string window_title = fmt::format("Azahar {} | {}-{}", Common::g_build_fullname, - Common::g_scm_branch, Common::g_scm_desc); - render_window = - SDL_CreateWindow(window_title.c_str(), - SDL_WINDOWPOS_UNDEFINED, // x position - SDL_WINDOWPOS_UNDEFINED, // y position - Core::kScreenTopWidth, Core::kScreenTopHeight + Core::kScreenBottomHeight, - SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); - SDL_SysWMinfo wm; - SDL_VERSION(&wm.version); - if (SDL_GetWindowWMInfo(render_window, &wm) == SDL_FALSE) { - LOG_CRITICAL(Frontend, "Failed to get information from the window manager"); - std::exit(EXIT_FAILURE); - } - - if (fullscreen) { - Fullscreen(); - SDL_ShowCursor(false); - } - - switch (wm.subsystem) { -#ifdef SDL_VIDEO_DRIVER_WINDOWS - case SDL_SYSWM_TYPE::SDL_SYSWM_WINDOWS: - window_info.type = Frontend::WindowSystemType::Windows; - window_info.render_surface = reinterpret_cast(wm.info.win.window); - break; -#endif -#ifdef SDL_VIDEO_DRIVER_X11 - case SDL_SYSWM_TYPE::SDL_SYSWM_X11: - window_info.type = Frontend::WindowSystemType::X11; - window_info.display_connection = wm.info.x11.display; - window_info.render_surface = reinterpret_cast(wm.info.x11.window); - break; -#endif -#ifdef SDL_VIDEO_DRIVER_WAYLAND - case SDL_SYSWM_TYPE::SDL_SYSWM_WAYLAND: - window_info.type = Frontend::WindowSystemType::Wayland; - window_info.display_connection = wm.info.wl.display; - window_info.render_surface = wm.info.wl.surface; - break; -#endif -#ifdef SDL_VIDEO_DRIVER_COCOA - case SDL_SYSWM_TYPE::SDL_SYSWM_COCOA: - window_info.type = Frontend::WindowSystemType::MacOS; - window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(render_window)); - break; -#endif -#ifdef SDL_VIDEO_DRIVER_ANDROID - case SDL_SYSWM_TYPE::SDL_SYSWM_ANDROID: - window_info.type = Frontend::WindowSystemType::Android; - window_info.render_surface = reinterpret_cast(wm.info.android.window); - break; -#endif - default: - LOG_CRITICAL(Frontend, "Window manager subsystem {} not implemented", wm.subsystem); - std::exit(EXIT_FAILURE); - break; - } - - render_window_id = SDL_GetWindowID(render_window); - - OnResize(); - OnMinimalClientAreaChangeRequest(GetActiveConfig().min_client_area_size); - SDL_PumpEvents(); -} - -EmuWindow_SDL2_VK::~EmuWindow_SDL2_VK() = default; - -std::unique_ptr EmuWindow_SDL2_VK::CreateSharedContext() const { - return std::make_unique(); -} diff --git a/src/citra_sdl/emu_window/emu_window_sdl2_vk.h b/src/citra_sdl/emu_window/emu_window_sdl2_vk.h deleted file mode 100644 index ce0f1d8ce..000000000 --- a/src/citra_sdl/emu_window/emu_window_sdl2_vk.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include -#include "citra_sdl/emu_window/emu_window_sdl2.h" - -namespace Frontend { -class GraphicsContext; -} - -namespace Core { -class System; -} - -class EmuWindow_SDL2_VK final : public EmuWindow_SDL2 { -public: - explicit EmuWindow_SDL2_VK(Core::System& system_, bool fullscreen, bool is_secondary); - ~EmuWindow_SDL2_VK() override; - - std::unique_ptr CreateSharedContext() const override; -}; diff --git a/src/citra_sdl/precompiled_headers.h b/src/citra_sdl/precompiled_headers.h deleted file mode 100644 index ffbb5e177..000000000 --- a/src/citra_sdl/precompiled_headers.h +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2022 Citra Emulator Project -// Licensed under GPLv2 or any later version -// Refer to the license.txt file included. - -#pragma once - -#include "common/common_precompiled_headers.h" diff --git a/src/citra_sdl/resource.h b/src/citra_sdl/resource.h deleted file mode 100644 index df8e459e4..000000000 --- a/src/citra_sdl/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by pcafe.rc -// -#define IDI_ICON3 103 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 105 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif From 4010f4bc1f558ee8dd22f4e1bbf039e28f46b563 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:34:03 +0100 Subject: [PATCH 12/94] common/cpu_detect: Remove SSE/SSE2 detection (#1754) --- src/common/x64/cpu_detect.cpp | 4 ---- src/common/x64/cpu_detect.h | 2 -- 2 files changed, 6 deletions(-) diff --git a/src/common/x64/cpu_detect.cpp b/src/common/x64/cpu_detect.cpp index 08290eb7f..b58c4f0c8 100644 --- a/src/common/x64/cpu_detect.cpp +++ b/src/common/x64/cpu_detect.cpp @@ -80,10 +80,6 @@ static CPUCaps Detect() { if (max_std_fn >= 1) { __cpuid(cpu_id, 0x00000001); - if ((cpu_id[3] >> 25) & 1) - caps.sse = true; - if ((cpu_id[3] >> 26) & 1) - caps.sse2 = true; if ((cpu_id[2]) & 1) caps.sse3 = true; if ((cpu_id[2] >> 9) & 1) diff --git a/src/common/x64/cpu_detect.h b/src/common/x64/cpu_detect.h index 7018ec0c1..6d09da549 100644 --- a/src/common/x64/cpu_detect.h +++ b/src/common/x64/cpu_detect.h @@ -16,8 +16,6 @@ namespace Common { struct CPUCaps { char cpu_string[0x21]; char brand_string[0x41]; - bool sse; - bool sse2; bool sse3; bool ssse3; bool sse4_1; From 9628300ff514cd36d035ec2055b6c46b674698a6 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:34:21 +0100 Subject: [PATCH 13/94] citra_meta: Use integrated SSE4.2 detection method (#1753) --- src/citra_meta/main.cpp | 35 +++++------------------------------ 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index f1dba092f..92ea7756f 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -26,43 +26,18 @@ __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; #endif #if CITRA_HAS_SSE42 -#if defined(_WIN32) +#include "common/x64/cpu_detect.h" +#ifdef _WIN32 #include -#if defined(_MSC_VER) -#include -#else -#include -#endif // _MSC_VER -#else -#include -#endif // _WIN32 - -static bool CpuSupportsSSE42() { - uint32_t ecx; - -#if defined(_MSC_VER) - int cpu_info[4]; - __cpuid(cpu_info, 1); - ecx = static_cast(cpu_info[2]); -#elif defined(__GNUC__) || defined(__clang__) - uint32_t eax, ebx, edx; - if (!__get_cpuid(1, &eax, &ebx, &ecx, &edx)) { - return false; - } -#else -#error "Unsupported compiler" #endif - // Bit 20 of ECX indicates SSE4.2 - return (ecx & (1 << 20)) != 0; -} - static bool CheckAndReportSSE42() { - if (!CpuSupportsSSE42()) { + const auto& caps = Common::GetCPUCaps(); + if (!caps.sse4_2) { const std::string error_msg = "This application requires a CPU with SSE4.2 support or higher.\nTo run on unsupported " "systems, recompile the application with the ENABLE_SSE42 option disabled."; -#if defined(_WIN32) +#ifdef _WIN32 MessageBoxA(nullptr, error_msg.c_str(), "Incompatible CPU", MB_OK | MB_ICONERROR); #endif std::cerr << "Error: " << error_msg << std::endl; From 1092295f2aeec32cee1aa6ad9120fe73dcf3d138 Mon Sep 17 00:00:00 2001 From: jbm11208 <81182113+jbm11208@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:39:04 -0500 Subject: [PATCH 14/94] Fix Shadow Rendering / Texture Filtering (#1675) * video_core/renderer_vulkan: Add texture filtering * Fix Shadow Rendering (again...) * Make individual image views per res scale * Refactor texture runtime * Fix some magic numbers * More fixes and filter pipeline cache. * Refactor Surface and Handle move and destructor --------- Co-authored-by: PabloMK7 --- src/common/common_funcs.h | 46 ++ .../rasterizer_cache/pixel_format.h | 9 +- .../renderer_vulkan/vk_blit_helper.cpp | 385 +++++++++++++- .../renderer_vulkan/vk_blit_helper.h | 36 +- .../renderer_vulkan/vk_rasterizer.cpp | 6 +- .../renderer_vulkan/vk_render_manager.cpp | 20 +- .../renderer_vulkan/vk_render_manager.h | 7 +- .../renderer_vulkan/vk_resource_pool.cpp | 5 +- .../renderer_vulkan/vk_texture_runtime.cpp | 486 +++++++++--------- .../renderer_vulkan/vk_texture_runtime.h | 146 ++++-- 10 files changed, 835 insertions(+), 311 deletions(-) diff --git a/src/common/common_funcs.h b/src/common/common_funcs.h index 8f109a8d3..0de82b040 100644 --- a/src/common/common_funcs.h +++ b/src/common/common_funcs.h @@ -1,3 +1,7 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + // Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -102,3 +106,45 @@ __declspec(dllimport) void __stdcall DebugBreak(void); using T = std::underlying_type_t; \ return static_cast(key) == 0; \ } + +#define DECLARE_ENUM_ARITHMETIC_OPERATORS(type) \ + [[nodiscard]] constexpr type operator+(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) + static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator-(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) - static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator*(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) * static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator/(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) / static_cast(b)); \ + } \ + [[nodiscard]] constexpr type operator%(type a, type b) noexcept { \ + using T = std::underlying_type_t; \ + return static_cast(static_cast(a) % static_cast(b)); \ + } \ + constexpr type& operator+=(type& a, type b) noexcept { \ + a = a + b; \ + return a; \ + } \ + constexpr type& operator-=(type& a, type b) noexcept { \ + a = a - b; \ + return a; \ + } \ + constexpr type& operator*=(type& a, type b) noexcept { \ + a = a * b; \ + return a; \ + } \ + constexpr type& operator/=(type& a, type b) noexcept { \ + a = a / b; \ + return a; \ + } \ + constexpr type& operator%=(type& a, type b) noexcept { \ + a = a % b; \ + return a; \ + } diff --git a/src/video_core/rasterizer_cache/pixel_format.h b/src/video_core/rasterizer_cache/pixel_format.h index 06e31ca46..51c5d6480 100644 --- a/src/video_core/rasterizer_cache/pixel_format.h +++ b/src/video_core/rasterizer_cache/pixel_format.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -30,13 +30,20 @@ enum class PixelFormat : u32 { A4 = 11, ETC1 = 12, ETC1A4 = 13, + D16 = 14, D24 = 16, D24S8 = 17, + MaxPixelFormat = 18, + + NumColorFormat = (ETC1A4 - RGBA8) + 1, + NumDepthFormat = (D24S8 - D16) + 1, + Invalid = std::numeric_limits::max(), }; constexpr std::size_t PIXEL_FORMAT_COUNT = static_cast(PixelFormat::MaxPixelFormat); +DECLARE_ENUM_ARITHMETIC_OPERATORS(PixelFormat) enum class SurfaceType : u32 { Color = 0, diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.cpp b/src/video_core/renderer_vulkan/vk_blit_helper.cpp index 0a7a3be44..3052082f7 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_helper.cpp @@ -1,7 +1,9 @@ -// Copyright 2022 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "common/hash.h" +#include "common/settings.h" #include "common/vector_math.h" #include "video_core/renderer_vulkan/vk_blit_helper.h" #include "video_core/renderer_vulkan/vk_descriptor_update_queue.h" @@ -16,8 +18,19 @@ #include "video_core/host_shaders/vulkan_blit_depth_stencil_frag.h" #include "video_core/host_shaders/vulkan_depth_to_buffer_comp.h" +// Texture filtering shader includes +#include "video_core/host_shaders/texture_filtering/bicubic_frag.h" +#include "video_core/host_shaders/texture_filtering/mmpx_frag.h" +#include "video_core/host_shaders/texture_filtering/refine_frag.h" +#include "video_core/host_shaders/texture_filtering/scale_force_frag.h" +#include "video_core/host_shaders/texture_filtering/x_gradient_frag.h" +#include "video_core/host_shaders/texture_filtering/xbrz_freescale_frag.h" +#include "video_core/host_shaders/texture_filtering/y_gradient_frag.h" +#include "vk_blit_helper.h" + namespace Vulkan { +using Settings::TextureFilter; using VideoCore::PixelFormat; namespace { @@ -55,8 +68,33 @@ constexpr std::array TWO_TEXTURES_BINDINGS = {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, }}; +// Texture filtering descriptor set bindings +constexpr std::array SINGLE_TEXTURE_BINDINGS = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, +}}; + +constexpr std::array THREE_TEXTURES_BINDINGS = {{ + {0, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, + {2, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment}, +}}; + +// Note: Removed FILTER_UTILITY_BINDINGS as texture filtering doesn't need shadow buffers + +// Push constant structure for texture filtering +struct FilterPushConstants { + std::array tex_scale; + std::array tex_offset; + float res_scale; // For xBRZ filter +}; + +inline constexpr vk::PushConstantRange FILTER_PUSH_CONSTANT_RANGE{ + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, + .offset = 0, + .size = sizeof(FilterPushConstants), +}; inline constexpr vk::PushConstantRange PUSH_CONSTANT_RANGE{ - .stageFlags = vk::ShaderStageFlagBits::eVertex, + .stageFlags = vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, .offset = 0, .size = sizeof(PushConstants), }; @@ -104,12 +142,17 @@ constexpr vk::PipelineDynamicStateCreateInfo PIPELINE_DYNAMIC_STATE_CREATE_INFO{ .dynamicStateCount = static_cast(DYNAMIC_STATES.size()), .pDynamicStates = DYNAMIC_STATES.data(), }; -constexpr vk::PipelineColorBlendStateCreateInfo PIPELINE_COLOR_BLEND_STATE_EMPTY_CREATE_INFO{ + +constexpr vk::PipelineColorBlendAttachmentState COLOR_BLEND_ATTACHMENT{ + .blendEnable = VK_FALSE, + .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA, +}; + +constexpr vk::PipelineColorBlendStateCreateInfo PIPELINE_COLOR_BLEND_STATE_CREATE_INFO{ .logicOpEnable = VK_FALSE, - .logicOp = vk::LogicOp::eClear, - .attachmentCount = 0, - .pAttachments = nullptr, - .blendConstants = std::array{0.0f, 0.0f, 0.0f, 0.0f}, + .attachmentCount = 1, + .pAttachments = &COLOR_BLEND_ATTACHMENT, }; constexpr vk::PipelineDepthStencilStateCreateInfo PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO{ .depthTestEnable = VK_TRUE, @@ -128,9 +171,9 @@ inline constexpr vk::SamplerCreateInfo SAMPLER_CREATE_INFO{ .magFilter = filter, .minFilter = filter, .mipmapMode = vk::SamplerMipmapMode::eNearest, - .addressModeU = vk::SamplerAddressMode::eClampToBorder, - .addressModeV = vk::SamplerAddressMode::eClampToBorder, - .addressModeW = vk::SamplerAddressMode::eClampToBorder, + .addressModeU = vk::SamplerAddressMode::eClampToEdge, + .addressModeV = vk::SamplerAddressMode::eClampToEdge, + .addressModeW = vk::SamplerAddressMode::eClampToEdge, .mipLodBias = 0.0f, .anisotropyEnable = VK_FALSE, .maxAnisotropy = 0.0f, @@ -143,12 +186,14 @@ inline constexpr vk::SamplerCreateInfo SAMPLER_CREATE_INFO{ }; constexpr vk::PipelineLayoutCreateInfo PipelineLayoutCreateInfo( - const vk::DescriptorSetLayout* set_layout, bool compute = false) { + const vk::DescriptorSetLayout* set_layout, bool compute = false, bool filter = false) { return vk::PipelineLayoutCreateInfo{ .setLayoutCount = 1, .pSetLayouts = set_layout, .pushConstantRangeCount = 1, - .pPushConstantRanges = (compute ? &COMPUTE_PUSH_CONSTANT_RANGE : &PUSH_CONSTANT_RANGE), + .pPushConstantRanges = + (compute ? &COMPUTE_PUSH_CONSTANT_RANGE + : (filter ? &FILTER_PUSH_CONSTANT_RANGE : &PUSH_CONSTANT_RANGE)), }; } @@ -185,12 +230,20 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, compute_provider{instance, scheduler.GetMasterSemaphore(), COMPUTE_BINDINGS}, compute_buffer_provider{instance, scheduler.GetMasterSemaphore(), COMPUTE_BUFFER_BINDINGS}, two_textures_provider{instance, scheduler.GetMasterSemaphore(), TWO_TEXTURES_BINDINGS, 16}, + single_texture_provider{instance, scheduler.GetMasterSemaphore(), SINGLE_TEXTURE_BINDINGS, + 16}, + three_textures_provider{instance, scheduler.GetMasterSemaphore(), THREE_TEXTURES_BINDINGS, + 16}, compute_pipeline_layout{ device.createPipelineLayout(PipelineLayoutCreateInfo(&compute_provider.Layout(), true))}, compute_buffer_pipeline_layout{device.createPipelineLayout( PipelineLayoutCreateInfo(&compute_buffer_provider.Layout(), true))}, two_textures_pipeline_layout{ device.createPipelineLayout(PipelineLayoutCreateInfo(&two_textures_provider.Layout()))}, + single_texture_pipeline_layout{device.createPipelineLayout( + PipelineLayoutCreateInfo(&single_texture_provider.Layout(), false, true))}, + three_textures_pipeline_layout{device.createPipelineLayout( + PipelineLayoutCreateInfo(&three_textures_provider.Layout(), false, true))}, full_screen_vert{Compile(HostShaders::FULL_SCREEN_TRIANGLE_VERT, vk::ShaderStageFlagBits::eVertex, device)}, d24s8_to_rgba8_comp{Compile(HostShaders::VULKAN_D24S8_TO_RGBA8_COMP, @@ -199,6 +252,14 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, vk::ShaderStageFlagBits::eCompute, device)}, blit_depth_stencil_frag{Compile(HostShaders::VULKAN_BLIT_DEPTH_STENCIL_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + // Texture filtering shader modules + bicubic_frag{Compile(HostShaders::BICUBIC_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + scale_force_frag{ + Compile(HostShaders::SCALE_FORCE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + xbrz_frag{ + Compile(HostShaders::XBRZ_FREESCALE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + mmpx_frag{Compile(HostShaders::MMPX_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, + refine_frag{Compile(HostShaders::REFINE_FRAG, vk::ShaderStageFlagBits::eFragment, device)}, d24s8_to_rgba8_pipeline{MakeComputePipeline(d24s8_to_rgba8_comp, compute_pipeline_layout)}, depth_to_buffer_pipeline{ MakeComputePipeline(depth_to_buffer_comp, compute_buffer_pipeline_layout)}, @@ -212,6 +273,10 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, "BlitHelper: compute_buffer_pipeline_layout"); SetObjectName(device, two_textures_pipeline_layout, "BlitHelper: two_textures_pipeline_layout"); + SetObjectName(device, single_texture_pipeline_layout, + "BlitHelper: single_texture_pipeline_layout"); + SetObjectName(device, three_textures_pipeline_layout, + "BlitHelper: three_textures_pipeline_layout"); SetObjectName(device, full_screen_vert, "BlitHelper: full_screen_vert"); SetObjectName(device, d24s8_to_rgba8_comp, "BlitHelper: d24s8_to_rgba8_comp"); SetObjectName(device, depth_to_buffer_comp, "BlitHelper: depth_to_buffer_comp"); @@ -227,13 +292,25 @@ BlitHelper::BlitHelper(const Instance& instance_, Scheduler& scheduler_, } BlitHelper::~BlitHelper() { + for (const auto& [_, pipeline] : filter_pipeline_cache) { + device.destroyPipeline(pipeline); + } + filter_pipeline_cache.clear(); device.destroyPipelineLayout(compute_pipeline_layout); device.destroyPipelineLayout(compute_buffer_pipeline_layout); device.destroyPipelineLayout(two_textures_pipeline_layout); + device.destroyPipelineLayout(single_texture_pipeline_layout); + device.destroyPipelineLayout(three_textures_pipeline_layout); device.destroyShaderModule(full_screen_vert); device.destroyShaderModule(d24s8_to_rgba8_comp); device.destroyShaderModule(depth_to_buffer_comp); device.destroyShaderModule(blit_depth_stencil_frag); + // Destroy texture filtering shader modules + device.destroyShaderModule(bicubic_frag); + device.destroyShaderModule(scale_force_frag); + device.destroyShaderModule(xbrz_frag); + device.destroyShaderModule(mmpx_frag); + device.destroyShaderModule(refine_frag); device.destroyPipeline(depth_to_buffer_pipeline); device.destroyPipeline(d24s8_to_rgba8_pipeline); device.destroyPipeline(depth_blit_pipeline); @@ -242,7 +319,7 @@ BlitHelper::~BlitHelper() { } void BindBlitState(vk::CommandBuffer cmdbuf, vk::PipelineLayout layout, - const VideoCore::TextureBlit& blit) { + const VideoCore::TextureBlit& blit, const Surface& dest) { const vk::Offset2D offset{ .x = std::min(blit.dst_rect.left, blit.dst_rect.right), .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), @@ -272,8 +349,9 @@ void BindBlitState(vk::CommandBuffer cmdbuf, vk::PipelineLayout layout, }; cmdbuf.setViewport(0, viewport); cmdbuf.setScissor(0, scissor); - cmdbuf.pushConstants(layout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(push_constants), - &push_constants); + cmdbuf.pushConstants(layout, + vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment, 0, + sizeof(push_constants), &push_constants); } bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, @@ -300,12 +378,12 @@ bool BlitHelper::BlitDepthStencil(Surface& source, Surface& dest, }; renderpass_cache.BeginRendering(depth_pass); - scheduler.Record([blit, descriptor_set, this](vk::CommandBuffer cmdbuf) { + scheduler.Record([blit, descriptor_set, &dest, this](vk::CommandBuffer cmdbuf) { const vk::PipelineLayout layout = two_textures_pipeline_layout; cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, depth_blit_pipeline); cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, descriptor_set, {}); - BindBlitState(cmdbuf, layout, blit); + BindBlitState(cmdbuf, layout, blit, dest); cmdbuf.draw(3, 1, 0, 0); }); scheduler.MakeDirty(StateFlags::Pipeline); @@ -531,7 +609,7 @@ vk::Pipeline BlitHelper::MakeDepthStencilBlitPipeline() { .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, .pDepthStencilState = &PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO, - .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_EMPTY_CREATE_INFO, + .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, .layout = two_textures_pipeline_layout, .renderPass = renderpass, @@ -547,4 +625,275 @@ vk::Pipeline BlitHelper::MakeDepthStencilBlitPipeline() { return VK_NULL_HANDLE; } +bool BlitHelper::Filter(Surface& surface, const VideoCore::TextureBlit& blit) { + const auto filter = Settings::values.texture_filter.GetValue(); + if (filter == Settings::TextureFilter::NoFilter) { + return false; + } + if (blit.src_level != 0) { + return true; + } + + switch (filter) { + case TextureFilter::Anime4K: + FilterAnime4K(surface, blit); + break; + case TextureFilter::Bicubic: + FilterBicubic(surface, blit); + break; + case TextureFilter::ScaleForce: + FilterScaleForce(surface, blit); + break; + case TextureFilter::xBRZ: + FilterXbrz(surface, blit); + break; + case TextureFilter::MMPX: + FilterMMPX(surface, blit); + break; + default: + LOG_ERROR(Render_Vulkan, "Unknown texture filter {}", filter); + return false; + } + return true; +} + +void BlitHelper::FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(refine_frag, three_textures_pipeline_layout, surface.pixel_format); + FilterPassThreeTextures(surface, pipeline, three_textures_pipeline_layout, blit); +} + +void BlitHelper::FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(bicubic_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(scale_force_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(xbrz_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +void BlitHelper::FilterMMPX(Surface& surface, const VideoCore::TextureBlit& blit) { + auto pipeline = + MakeFilterPipeline(mmpx_frag, single_texture_pipeline_layout, surface.pixel_format); + FilterPass(surface, pipeline, single_texture_pipeline_layout, blit); +} + +vk::Pipeline BlitHelper::MakeFilterPipeline(vk::ShaderModule fragment_shader, + vk::PipelineLayout layout, + VideoCore::PixelFormat color_format) { + + const VkShaderModule c_shader = static_cast(fragment_shader); + const VkPipelineLayout c_layout = static_cast(layout); + const u64 cache_key = Common::HashCombine( + Common::HashCombine(static_cast(reinterpret_cast(c_shader)), + static_cast(reinterpret_cast(c_layout))), + static_cast(color_format)); + + if (const auto it = filter_pipeline_cache.find(cache_key); it != filter_pipeline_cache.end()) { + return it->second; + } + + const std::array stages = MakeStages(full_screen_vert, fragment_shader); + // Use the provided color format for render pass compatibility + const auto renderpass = + renderpass_cache.GetRenderpass(color_format, VideoCore::PixelFormat::Invalid, false); + + vk::GraphicsPipelineCreateInfo pipeline_info = { + .stageCount = static_cast(stages.size()), + .pStages = stages.data(), + .pVertexInputState = &PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pInputAssemblyState = &PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pTessellationState = nullptr, + .pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pDepthStencilState = nullptr, + .pColorBlendState = &PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .layout = layout, + .renderPass = renderpass, + }; + + if (const auto result = device.createGraphicsPipeline({}, pipeline_info); + result.result == vk::Result::eSuccess) { + const vk::Pipeline pipeline = result.value; + filter_pipeline_cache.emplace(cache_key, pipeline); + return pipeline; + } else { + LOG_CRITICAL(Render_Vulkan, "Filter pipeline creation failed!"); + UNREACHABLE(); + } +} + +void BlitHelper::FilterPass(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit) { + const auto texture_descriptor_set = single_texture_provider.Commit(); + update_queue.AddImageSampler(texture_descriptor_set, 0, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + + const auto renderpass = renderpass_cache.GetRenderpass(surface.pixel_format, + VideoCore::PixelFormat::Invalid, false); + + const RenderPass render_pass = { + .framebuffer = surface.Framebuffer(), + .render_pass = renderpass, + .render_area = + { + .offset = {0, 0}, + .extent = {surface.GetScaledWidth(), surface.GetScaledHeight()}, + }, + }; + renderpass_cache.BeginRendering(render_pass); + const float src_scale = static_cast(surface.GetResScale()); + // Calculate normalized texture coordinates like OpenGL does + const auto src_extent = surface.RealExtent(false); // Get unscaled texture extent + const float tex_scale_x = + static_cast(blit.src_rect.GetWidth()) / static_cast(src_extent.width); + const float tex_scale_y = + static_cast(blit.src_rect.GetHeight()) / static_cast(src_extent.height); + const float tex_offset_x = + static_cast(blit.src_rect.left) / static_cast(src_extent.width); + const float tex_offset_y = + static_cast(blit.src_rect.bottom) / static_cast(src_extent.height); + + scheduler.Record([pipeline, layout, texture_descriptor_set, blit, tex_scale_x, tex_scale_y, + tex_offset_x, tex_offset_y, src_scale](vk::CommandBuffer cmdbuf) { + const FilterPushConstants push_constants{.tex_scale = {tex_scale_x, tex_scale_y}, + .tex_offset = {tex_offset_x, tex_offset_y}, + .res_scale = src_scale}; + + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind single texture descriptor set + cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, + texture_descriptor_set, {}); + + cmdbuf.pushConstants(layout, FILTER_PUSH_CONSTANT_RANGE.stageFlags, + FILTER_PUSH_CONSTANT_RANGE.offset, FILTER_PUSH_CONSTANT_RANGE.size, + &push_constants); + + // Set up viewport and scissor for filtering (don't use BindBlitState as it overwrites push + // constants) + const vk::Offset2D offset{ + .x = std::min(blit.dst_rect.left, blit.dst_rect.right), + .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), + }; + const vk::Extent2D extent{ + .width = blit.dst_rect.GetWidth(), + .height = blit.dst_rect.GetHeight(), + }; + const vk::Viewport viewport{ + .x = static_cast(offset.x), + .y = static_cast(offset.y), + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + const vk::Rect2D scissor{ + .offset = offset, + .extent = extent, + }; + cmdbuf.setViewport(0, viewport); + cmdbuf.setScissor(0, scissor); + cmdbuf.draw(3, 1, 0, 0); + }); + scheduler.MakeDirty(StateFlags::Pipeline); +} + +void BlitHelper::FilterPassThreeTextures(Surface& surface, vk::Pipeline pipeline, + vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit) { + const auto texture_descriptor_set = three_textures_provider.Commit(); + + update_queue.AddImageSampler(texture_descriptor_set, 0, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + update_queue.AddImageSampler(texture_descriptor_set, 1, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + update_queue.AddImageSampler(texture_descriptor_set, 2, 0, + surface.ImageView(ViewType::Sample, Type::Base), linear_sampler, + vk::ImageLayout::eGeneral); + + const auto renderpass = renderpass_cache.GetRenderpass(surface.pixel_format, + VideoCore::PixelFormat::Invalid, false); + + const RenderPass render_pass = { + .framebuffer = surface.Framebuffer(), + .render_pass = renderpass, + .render_area = + { + .offset = {0, 0}, + .extent = {surface.GetScaledWidth(), surface.GetScaledHeight()}, + }, + }; + renderpass_cache.BeginRendering(render_pass); + + const float src_scale = static_cast(surface.GetResScale()); + // Calculate normalized texture coordinates like OpenGL does + const auto src_extent = surface.RealExtent(false); // Get unscaled texture extent + const float tex_scale_x = + static_cast(blit.src_rect.GetWidth()) / static_cast(src_extent.width); + const float tex_scale_y = + static_cast(blit.src_rect.GetHeight()) / static_cast(src_extent.height); + const float tex_offset_x = + static_cast(blit.src_rect.left) / static_cast(src_extent.width); + const float tex_offset_y = + static_cast(blit.src_rect.bottom) / static_cast(src_extent.height); + + scheduler.Record([pipeline, layout, texture_descriptor_set, blit, tex_scale_x, tex_scale_y, + tex_offset_x, tex_offset_y, src_scale](vk::CommandBuffer cmdbuf) { + const FilterPushConstants push_constants{.tex_scale = {tex_scale_x, tex_scale_y}, + .tex_offset = {tex_offset_x, tex_offset_y}, + .res_scale = src_scale}; + + cmdbuf.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); + + // Bind single texture descriptor set + cmdbuf.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, layout, 0, + texture_descriptor_set, {}); + + cmdbuf.pushConstants(layout, FILTER_PUSH_CONSTANT_RANGE.stageFlags, + FILTER_PUSH_CONSTANT_RANGE.offset, FILTER_PUSH_CONSTANT_RANGE.size, + &push_constants); + + // Set up viewport and scissor using safe viewport like working filters + const vk::Offset2D offset{ + .x = std::min(blit.dst_rect.left, blit.dst_rect.right), + .y = std::min(blit.dst_rect.bottom, blit.dst_rect.top), + }; + const vk::Extent2D extent{ + .width = blit.dst_rect.GetWidth(), + .height = blit.dst_rect.GetHeight(), + }; + const vk::Viewport viewport{ + .x = static_cast(offset.x), + .y = static_cast(offset.y), + .width = static_cast(extent.width), + .height = static_cast(extent.height), + .minDepth = 0.0f, + .maxDepth = 1.0f, + }; + const vk::Rect2D scissor{ + .offset = offset, + .extent = extent, + }; + cmdbuf.setViewport(0, viewport); + cmdbuf.setScissor(0, scissor); + cmdbuf.draw(3, 1, 0, 0); + }); + scheduler.MakeDirty(StateFlags::Pipeline); +} + } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_blit_helper.h b/src/video_core/renderer_vulkan/vk_blit_helper.h index d9b5c7760..59aee655f 100644 --- a/src/video_core/renderer_vulkan/vk_blit_helper.h +++ b/src/video_core/renderer_vulkan/vk_blit_helper.h @@ -1,9 +1,12 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once +#include + +#include "video_core/rasterizer_cache/pixel_format.h" #include "video_core/renderer_vulkan/vk_resource_pool.h" namespace VideoCore { @@ -27,6 +30,7 @@ public: explicit BlitHelper(const Instance& instance, Scheduler& scheduler, RenderManager& renderpass_cache, DescriptorUpdateQueue& update_queue); ~BlitHelper(); + bool Filter(Surface& surface, const VideoCore::TextureBlit& blit); bool BlitDepthStencil(Surface& source, Surface& dest, const VideoCore::TextureBlit& blit); @@ -38,6 +42,24 @@ public: private: vk::Pipeline MakeComputePipeline(vk::ShaderModule shader, vk::PipelineLayout layout); vk::Pipeline MakeDepthStencilBlitPipeline(); + vk::Pipeline MakeFilterPipeline( + vk::ShaderModule fragment_shader, vk::PipelineLayout layout, + VideoCore::PixelFormat color_format = VideoCore::PixelFormat::RGBA8); + + void FilterAnime4K(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterBicubic(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterScaleForce(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterXbrz(Surface& surface, const VideoCore::TextureBlit& blit); + void FilterMMPX(Surface& surface, const VideoCore::TextureBlit& blit); + + void FilterPass(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); + + void FilterPassThreeTextures(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); + + void FilterPassYGradient(Surface& surface, vk::Pipeline pipeline, vk::PipelineLayout layout, + const VideoCore::TextureBlit& blit); private: const Instance& instance; @@ -51,20 +73,32 @@ private: DescriptorHeap compute_provider; DescriptorHeap compute_buffer_provider; DescriptorHeap two_textures_provider; + DescriptorHeap single_texture_provider; + DescriptorHeap three_textures_provider; vk::PipelineLayout compute_pipeline_layout; vk::PipelineLayout compute_buffer_pipeline_layout; vk::PipelineLayout two_textures_pipeline_layout; + vk::PipelineLayout single_texture_pipeline_layout; + vk::PipelineLayout three_textures_pipeline_layout; vk::ShaderModule full_screen_vert; vk::ShaderModule d24s8_to_rgba8_comp; vk::ShaderModule depth_to_buffer_comp; vk::ShaderModule blit_depth_stencil_frag; + vk::ShaderModule bicubic_frag; + vk::ShaderModule scale_force_frag; + vk::ShaderModule xbrz_frag; + vk::ShaderModule mmpx_frag; + vk::ShaderModule refine_frag; vk::Pipeline d24s8_to_rgba8_pipeline; vk::Pipeline depth_to_buffer_pipeline; vk::Pipeline depth_blit_pipeline; vk::Sampler linear_sampler; vk::Sampler nearest_sampler; + + /// Cache of texture filter pipelines (keyed by shader+layout+format hash) + std::unordered_map filter_pipeline_cache; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 7a27032e9..abab77c4e 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -634,7 +634,7 @@ void RasterizerVulkan::SyncTextureUnits(const Framebuffer* framebuffer) { // If the texture unit is disabled bind a null surface to it if (!texture.enabled) { - const Surface& null_surface = res_cache.GetSurface(VideoCore::NULL_SURFACE_ID); + Surface& null_surface = res_cache.GetSurface(VideoCore::NULL_SURFACE_ID); const Sampler& null_sampler = res_cache.GetSampler(VideoCore::NULL_SAMPLER_ID); update_queue.AddImageSampler(texture_set, texture_index, 0, null_surface.ImageView(), null_sampler.Handle()); @@ -669,7 +669,7 @@ void RasterizerVulkan::SyncTextureUnits(const Framebuffer* framebuffer) { Surface& surface = res_cache.GetTextureSurface(texture); Sampler& sampler = res_cache.GetSampler(texture.config); const vk::ImageView color_view = framebuffer->ImageView(SurfaceType::Color); - const bool is_feedback_loop = color_view == surface.ImageView(); + const bool is_feedback_loop = color_view == surface.FramebufferView(); const vk::ImageView texture_view = is_feedback_loop ? surface.CopyImageView() : surface.ImageView(); update_queue.AddImageSampler(texture_set, texture_index, 0, texture_view, sampler.Handle()); @@ -785,7 +785,7 @@ bool RasterizerVulkan::AccelerateDisplay(const Pica::FramebufferConfig& config, return false; } - const Surface& src_surface = res_cache.GetSurface(src_surface_id); + Surface& src_surface = res_cache.GetSurface(src_surface_id); const u32 scaled_width = src_surface.GetScaledWidth(); const u32 scaled_height = src_surface.GetScaledHeight(); diff --git a/src/video_core/renderer_vulkan/vk_render_manager.cpp b/src/video_core/renderer_vulkan/vk_render_manager.cpp index 7b986e6b2..b3615e1ae 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_render_manager.cpp @@ -42,6 +42,7 @@ void RenderManager::BeginRendering(const Framebuffer* framebuffer, }; images = framebuffer->Images(); aspects = framebuffer->Aspects(); + shadow_rendering = framebuffer->shadow_rendering; BeginRendering(new_pass); } @@ -71,9 +72,11 @@ void RenderManager::EndRendering() { return; } - scheduler.Record([images = images, aspects = aspects](vk::CommandBuffer cmdbuf) { + scheduler.Record([images = images, aspects = aspects, + shadow_rendering = shadow_rendering](vk::CommandBuffer cmdbuf) { u32 num_barriers = 0; vk::PipelineStageFlags pipeline_flags{}; + vk::AccessFlags src_access_flags{}; std::array barriers; for (u32 i = 0; i < images.size(); i++) { if (!images[i]) { @@ -81,14 +84,18 @@ void RenderManager::EndRendering() { } const bool is_color = static_cast(aspects[i] & vk::ImageAspectFlagBits::eColor); if (is_color) { - pipeline_flags |= vk::PipelineStageFlagBits::eColorAttachmentOutput; + pipeline_flags |= shadow_rendering + ? vk::PipelineStageFlagBits::eFragmentShader + : vk::PipelineStageFlagBits::eColorAttachmentOutput; + src_access_flags = shadow_rendering ? vk::AccessFlagBits::eShaderWrite + : vk::AccessFlagBits::eColorAttachmentWrite; } else { pipeline_flags |= vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests; + src_access_flags = vk::AccessFlagBits::eDepthStencilAttachmentWrite; } barriers[num_barriers++] = vk::ImageMemoryBarrier{ - .srcAccessMask = is_color ? vk::AccessFlagBits::eColorAttachmentWrite - : vk::AccessFlagBits::eDepthStencilAttachmentWrite, + .srcAccessMask = src_access_flags, .dstAccessMask = vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eTransferRead, .oldLayout = vk::ImageLayout::eGeneral, @@ -120,6 +127,7 @@ void RenderManager::EndRendering() { pass.render_pass = VK_NULL_HANDLE; images = {}; aspects = {}; + shadow_rendering = false; // The Mali guide recommends flushing at the end of each major renderpass // Testing has shown this has a significant effect on rendering performance @@ -136,7 +144,9 @@ vk::RenderPass RenderManager::GetRenderpass(VideoCore::PixelFormat color, const u32 color_index = color == VideoCore::PixelFormat::Invalid ? NumColorFormats : static_cast(color); const u32 depth_index = - depth == VideoCore::PixelFormat::Invalid ? NumDepthFormats : (static_cast(depth) - 14); + depth == VideoCore::PixelFormat::Invalid + ? NumDepthFormats + : (static_cast(depth - VideoCore::PixelFormat::NumColorFormat)); ASSERT_MSG(color_index <= NumColorFormats && depth_index <= NumDepthFormats, "Invalid color index {} and/or depth_index {}", color_index, depth_index); diff --git a/src/video_core/renderer_vulkan/vk_render_manager.h b/src/video_core/renderer_vulkan/vk_render_manager.h index 9d8a8fdff..3ebbd817b 100644 --- a/src/video_core/renderer_vulkan/vk_render_manager.h +++ b/src/video_core/renderer_vulkan/vk_render_manager.h @@ -1,4 +1,4 @@ -// Copyright 2024 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -35,8 +35,8 @@ struct RenderPass { }; class RenderManager { - static constexpr u32 NumColorFormats = 13; - static constexpr u32 NumDepthFormats = 4; + static constexpr u32 NumColorFormats = static_cast(VideoCore::PixelFormat::NumColorFormat); + static constexpr u32 NumDepthFormats = static_cast(VideoCore::PixelFormat::NumDepthFormat); public: explicit RenderManager(const Instance& instance, Scheduler& scheduler); @@ -67,6 +67,7 @@ private: std::mutex cache_mutex; std::array images; std::array aspects; + bool shadow_rendering{}; RenderPass pass{}; u32 num_draws{}; }; diff --git a/src/video_core/renderer_vulkan/vk_resource_pool.cpp b/src/video_core/renderer_vulkan/vk_resource_pool.cpp index 03b644ea2..7dbe90ebd 100644 --- a/src/video_core/renderer_vulkan/vk_resource_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_resource_pool.cpp @@ -107,13 +107,14 @@ vk::CommandBuffer CommandPool::Commit() { return cmd_buffers[index]; } -constexpr u32 DESCRIPTOR_SET_BATCH = 32; +constexpr u32 DESCRIPTOR_SET_BATCH = 64; +constexpr u32 DESCRIPTOR_MULTIPLIER = 4; // Increase capacity of each pool DescriptorHeap::DescriptorHeap(const Instance& instance, MasterSemaphore* master_semaphore, std::span bindings, u32 descriptor_heap_count_) : ResourcePool{master_semaphore, DESCRIPTOR_SET_BATCH}, device{instance.GetDevice()}, - descriptor_heap_count{descriptor_heap_count_} { + descriptor_heap_count{descriptor_heap_count_ * DESCRIPTOR_MULTIPLIER} { // Increase pool size // Create descriptor set layout. const vk::DescriptorSetLayoutCreateInfo layout_ci = { .bindingCount = static_cast(bindings.size()), diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index cfcb199a1..86f46787e 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -2,8 +2,20 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "video_core/renderer_vulkan/vk_texture_runtime.h" + +#include +#include +#include #include #include +#include +#include "video_core/custom_textures/custom_tex_manager.h" +#include "video_core/rasterizer_cache/pixel_format.h" +#include "video_core/rasterizer_cache/surface_params.h" +#include "video_core/renderer_vulkan/vk_blit_helper.h" +#include "video_core/renderer_vulkan/vk_descriptor_update_queue.h" +#include "video_core/renderer_vulkan/vk_stream_buffer.h" #include "common/literals.h" #include "common/microprofile.h" @@ -20,12 +32,6 @@ #include #include -// Ignore the -Wclass-memaccess warning on memcpy for non-trivially default constructible objects. -#if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wclass-memaccess" -#endif - MICROPROFILE_DEFINE(Vulkan_ImageAlloc, "Vulkan", "Texture Allocation", MP_RGB(192, 52, 235)); namespace Vulkan { @@ -118,18 +124,18 @@ u32 UnpackDepthStencil(const VideoCore::StagingData& data, vk::Format dest) { return depth_offset; } -boost::container::small_vector MakeInitBarriers( - vk::ImageAspectFlags aspect, std::span images) { - boost::container::small_vector barriers; - for (const vk::Image& image : images) { - barriers.push_back(vk::ImageMemoryBarrier{ +void MakeInitBarriers(vk::ImageAspectFlags aspect, u32 num_images, + std::span images, + std::span out_barriers) { + for (u32 i = 0; i < num_images; i++) { + out_barriers[i] = vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eNone, .dstAccessMask = vk::AccessFlagBits::eNone, .oldLayout = vk::ImageLayout::eUndefined, .newLayout = vk::ImageLayout::eGeneral, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, - .image = image, + .image = images[i], .subresourceRange{ .aspectMask = aspect, .baseMipLevel = 0, @@ -137,19 +143,38 @@ boost::container::small_vector MakeInitBarriers( .baseArrayLayer = 0, .layerCount = VK_REMAINING_ARRAY_LAYERS, }, - }); + }; } - return barriers; } -Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, TextureType type, - vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, - vk::ImageAspectFlags aspect, bool need_format_list, - std::string_view debug_name = {}) { - // On tvOS/iOS, fall back to 2D textures when layered rendering isn't supported +vk::ImageSubresourceRange MakeSubresourceRange(vk::ImageAspectFlags aspect, u32 level = 0, + u32 levels = 1, u32 layer = 0) { + return vk::ImageSubresourceRange{ + .aspectMask = aspect, + .baseMipLevel = level, + .levelCount = levels, + .baseArrayLayer = layer, + .layerCount = VK_REMAINING_ARRAY_LAYERS, + }; +} + +constexpr u64 UPLOAD_BUFFER_SIZE = 512_MiB; +constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; + +} // Anonymous namespace + +void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, TextureType type, + vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, + vk::ImageAspectFlags aspect, bool need_format_list, + std::string_view debug_name) { const bool is_cube_map = type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); - const u32 layers = is_cube_map ? 6 : 1; + + this->instance = instance; + this->width = width; + this->height = height; + this->levels = levels; + this->layers = is_cube_map ? 6 : 1; const std::array format_list = { vk::Format::eR8G8B8A8Unorm, @@ -183,7 +208,6 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T VkImage unsafe_image{}; VkImageCreateInfo unsafe_image_info = static_cast(image_info); - VmaAllocation allocation{}; VkResult result = vmaCreateImage(instance->GetAllocator(), &unsafe_image_info, &alloc_info, &unsafe_image, &allocation, nullptr); @@ -192,7 +216,8 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T UNREACHABLE(); } - const vk::Image image{unsafe_image}; + image = vk::Image{unsafe_image}; + const vk::ImageViewCreateInfo view_info = { .image = image, .viewType = is_cube_map ? vk::ImageViewType::eCube : vk::ImageViewType::e2D, @@ -200,55 +225,61 @@ Handle MakeHandle(const Instance* instance, u32 width, u32 height, u32 levels, T .subresourceRange{ .aspectMask = aspect, .baseMipLevel = 0, - .levelCount = levels, + .levelCount = VK_REMAINING_MIP_LEVELS, .baseArrayLayer = 0, - .layerCount = layers, + .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - vk::UniqueImageView image_view = instance->GetDevice().createImageViewUnique(view_info); + image_views[ViewType::Sample] = instance->GetDevice().createImageView(view_info); + if (levels == 1) { + image_views[ViewType::Mip0] = image_views[ViewType::Mip0]; + } if (!debug_name.empty() && instance->HasDebuggingToolAttached()) { SetObjectName(instance->GetDevice(), image, debug_name); - SetObjectName(instance->GetDevice(), image_view.get(), "{} View({})", debug_name, - vk::to_string(aspect)); + SetObjectName(instance->GetDevice(), image_views[ViewType::Sample], "{} View({})", + debug_name, vk::to_string(aspect)); + } +} + +void Handle::Destroy() { + if (!allocation || !instance) { + return; } - return Handle{ - .alloc = allocation, - .image = image, - .image_view = std::move(image_view), - }; + const auto device = instance->GetDevice(); + const auto allocator = instance->GetAllocator(); + + // Image views + if (auto view = image_views[ViewType::Sample]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Mip0]; view && view != image_views[ViewType::Sample]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Storage]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Depth]) { + device.destroyImageView(view); + } + if (auto view = image_views[ViewType::Stencil]) { + device.destroyImageView(view); + } + + image_views = {}; + + if (framebuffer) { + device.destroyFramebuffer(framebuffer); + framebuffer = VK_NULL_HANDLE; + } + + vmaDestroyImage(allocator, image, allocation); + + image = VK_NULL_HANDLE; + allocation = VK_NULL_HANDLE; } -vk::UniqueFramebuffer MakeFramebuffer(vk::Device device, vk::RenderPass render_pass, u32 width, - u32 height, std::span attachments) { - const vk::FramebufferCreateInfo framebuffer_info = { - .renderPass = render_pass, - .attachmentCount = static_cast(attachments.size()), - .pAttachments = attachments.data(), - .width = width, - .height = height, - .layers = 1, - }; - return device.createFramebufferUnique(framebuffer_info); -} - -vk::ImageSubresourceRange MakeSubresourceRange(vk::ImageAspectFlags aspect, u32 level = 0, - u32 levels = 1, u32 layer = 0) { - return vk::ImageSubresourceRange{ - .aspectMask = aspect, - .baseMipLevel = level, - .levelCount = levels, - .baseArrayLayer = layer, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }; -} - -constexpr u64 UPLOAD_BUFFER_SIZE = 512_MiB; -constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; - -} // Anonymous namespace - TextureRuntime::TextureRuntime(const Instance& instance, Scheduler& scheduler, RenderManager& renderpass_cache, DescriptorUpdateQueue& update_queue, u32 num_swapchain_images_) @@ -702,7 +733,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param : SurfaceBase{params}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(pixel_format)} { - if (pixel_format == VideoCore::PixelFormat::Invalid) { + if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; } @@ -712,7 +743,8 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param ASSERT_MSG(format != vk::Format::eUndefined && levels >= 1, "Image allocation parameters are invalid"); - boost::container::static_vector raw_images; + u32 num_images{}; + std::array raw_images; vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { @@ -722,24 +754,35 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param flags |= vk::ImageCreateFlagBits::eMutableFormat; } - const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); - handles[0] = MakeHandle(instance, width, height, levels, texture_type, format, traits.usage, - flags, traits.aspect, need_format_list, DebugName(false)); - raw_images.emplace_back(handles[0].image); - - if (res_scale != 1) { - handles[1] = - MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, format, - traits.usage, flags, traits.aspect, need_format_list, DebugName(true)); - raw_images.emplace_back(handles[1].image); + // Ensure color formats have the color attachment bit set for framebuffers + auto usage = traits.usage; + const bool is_color = + (traits.aspect & vk::ImageAspectFlagBits::eColor) != vk::ImageAspectFlags{}; + if (is_color) { + usage |= vk::ImageUsageFlagBits::eColorAttachment; } + const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); + handles[Type::Base].Create(instance, width, height, levels, texture_type, format, usage, flags, + traits.aspect, need_format_list, DebugName(false)); + raw_images[num_images++] = handles[Type::Base].image; + + if (res_scale != 1) { + handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, + texture_type, format, usage, flags, traits.aspect, + need_format_list, DebugName(true)); + raw_images[num_images++] = handles[Type::Scaled].image; + } + + current = res_scale != 1 ? Type::Scaled : Type::Base; + runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - const auto barriers = MakeInitBarriers(aspect, raw_images); - cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, - vk::PipelineStageFlagBits::eTopOfPipe, - vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, num_images, raw_images, barriers); + cmdbuf.pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, num_images, barriers.data()); }); } @@ -754,7 +797,8 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface const bool has_normal = mat && mat->Map(MapType::Normal); const vk::Format format = traits.native; - boost::container::static_vector raw_images; + u32 num_images{}; + std::array raw_images; vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { @@ -762,48 +806,37 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface } const std::string debug_name = DebugName(false, true); - handles[0] = MakeHandle(instance, mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); - raw_images.emplace_back(handles[0].image); + handles[Type::Base].Create(instance, mat->width, mat->height, levels, texture_type, format, + traits.usage, flags, traits.aspect, false, debug_name); + raw_images[num_images++] = handles[Type::Base].image; if (res_scale != 1) { - handles[1] = MakeHandle(instance, mat->width, mat->height, levels, texture_type, - vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, - false, debug_name); - raw_images.emplace_back(handles[1].image); + handles[Type::Scaled].Create(instance, mat->width, mat->height, levels, texture_type, + vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, + false, debug_name); + raw_images[num_images++] = handles[Type::Scaled].image; } if (has_normal) { - handles[2] = MakeHandle(instance, mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); - raw_images.emplace_back(handles[2].image); + handles[Type::Custom].Create(instance, mat->width, mat->height, levels, texture_type, + format, traits.usage, flags, traits.aspect, false, debug_name); + raw_images[num_images++] = handles[Type::Custom].image; } + current = res_scale != 1 ? Type::Scaled : Type::Base; + runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - const auto barriers = MakeInitBarriers(aspect, raw_images); - cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, - vk::PipelineStageFlagBits::eTopOfPipe, - vk::DependencyFlagBits::eByRegion, {}, {}, barriers); + scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + std::array barriers; + MakeInitBarriers(aspect, num_images, raw_images, barriers); + cmdbuf.pipelineBarrier( + vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, num_images, barriers.data()); }); custom_format = mat->format; material = mat; } -Surface::~Surface() { - if (!handles[0].image_view) { - return; - } - for (const auto& [alloc, image, image_view] : handles) { - if (image) { - vmaDestroyImage(instance->GetAllocator(), image, alloc); - } - } - if (copy_handle.image_view) { - vmaDestroyImage(instance->GetAllocator(), copy_handle.image, copy_handle.alloc); - } -} - void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging) { runtime->renderpass_cache.EndRendering(); @@ -812,7 +845,7 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .aspect = Aspect(), .pipeline_flags = PipelineStageFlags(), .src_access = AccessFlags(), - .src_image = Image(0), + .src_image = Image(Type::Base), }; scheduler->Record([buffer = runtime->upload_buffer.Handle(), format = traits.native, params, @@ -878,14 +911,17 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, runtime->upload_buffer.Commit(staging.size); if (res_scale != 1) { + ASSERT_MSG(handles[Type::Scaled], "Scaled allocation missing during upload"); + const VideoCore::TextureBlit blit = { .src_level = upload.texture_level, .dst_level = upload.texture_level, .src_rect = upload.texture_rect, .dst_rect = upload.texture_rect * res_scale, }; - - BlitScale(blit, true); + if (type != SurfaceType::Texture || !runtime->blit_helper.Filter(*this, blit)) { + BlitScale(blit, true); + } } } @@ -895,13 +931,13 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { const auto color = material->textures[0]; const Common::Rectangle rect{0U, height, width, 0U}; - const auto upload = [&](u32 index, VideoCore::CustomTexture* texture) { + const auto upload = [&](Type type, VideoCore::CustomTexture* texture) { const u32 custom_size = static_cast(texture->data.size()); const RecordParams params = { .aspect = vk::ImageAspectFlagBits::eColor, .pipeline_flags = PipelineStageFlags(), .src_access = AccessFlags(), - .src_image = Image(index), + .src_image = Image(type), }; const auto [data, offset, invalidate] = runtime->upload_buffer.Map(custom_size, 0); @@ -956,14 +992,9 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { }); }; - upload(0, color); - - for (u32 i = 1; i < VideoCore::MAX_MAPS; i++) { - const auto texture = material->textures[i]; - if (!texture) { - continue; - } - upload(i + 1, texture); + upload(Type::Base, color); + if (auto* texture = material->textures[u32(MapType::Normal)]) { + upload(Type::Custom, texture); } } @@ -996,7 +1027,7 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, .aspect = Aspect(), .pipeline_flags = PipelineStageFlags(), .src_access = AccessFlags(), - .src_image = Image(0), + .src_image = Image(Type::Base), }; scheduler->Record( @@ -1070,14 +1101,16 @@ void Surface::ScaleUp(u32 new_scale) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - handles[1] = - MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false, DebugName(true)); + handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, + texture_type, traits.native, traits.usage, flags, traits.aspect, + false, DebugName(true)); + current = Type::Scaled; runtime->renderpass_cache.EndRendering(); scheduler->Record( [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { - const auto barriers = MakeInitBarriers(aspect, raw_images); + std::array barriers; + MakeInitBarriers(aspect, 1, raw_images, barriers); cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, vk::PipelineStageFlagBits::eTopOfPipe, vk::DependencyFlagBits::eByRegion, {}, {}, barriers); @@ -1131,24 +1164,16 @@ vk::PipelineStageFlags Surface::PipelineStageFlags() const noexcept { : vk::PipelineStageFlagBits::eNone); } -vk::Image Surface::Image(u32 index) const noexcept { - const vk::Image image = handles[index].image; - if (!image) { - return handles[0].image; - } - return image; -} - vk::ImageView Surface::CopyImageView() noexcept { + auto& copy_handle = handles[Type::Copy]; vk::ImageLayout copy_layout = vk::ImageLayout::eGeneral; - if (!copy_handle.image) { + if (!copy_handle) { vk::ImageCreateFlags flags{}; if (texture_type == VideoCore::TextureType::CubeMap) { flags |= vk::ImageCreateFlagBits::eCubeCompatible; } - copy_handle = - MakeHandle(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false); + copy_handle.Create(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, + traits.native, traits.usage, flags, traits.aspect, false); copy_layout = vk::ImageLayout::eUndefined; } @@ -1240,123 +1265,81 @@ vk::ImageView Surface::CopyImageView() noexcept { vk::DependencyFlagBits::eByRegion, {}, {}, post_barriers); }); - return copy_handle.image_view.get(); + return copy_handle.image_views[ViewType::Sample]; } -vk::ImageView Surface::ImageView(u32 index) const noexcept { - const auto& image_view = handles[index].image_view.get(); - if (!image_view) { - return handles[0].image_view.get(); +vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { + auto& handle = handles[type == Type::Current ? current : type]; + if (auto image_view = handle.image_views[view_type]) { + return image_view; } - return image_view; -} -vk::ImageView Surface::FramebufferView() noexcept { - is_framebuffer = true; - return ImageView(); -} + auto aspect = traits.aspect; -vk::ImageView Surface::DepthView() noexcept { - if (depth_view) { - return depth_view.get(); + if (view_type == ViewType::Storage) { + ASSERT(pixel_format == PixelFormat::RGBA8); + is_storage = true; + } + if (view_type == ViewType::Depth || view_type == ViewType::Stencil) { + ASSERT(this->type == SurfaceType::DepthStencil); + aspect = view_type == ViewType::Depth ? vk::ImageAspectFlagBits::eDepth + : vk::ImageAspectFlagBits::eStencil; } const vk::ImageViewCreateInfo view_info = { - .image = Image(), + .image = handle.image, .viewType = vk::ImageViewType::e2D, - .format = instance->GetTraits(pixel_format).native, + .format = view_type == ViewType::Storage ? vk::Format::eR32Uint : traits.native, .subresourceRange{ - .aspectMask = vk::ImageAspectFlagBits::eDepth, + .aspectMask = aspect, .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, + .levelCount = (view_type == ViewType::Mip0 || view_type == ViewType::Storage) + ? 1u + : VK_REMAINING_MIP_LEVELS, .baseArrayLayer = 0, .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - - depth_view = instance->GetDevice().createImageViewUnique(view_info); - return depth_view.get(); + handle.image_views[view_type] = instance->GetDevice().createImageView(view_info); + return handle.image_views[view_type]; } -vk::ImageView Surface::StencilView() noexcept { - if (stencil_view) { - return stencil_view.get(); +vk::Framebuffer Surface::Framebuffer(Type type) noexcept { + auto& handle = handles[type == Type::Current ? current : type]; + if (handle.framebuffer) { + return handle.framebuffer; } - const vk::ImageViewCreateInfo view_info = { - .image = Image(), - .viewType = vk::ImageViewType::e2D, - .format = instance->GetTraits(pixel_format).native, - .subresourceRange{ - .aspectMask = vk::ImageAspectFlagBits::eStencil, - .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }, - }; - - stencil_view = instance->GetDevice().createImageViewUnique(view_info); - return stencil_view.get(); -} - -vk::ImageView Surface::StorageView() noexcept { - if (storage_view) { - return storage_view.get(); - } - - if (pixel_format != VideoCore::PixelFormat::RGBA8) { - LOG_WARNING(Render_Vulkan, - "Attempted to retrieve storage view from unsupported surface with format {}", - VideoCore::PixelFormatAsString(pixel_format)); - return ImageView(); - } - - is_storage = true; - - const vk::ImageViewCreateInfo storage_view_info = { - .image = Image(), - .viewType = vk::ImageViewType::e2D, - .format = vk::Format::eR32Uint, - .subresourceRange{ - .aspectMask = vk::ImageAspectFlagBits::eColor, - .baseMipLevel = 0, - .levelCount = VK_REMAINING_MIP_LEVELS, - .baseArrayLayer = 0, - .layerCount = VK_REMAINING_ARRAY_LAYERS, - }, - }; - storage_view = instance->GetDevice().createImageViewUnique(storage_view_info); - return storage_view.get(); -} - -vk::Framebuffer Surface::Framebuffer() noexcept { - const u32 index = res_scale == 1 ? 0u : 1u; - if (framebuffers[index]) { - return framebuffers[index].get(); - } - - const bool is_depth = type == SurfaceType::Depth || type == SurfaceType::DepthStencil; + const bool is_depth = + this->type == SurfaceType::Depth || this->type == SurfaceType::DepthStencil; const auto color_format = is_depth ? PixelFormat::Invalid : pixel_format; const auto depth_format = is_depth ? pixel_format : PixelFormat::Invalid; - const auto render_pass = - runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false); - const auto attachments = std::array{ImageView()}; - framebuffers[index] = MakeFramebuffer(instance->GetDevice(), render_pass, GetScaledWidth(), - GetScaledHeight(), attachments); - return framebuffers[index].get(); + + const auto image_view = ImageView(ViewType::Mip0, type); + const vk::FramebufferCreateInfo framebuffer_info = { + .renderPass = runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false), + .attachmentCount = 1u, + .pAttachments = &image_view, + .width = handle.width, + .height = handle.height, + .layers = handle.layers, + }; + handle.framebuffer = instance->GetDevice().createFramebuffer(framebuffer_info); + return handle.framebuffer; } void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { - const FormatTraits& depth_traits = instance->GetTraits(pixel_format); const bool is_depth_stencil = pixel_format == PixelFormat::D24S8; - if (is_depth_stencil && !depth_traits.blit_support) { + if (is_depth_stencil && !traits.blit_support) { LOG_WARNING(Render_Vulkan, "Depth scale unsupported by hardware"); return; } - scheduler->Record([src_image = Image(!up_scale), aspect = Aspect(), - filter = MakeFilter(pixel_format), dst_image = Image(up_scale), + const auto src_type = up_scale ? Type::Base : Type::Scaled; + const auto dst_type = up_scale ? Type::Scaled : Type::Base; + + scheduler->Record([src_image = Image(src_type), aspect = Aspect(), + filter = MakeFilter(pixel_format), dst_image = Image(dst_type), blit](vk::CommandBuffer render_cmdbuf) { const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), @@ -1461,40 +1444,49 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa width = height = std::numeric_limits::max(); + u32 num_attachments{}; + std::array attachments; + const auto prepare = [&](u32 index, Surface* surface) { - const VideoCore::Extent extent = surface->RealExtent(); + const auto extent = surface->RealExtent(); width = std::min(width, extent.width); height = std::min(height, extent.height); - if (!shadow_rendering) { - formats[index] = surface->pixel_format; - } + formats[index] = surface->pixel_format; images[index] = surface->Image(); aspects[index] = surface->Aspect(); - image_views[index] = shadow_rendering ? surface->StorageView() : surface->FramebufferView(); + image_views[index] = surface->FramebufferView(); }; - boost::container::static_vector attachments; - - if (color) { - prepare(0, color); - attachments.emplace_back(image_views[0]); - } - - if (depth) { - prepare(1, depth); - attachments.emplace_back(image_views[1]); - } - - const vk::Device device = runtime.GetInstance().GetDevice(); if (shadow_rendering) { + const auto extent = color->RealExtent(); + width = extent.width; + height = extent.height; render_pass = renderpass_cache.GetRenderpass(PixelFormat::Invalid, PixelFormat::Invalid, false); - framebuffer = MakeFramebuffer(device, render_pass, color->GetScaledWidth(), - color->GetScaledHeight(), {}); + images[0] = color->Image(); + image_views[0] = color->StorageView(); + aspects[0] = vk::ImageAspectFlagBits::eColor; } else { + if (color) { + prepare(0, color); + attachments[num_attachments++] = image_views[0]; + } + if (depth) { + prepare(1, depth); + attachments[num_attachments++] = image_views[1]; + } render_pass = renderpass_cache.GetRenderpass(formats[0], formats[1], false); - framebuffer = MakeFramebuffer(device, render_pass, width, height, attachments); } + + const vk::FramebufferCreateInfo framebuffer_info = { + .renderPass = render_pass, + .attachmentCount = num_attachments, + .pAttachments = attachments.data(), + .width = width, + .height = height, + .layers = 1, + }; + framebuffer = runtime.GetInstance().GetDevice().createFramebuffer(framebuffer_info); } Framebuffer::~Framebuffer() = default; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index e1745b22b..bb5b5ce91 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -1,10 +1,9 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once -#include #include #include "video_core/rasterizer_cache/framebuffer_base.h" #include "video_core/rasterizer_cache/rasterizer_cache_base.h" @@ -26,10 +25,76 @@ class RenderManager; class Surface; class DescriptorUpdateQueue; +enum Type { + Current = -1, + Base = 0, + Scaled, + Custom, + Copy, + Num, +}; + +enum ViewType { + Sample = 0, + Mip0, + Storage, + Depth, + Stencil, + Max, +}; + struct Handle { - VmaAllocation alloc; - vk::Image image; - vk::UniqueImageView image_view; + explicit Handle() = default; + + ~Handle() { + Destroy(); + } + + Handle(Handle&& other) noexcept + : allocation(std::exchange(other.allocation, VK_NULL_HANDLE)), + image(std::exchange(other.image, VK_NULL_HANDLE)), + image_views(std::exchange(other.image_views, {})), + framebuffer(std::exchange(other.framebuffer, VK_NULL_HANDLE)), + width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), + levels(std::exchange(other.levels, 0)), layers(std::exchange(other.layers, 0)) {} + + Handle& operator=(Handle&& other) noexcept { + if (this == &other) + return *this; + + allocation = std::exchange(other.allocation, VK_NULL_HANDLE); + image = std::exchange(other.image, VK_NULL_HANDLE); + image_views = std::exchange(other.image_views, {}); + framebuffer = std::exchange(other.framebuffer, VK_NULL_HANDLE); + width = std::exchange(other.width, 0); + height = std::exchange(other.height, 0); + levels = std::exchange(other.levels, 0); + layers = std::exchange(other.layers, 0); + + return *this; + } + + void Create(const Instance* instance, u32 width, u32 height, u32 levels, + VideoCore::TextureType type, vk::Format format, vk::ImageUsageFlags usage, + vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, + std::string_view debug_name = {}); + + void Destroy(); + + operator bool() const { + return allocation; + } + + const Instance* instance{nullptr}; + + VmaAllocation allocation{VK_NULL_HANDLE}; + vk::Image image{VK_NULL_HANDLE}; + std::array image_views{}; + vk::Framebuffer framebuffer{VK_NULL_HANDLE}; + u32 width{}; + u32 height{}; + u32 levels{}; + u32 layers{}; }; /** @@ -110,7 +175,6 @@ public: explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceParams& params); explicit Surface(TextureRuntime& runtime, const VideoCore::SurfaceBase& surface, const VideoCore::Material* materal); - ~Surface(); Surface(const Surface&) = delete; Surface& operator=(const Surface&) = delete; @@ -123,28 +187,56 @@ public: } /// Returns the image at index, otherwise the base image - vk::Image Image(u32 index = 1) const noexcept; + vk::Image Image(Type type = Type::Current) const noexcept { + return handles[type == Type::Current ? current : type].image; + } /// Returns the image view at index, otherwise the base view - vk::ImageView ImageView(u32 index = 1) const noexcept; + vk::ImageView ImageView(ViewType view_type = ViewType::Sample, + Type type = Type::Current) noexcept; + + /// Returns a framebuffer handle for rendering to this surface + vk::Framebuffer Framebuffer(Type type = Type::Current) noexcept; + + /// Returns width of the surface + u32 GetWidth() const noexcept { + return width; + } + + /// Returns height of the surface + u32 GetHeight() const noexcept { + return height; + } + + /// Returns resolution scale of the surface + u32 GetResScale() const noexcept { + return res_scale; + } /// Returns a copy of the upscaled image handle, used for feedback loops. vk::ImageView CopyImageView() noexcept; /// Returns the framebuffer view of the surface image - vk::ImageView FramebufferView() noexcept; + vk::ImageView FramebufferView() noexcept { + is_framebuffer = true; + return ImageView(ViewType::Mip0); + } /// Returns the depth view of the surface image - vk::ImageView DepthView() noexcept; + vk::ImageView DepthView() noexcept { + return ImageView(ViewType::Depth); + } /// Returns the stencil view of the surface image - vk::ImageView StencilView() noexcept; + vk::ImageView StencilView() noexcept { + return ImageView(ViewType::Stencil); + } - /// Returns the R32 image view used for atomic load/store - vk::ImageView StorageView() noexcept; - - /// Returns a framebuffer handle for rendering to this surface - vk::Framebuffer Framebuffer() noexcept; + /// Returns the R32 image view used for atomic load/store. + vk::ImageView StorageView() noexcept { + is_storage = true; + return ImageView(ViewType::Storage); + } /// Uploads pixel data in staging to a rectangle region of the surface texture void Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging); @@ -181,9 +273,8 @@ public: const Instance* instance; Scheduler* scheduler; FormatTraits traits; - std::array handles{}; - std::array framebuffers{}; - Handle copy_handle; + std::array handles; + Type current{}; vk::UniqueImageView depth_view; vk::UniqueImageView stencil_view; vk::UniqueImageView storage_view; @@ -212,7 +303,7 @@ public: } [[nodiscard]] vk::Framebuffer Handle() const noexcept { - return framebuffer.get(); + return framebuffer; } [[nodiscard]] std::array Images() const noexcept { @@ -231,24 +322,17 @@ public: return res_scale; } - u32 Width() const noexcept { - return width; - } - - u32 Height() const noexcept { - return height; - } - private: std::array images{}; std::array image_views{}; - vk::UniqueFramebuffer framebuffer; + vk::Framebuffer framebuffer; vk::RenderPass render_pass; + std::vector framebuffer_views; std::array aspects{}; std::array formats{VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}; - u32 width{}; - u32 height{}; + u32 width; + u32 height; u32 res_scale{1}; }; From c55165e19b8f015bf754bfd32b71b2794cfe9436 Mon Sep 17 00:00:00 2001 From: Chase Harkcom Date: Fri, 20 Feb 2026 15:40:39 -0700 Subject: [PATCH 15/94] Fix segfault when resetting default settings (#1751) --- src/citra_qt/configuration/configure_general.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp index ab8eb2a3c..ca548b21b 100644 --- a/src/citra_qt/configuration/configure_general.cpp +++ b/src/citra_qt/configuration/configure_general.cpp @@ -163,7 +163,7 @@ void ConfigureGeneral::ResetDefaults() { FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "qt-config.ini"); FileUtil::DeleteDirRecursively(FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "custom"); - std::exit(0); + qApp->quit(); } void ConfigureGeneral::ApplyConfiguration() { From ac0ec5edea2ff2d8c5343308ed0fccfec8944876 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Sat, 21 Feb 2026 17:17:12 +0000 Subject: [PATCH 16/94] Updated translations via Transifex --- dist/languages/fr.ts | 18 +++++++++--------- dist/languages/it.ts | 4 ++-- dist/languages/pt_BR.ts | 4 ++-- .../src/main/res/values-b+pt+BR/strings.xml | 6 ++++++ .../app/src/main/res/values-fr/strings.xml | 2 ++ .../app/src/main/res/values-it/strings.xml | 4 ++++ 6 files changed, 25 insertions(+), 13 deletions(-) diff --git a/dist/languages/fr.ts b/dist/languages/fr.ts index d8a6179f8..e35d9e806 100644 --- a/dist/languages/fr.ts +++ b/dist/languages/fr.ts @@ -329,7 +329,7 @@ Cela bannira à la fois son nom du forum et son adresse IP. <html><head/><body><p>This post-processing effect adjusts audio speed to match emulation speed and helps prevent audio stutter. This however increases audio latency.</p></body></html> - + <html><head/><body><p>Cet effet de post-traitement ajuste la vitesse audio pour correspondre à la vitesse d'émulation et aide à prévenir les distorsions. Cela augmente cependant la latence du son.</p></body></html> @@ -339,7 +339,7 @@ Cela bannira à la fois son nom du forum et son adresse IP. <html><head/><body><p>Scales audio playback speed to account for drops in emulation framerate. This means that audio will play at full speed even while the application framerate is low. May cause audio desync issues.</p></body></html> - + <html><head/><body><p>Adapte la vitesse de lecture de l'audio pour tenir compte des baisses de fréquence d'images de l'émulation. Cela signifie que l'audio sera lu à pleine vitesse même si la fréquence d'images de l'application est faible. Peut entraîner des problèmes de désynchronisation de l'audio.</p></body></html> @@ -479,7 +479,7 @@ Cela bannira à la fois son nom du forum et son adresse IP. <html><head/><body><p>Select where the image of the emulated camera comes from. It may be an image or a real camera.</p></body></html> - + <html><head/><body><p>Choisissez la provenance de l'image de la caméra émulée. Elle peut être une image ou une vraie caméra.</p></body></html> @@ -1495,7 +1495,7 @@ Souhaitez vous ignorer l'erreur et poursuivre ? <html><head/><body><p>VSync prevents the screen from tearing, but some graphics cards have lower performance with VSync enabled. Keep it enabled if you don't notice a performance difference.</p></body></html> - + <html><head/><body><p>VSync empêche les effets de déchirement de l'image, mais elle réduira la performance de certaines cartes graphiques. Laissez-la activée si vous ne constatez pas de différence.</p></body></html> @@ -1505,12 +1505,12 @@ Souhaitez vous ignorer l'erreur et poursuivre ? <html><head/><body><p>When enabled, this setting detects when the refresh rate of the screen is below that of the 3DS, and when it is, disables VSync automatically to avoid emulation speed being forced below 100%.</p></body></html> - + <html><head/><body><p>Si activée, cette option détecte lorsque le taux de rafraichissement de l'écran est inférieur à celui de la 3DS, et si c'est le cas, elle désactive automatiquement la VSync pour éviter que la vitesse d'émulation soit contrainte d'être en dessous de 100%.</p></body></html> Enable display refresh rate detection - + Activer la détection du taux de rafraichissement de l'écran @@ -2532,7 +2532,7 @@ Souhaitez vous ignorer l'erreur et poursuivre ? <html><head/><body><p>Compresses the content of CIA files when installed to the emulated SD card. Only affects CIA content which is installed while the setting is enabled.</p></body></html> - + <html><head/><body><p>Compresse le contenu des fichiers CIA lorsqu'ils sont installés sur la carte SD émulée. N'affecte que le contenu CIA installé lorsque le paramètre est activé.</p></body></html> @@ -5196,7 +5196,7 @@ Souhaitez-vous la télécharger ? Delete Vulkan Shader Cache - + Supprimer le cache de shader Vulkan @@ -6082,7 +6082,7 @@ Message de débogage : Loading %3 %1 / %2 - + Chargement %3 %1 / %2 diff --git a/dist/languages/it.ts b/dist/languages/it.ts index 0df8a2c12..f42aabf76 100644 --- a/dist/languages/it.ts +++ b/dist/languages/it.ts @@ -5194,7 +5194,7 @@ Vuoi installarlo? Delete Vulkan Shader Cache - + Elimina la cache degli shader Vulkan @@ -6080,7 +6080,7 @@ Messaggio di debug: Loading %3 %1 / %2 - + Caricamento %3 %1 / %2 diff --git a/dist/languages/pt_BR.ts b/dist/languages/pt_BR.ts index 8c78ab5d9..ff289fb02 100644 --- a/dist/languages/pt_BR.ts +++ b/dist/languages/pt_BR.ts @@ -5195,7 +5195,7 @@ Você gostaria de baixá-la? Delete Vulkan Shader Cache - + Excluir Cache de Shaders Vulkan @@ -6081,7 +6081,7 @@ Mensagem de depuração: Loading %3 %1 / %2 - + Carregando %3 %1 / %2 diff --git a/src/android/app/src/main/res/values-b+pt+BR/strings.xml b/src/android/app/src/main/res/values-b+pt+BR/strings.xml index f963b9ad0..1f5f874af 100644 --- a/src/android/app/src/main/res/values-b+pt+BR/strings.xml +++ b/src/android/app/src/main/res/values-b+pt+BR/strings.xml @@ -118,6 +118,8 @@ Alguns controles podem não ser capazes de mapear os D-pads para um eixo. Se esse for o caso, use a seção de D-Pad (Botões). D-Pad (Botão) Só mapeie o D-pad para isso se você se você estiver encontrando problemas com o mapeamento de botão do D-Pad (Eixo). + Eixo Vertical + Eixo Horizontal Cima Baixo Esquerda @@ -126,6 +128,8 @@ Pressione ou mova uma entrada. Mapeamento de controles Pressione ou mova um botão/alavanca para mapear para %1$s. + Pressione para CIMA no seu joystick. + Pressione para a DIREITA no seu joystick. Menu Principal Trocar telas Turbo @@ -546,6 +550,8 @@ Preparando Shaders + Construindo %s + Jogar Desinstalar Aplicativo diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml index 95a18358a..154add85d 100644 --- a/src/android/app/src/main/res/values-fr/strings.xml +++ b/src/android/app/src/main/res/values-fr/strings.xml @@ -550,6 +550,8 @@ Préparation des shaders + Construction %s + Jouer Désinstaller l\'application diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml index 7c3f37af6..84b7b803a 100644 --- a/src/android/app/src/main/res/values-it/strings.xml +++ b/src/android/app/src/main/res/values-it/strings.xml @@ -133,6 +133,8 @@ Divertiti usando l\'emulatore! Premi o sposta un comando Assegnazione Input Premi o muovi un comando per assegnarlo a %1$s. + Premi UP sul tuo controller. + Premi RIGHT sul tuo controller. Home Inverti schermi Turbo @@ -553,6 +555,8 @@ Divertiti usando l\'emulatore! Preparazione degli shader + Compilazione %s + Riproduci Disinstalla applicazione From 43cecd1692f09aa25d527987f07aabda68ee63d4 Mon Sep 17 00:00:00 2001 From: lannoene <77375172+lannoene@users.noreply.github.com> Date: Sun, 22 Feb 2026 08:07:24 -0800 Subject: [PATCH 17/94] Update File Core and Add HLE DLP Client (#1741) --- src/core/CMakeLists.txt | 5 + src/core/file_sys/ncch_container.cpp | 10 +- src/core/hle/service/am/am.cpp | 5 + src/core/hle/service/am/am.h | 3 + src/core/hle/service/apt/apt.cpp | 4 +- src/core/hle/service/dlp/dlp.cpp | 2 +- src/core/hle/service/dlp/dlp.h | 2 +- src/core/hle/service/dlp/dlp_base.cpp | 251 +++++++ src/core/hle/service/dlp/dlp_base.h | 387 ++++++++++ src/core/hle/service/dlp/dlp_clnt.cpp | 165 ++++- src/core/hle/service/dlp/dlp_clnt.h | 27 +- src/core/hle/service/dlp/dlp_clt_base.cpp | 856 ++++++++++++++++++++++ src/core/hle/service/dlp/dlp_clt_base.h | 144 ++++ src/core/hle/service/dlp/dlp_crypto.cpp | 31 + src/core/hle/service/dlp/dlp_fkcl.cpp | 84 ++- src/core/hle/service/dlp/dlp_fkcl.h | 15 +- src/core/hle/service/dlp/dlp_srvr.cpp | 37 +- src/core/hle/service/dlp/dlp_srvr.h | 10 +- src/core/hle/service/http/http_c.cpp | 3 +- src/core/hle/service/nwm/nwm_uds.cpp | 596 +++++++++------ src/core/hle/service/nwm/nwm_uds.h | 55 +- src/core/hle/service/nwm/uds_data.cpp | 2 +- src/core/hle/service/nwm/uds_data.h | 2 +- src/core/hw/aes/key.cpp | 14 +- src/core/hw/aes/key.h | 4 +- src/core/hw/default_keys.h | 383 +++++----- src/core/loader/ncch.cpp | 17 +- 27 files changed, 2617 insertions(+), 497 deletions(-) create mode 100644 src/core/hle/service/dlp/dlp_base.cpp create mode 100644 src/core/hle/service/dlp/dlp_base.h create mode 100644 src/core/hle/service/dlp/dlp_clt_base.cpp create mode 100644 src/core/hle/service/dlp/dlp_clt_base.h create mode 100644 src/core/hle/service/dlp/dlp_crypto.cpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f069ff33d..a5abbc585 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -293,6 +293,11 @@ add_library(citra_core STATIC hle/service/dlp/dlp_fkcl.h hle/service/dlp/dlp_srvr.cpp hle/service/dlp/dlp_srvr.h + hle/service/dlp/dlp_clt_base.cpp + hle/service/dlp/dlp_clt_base.h + hle/service/dlp/dlp_base.cpp + hle/service/dlp/dlp_base.h + hle/service/dlp/dlp_crypto.cpp hle/service/dsp/dsp_dsp.cpp hle/service/dsp/dsp_dsp.h hle/service/err/err_f.cpp diff --git a/src/core/file_sys/ncch_container.cpp b/src/core/file_sys/ncch_container.cpp index 1438a766a..382c8943e 100644 --- a/src/core/file_sys/ncch_container.cpp +++ b/src/core/file_sys/ncch_container.cpp @@ -138,7 +138,7 @@ Loader::ResultStatus NCCHContainer::LoadHeader() { return Loader::ResultStatus::Success; } - if (!file->IsOpen()) { + if (!file || !file->IsOpen()) { return Loader::ResultStatus::Error; } @@ -205,6 +205,9 @@ Loader::ResultStatus NCCHContainer::Load() { if (is_loaded) return Loader::ResultStatus::Success; + if (!file) + return Loader::ResultStatus::Error; + int block_size = kBlockSize; if (file->IsOpen()) { @@ -621,7 +624,7 @@ Loader::ResultStatus NCCHContainer::ReadRomFS(std::shared_ptr& romf return Loader::ResultStatus::ErrorNotUsed; } - if (!file->IsOpen()) + if (!file || !file->IsOpen()) return Loader::ResultStatus::Error; u32 romfs_offset = ncch_offset + (ncch_header.romfs_offset * block_size) + 0x1000; @@ -767,6 +770,9 @@ bool NCCHContainer::HasExHeader() { std::unique_ptr NCCHContainer::Reopen( const std::unique_ptr& orig_file, const std::string& new_filename) { + if (!orig_file) + return nullptr; + const bool is_compressed = orig_file->IsCompressed(); const bool is_crypto = orig_file->IsCrypto(); const std::string filename = new_filename.empty() ? orig_file->Filename() : new_filename; diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 645a015b6..af2739590 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -417,6 +417,10 @@ void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ct } } +void CIAFile::AuthorizeDecryptionFromHLE() { + decryption_authorized = true; +} + CIAFile::CIAFile(Core::System& system_, Service::FS::MediaType media_type, bool from_cdn_) : system(system_), from_cdn(from_cdn_), decryption_authorized(false), media_type(media_type), decryption_state(std::make_unique()) { @@ -873,6 +877,7 @@ bool CIAFile::Close() { // Only delete the content folder as there may be user save data in the title folder. const std::string title_content_path = GetTitlePath(media_type, container.GetTitleMetadata().GetTitleID()) + "content/"; + current_content_file.reset(); FileUtil::DeleteDirRecursively(title_content_path); } return true; diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index 4dec69e80..96258ecf9 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -233,8 +233,11 @@ public: return install_results; } + void AuthorizeDecryptionFromHLE(); + private: friend void AuthorizeCIAFileDecryption(CIAFile* cia_file, Kernel::HLERequestContext& ctx); + Core::System& system; // Sections (tik, tmd, contents) are being imported individually diff --git a/src/core/hle/service/apt/apt.cpp b/src/core/hle/service/apt/apt.cpp index fcc9b2a89..7b2dfeb19 100644 --- a/src/core/hle/service/apt/apt.cpp +++ b/src/core/hle/service/apt/apt.cpp @@ -39,6 +39,8 @@ SERVICE_CONSTRUCT_IMPL(Service::APT::Module) namespace Service::APT { +constexpr u32 max_wireless_reboot_info_size = 0x10; + template void Module::serialize(Archive& ar, const unsigned int file_version) { DEBUG_SERIALIZATION_POINT; @@ -64,7 +66,7 @@ std::shared_ptr Module::NSInterface::GetModule() const { void Module::NSInterface::SetWirelessRebootInfo(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - const auto size = rp.Pop(); + const auto size = std::min(rp.Pop(), max_wireless_reboot_info_size); const auto buffer = rp.PopStaticBuffer(); apt->wireless_reboot_info = std::move(buffer); diff --git a/src/core/hle/service/dlp/dlp.cpp b/src/core/hle/service/dlp/dlp.cpp index edbd22321..c0565d330 100644 --- a/src/core/hle/service/dlp/dlp.cpp +++ b/src/core/hle/service/dlp/dlp.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/core/hle/service/dlp/dlp.h b/src/core/hle/service/dlp/dlp.h index 28fb73718..ea34e83e2 100644 --- a/src/core/hle/service/dlp/dlp.h +++ b/src/core/hle/service/dlp/dlp.h @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. diff --git a/src/core/hle/service/dlp/dlp_base.cpp b/src/core/hle/service/dlp/dlp_base.cpp new file mode 100644 index 000000000..a2e22212f --- /dev/null +++ b/src/core/hle/service/dlp/dlp_base.cpp @@ -0,0 +1,251 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "dlp_base.h" + +#include +#include +#include "common/alignment.h" +#include "common/swap.h" +#include "common/timer.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/nwm/uds_data.h" +#include "core/hle/service/service.h" +#include "core/hw/aes/key.h" +#include "core/hw/unique_data.h" + +namespace Service::DLP { + +DLP_Base::DLP_Base(Core::System& s) : system(s) {} + +std::shared_ptr DLP_Base::GetCFG() { + return Service::CFG::GetModule(system); +} + +std::shared_ptr DLP_Base::GetUDS() { + return system.ServiceManager().GetService("nwm::UDS"); +} + +std::u16string DLP_Base::DLPUsernameAsString16(DLP_Username uname) { + std::u16string strUsername; + for (auto c : uname) { + strUsername.push_back(c); + } + return strUsername; +} + +DLP_Username DLP_Base::String16AsDLPUsername(std::u16string str) { + DLP_Username out{}; + u32 num_chars_copy = std::min(out.size(), str.size()); + memcpy(out.data(), str.data(), num_chars_copy * sizeof(u16_le)); + return out; +} + +DLPNodeInfo DLP_Base::UDSToDLPNodeInfo(NWM::NodeInfo node_info) { + DLPNodeInfo out{}; + out.username = node_info.username; + out.network_node_id = node_info.network_node_id; + out.friend_code_seed = node_info.friend_code_seed; + return out; +} + +void DLP_Base::GetEventDescription(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + DLPEventDescription desc{}; + + IPC::RequestBuilder rb = rp.MakeBuilder(8, 0); + + rb.Push(ResultSuccess); + rb.PushRaw(desc); +} + +void DLP_Base::InitializeDlpBase(u32 shared_mem_size, + std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username uname) { + dlp_sharedmem_size = shared_mem_size; + dlp_sharedmem = shared_mem; + dlp_status_event = event; + username = uname; + + uds_sharedmem = + system.Kernel() + .CreateSharedMemory(nullptr, uds_sharedmem_size, Kernel::MemoryPermission::ReadWrite, + Kernel::MemoryPermission::ReadWrite, 0, Kernel::MemoryRegion::BASE, + "NWM::UDS:SharedMemory") + .Unwrap(); + + NWM::NodeInfo cnode_info{ + .friend_code_seed = HW::UniqueData::GetLocalFriendCodeSeedB().body.friend_code_seed, + .username = uname, + }; + GetUDS()->Initialize(uds_sharedmem_size, cnode_info, uds_version, uds_sharedmem); +} + +void DLP_Base::FinalizeDlpBase() { + GetUDS()->ShutdownHLE(); + dlp_sharedmem.reset(); + uds_sharedmem.reset(); + dlp_status_event.reset(); + username = DLP_Username{}; +} + +bool DLP_Base::ConnectToNetworkAsync(NWM::NetworkInfo net_info, NWM::ConnectionType conn_type, + std::vector passphrase) { + auto uds = GetUDS(); + + // we need to make this event manually + uds->connection_event = + system.Kernel().CreateEvent(Kernel::ResetType::OneShot, "dlp_connect_to_beacon"); + + uds->ConnectToNetworkHLE(net_info, static_cast(conn_type), passphrase); + + // wait for connection + Common::Timer t_time_out; + t_time_out.Start(); + bool timed_out = false; + while (true) { // busy wait, TODO: change to not busy wait? + if (uds->GetConnectionStatusHLE().status == NWM::NetworkStatus::ConnectedAsSpectator || + uds->GetConnectionStatusHLE().status == NWM::NetworkStatus::ConnectedAsClient) { + // connected + break; + } + constexpr u32 connect_network_timeout_ms = 3000; + if (t_time_out.GetTimeElapsed().count() > connect_network_timeout_ms) { + timed_out = true; + break; + } + } + + if (timed_out) { + // TODO: fix unlikely race cond, timeout happens, we disconnect, then server registers our + // connection + uds->DisconnectNetworkHLE(); + LOG_ERROR(Service_DLP, "Timed out when trying to connect to beacon"); + return false; + } + + if (uds->GetConnectionStatusHLE().status != NWM::NetworkStatus::ConnectedAsSpectator && + uds->GetConnectionStatusHLE().status != NWM::NetworkStatus::ConnectedAsClient) { + // error! + LOG_ERROR(Service_DLP, "Could not connect to network, connected as 0x{:x}", + static_cast(uds->GetConnectionStatusHLE().status)); + return false; + } + + return true; +} + +int DLP_Base::RecvFrom(u16 node_id, std::vector& buffer) { + constexpr u32 max_pullpacket_size = 0x3c00; + std::vector buffer_out; + + NWM::SecureDataHeader secure_data; + auto uds = GetUDS(); + if (!uds) { + LOG_ERROR(Service_DLP, "Could not get get pointer to UDS service!"); + return 0; + } + auto ret = + uds->PullPacketHLE(node_id, max_pullpacket_size, static_cast(max_pullpacket_size) >> 2, + buffer_out, &secure_data); + + if (!ret) { + return 0; + } + + buffer = buffer_out; + return *ret; // size +} + +bool DLP_Base::SendTo(u16 node_id, u8 data_channel, std::vector& buffer, u8 flags) { + constexpr u32 max_sendto_size = 0x3c00; + + if (buffer.size() > max_sendto_size) { + LOG_WARNING(Service_DLP, "Packet size is larger than 0x{:x}", max_sendto_size); + } + + return GetUDS()->SendToHLE(node_id, data_channel, buffer.size(), flags, buffer) == + NWM::ResultStatus::ResultSuccess; +} + +u32 DLP_Base::GeneratePKChecksum(u32 aes_value, void* _input_buffer, u32 packet_size) { + auto input_buffer = reinterpret_cast(_input_buffer); + + u32 working_hash = 0; + // add all word aligned bytes + for (u32 i = 0; i < packet_size / sizeof(u32); i++) { + u32 inp_buf_word = reinterpret_cast(input_buffer)[i]; + working_hash += Common::swap32(inp_buf_word); + } + // add any remaining non word-aligned bytes + if (u32 num_bytes_non_aligned = packet_size & 3; num_bytes_non_aligned != 0) { + u32 non_aligned = 0; + memcpy(&non_aligned, input_buffer + packet_size - num_bytes_non_aligned, + num_bytes_non_aligned); + working_hash += Common::swap32(non_aligned); + } + // hash by the aes value + u8 num_extra_hash = (reinterpret_cast(&aes_value)[3] & 0b0111) + 2; + u8 num_shift_extra_hash = (reinterpret_cast(&aes_value)[2] & 0b1111) + 4; + u32 aes_swap = Common::swap32(aes_value); + for (u8 i = 0; i < num_extra_hash; i++) { + working_hash = + (working_hash >> num_shift_extra_hash | working_hash << num_shift_extra_hash) ^ + aes_swap; + } + return Common::swap32(working_hash); +} + +u32 DLP_Base::GenDLPChecksumKey(Network::MacAddress mac_addr) { + auto dlp_iv_ctr_buf = HW::AES::GetDlpChecksumModIv(); + + std::array ctr_encrypt_buf{}; + for (u32 i = 0; i < 0x10; i++) { + ctr_encrypt_buf[i] = mac_addr[i % 6] ^ dlp_iv_ctr_buf[i]; + } + + u32 val_out = 0; + DLPEncryptCTR(&val_out, sizeof(val_out), ctr_encrypt_buf.data()); + return val_out; +} + +bool DLP_Base::ValidatePacket(u32 aes, void* pk, size_t sz, bool checksum) { + if (sz < sizeof(DLPPacketHeader)) { + LOG_ERROR(Service_DLP, "Packet size is too small"); + return false; + } + + auto ph = reinterpret_cast(pk); + + if (ph->size != sz) { + LOG_ERROR(Service_DLP, "Packet size in header does not match size received"); + return false; + } + + if (checksum) { + std::vector pk_copy; + pk_copy.resize(sz); + memcpy(pk_copy.data(), pk, sz); + + auto ph_cpy = reinterpret_cast(pk_copy.data()); + ph_cpy->checksum = 0; + u32 new_checksum = GeneratePKChecksum(aes, pk_copy.data(), pk_copy.size()); + if (new_checksum != ph->checksum) { + LOG_ERROR(Service_DLP, "Could not verify packet checksum 0x{:x} != 0x{:x}", + new_checksum, ph->checksum); + return false; + } + } + return true; +} + +u32 DLP_Base::GetNumFragmentsFromTitleSize(u32 tsize) { + return Common::AlignUp(tsize - broad_title_size_diff, content_fragment_size) / + content_fragment_size; +} + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_base.h b/src/core/hle/service/dlp/dlp_base.h new file mode 100644 index 000000000..6708937f9 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_base.h @@ -0,0 +1,387 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/core.h" +#include "core/hle/service/cfg/cfg.h" +#include "core/hle/service/nwm/nwm_uds.h" +#include "core/hle/service/service.h" + +#include + +// DLP save states are not supported + +namespace Service::DLP { + +using DLP_Username = std::array; +constexpr inline u64 DLP_CHILD_TID_HIGH = 0x0004000100000000; +constexpr inline u32 content_fragment_size = 1440; + +struct DLPTitleInfo { + u32 unique_id; // games look at this to make sure it's their title info + u32 variation; + Network::MacAddress mac_addr; + u16 version; // XX: probably? + std::array age_ratings; + std::array short_description; // UTF-16 + std::array long_description; // UTF-16 + std::array icon; // 48x48, RGB565 + u32 size; + u8 unk2; + u8 unk3; + u16 padding; + std::vector ToBuffer() { + std::vector out; + out.resize(sizeof(DLPTitleInfo)); + memcpy(out.data(), this, sizeof(DLPTitleInfo)); + return out; + } +}; + +static_assert(sizeof(DLPTitleInfo) == 5032, "DLPTitleInfo is the wrong size"); + +struct DLPNodeInfo { + u64 friend_code_seed; + std::array pad; + DLP_Username username; + u32 unk1; + u32 network_node_id; +}; + +static_assert(sizeof(DLPNodeInfo) == 0x28); + +struct DLPEventDescription { + std::array unk; +}; + +static_assert(sizeof(DLPEventDescription) == 0x18); + +// START BIG ENDIAN + +constexpr inline u8 dl_pk_type_broadcast = 0x01; +constexpr inline u8 dl_pk_type_auth = 0x02; +constexpr inline u8 dl_pk_type_start_dist = 0x03; +constexpr inline u8 dl_pk_type_distribute = 0x04; +constexpr inline u8 dl_pk_type_finish_dist = 0x05; +constexpr inline u8 dl_pk_type_start_game = 0x06; + +constexpr inline std::array dl_pk_head_broadcast_header = {dl_pk_type_broadcast, 0x02}; +constexpr inline std::array dl_pk_head_auth_header = {dl_pk_type_auth, 0x02}; +constexpr inline std::array dl_pk_head_start_dist_header = {dl_pk_type_start_dist, 0x02}; +constexpr inline std::array dl_pk_head_distribute_header = {dl_pk_type_distribute, 0x02}; +constexpr inline std::array dl_pk_head_finish_dist_header = {dl_pk_type_finish_dist, 0x02}; +constexpr inline std::array dl_pk_head_start_game_header = {dl_pk_type_start_game, 0x02}; + +struct DLPPacketHeader { + union { + std::array magic; + struct { + u8 type; + u8 mag0x02; + u16 unk; // usually 0x00 0x00 + }; + }; + u16_be size; // size of the whole packet, including the header + std::array unk1; // always 0x02 0x00 + u32 checksum; // always calculate + u8 packet_index; // starts at 0 + std::array resp_id; // copies this from host packet when responding to it +}; + +static_assert(sizeof(DLPPacketHeader) == 0x10); + +// bool with 3ds padding included +struct DLPPacketBool { + union { + u32 raw; + struct { + u8 active_value : 1; + u8 padding : 7; + std::array padding2; + }; + }; + operator bool() { + return active_value; + } + DLPPacketBool& operator=(const bool& o) { + raw = 0x0; + active_value = o; + return *this; + } +}; + +static_assert(sizeof(DLPPacketBool) == sizeof(u32_be)); + +constexpr u32 broad_title_size_diff = 111360; + +#pragma pack(push, 2) +struct DLPBroadcastPacket1 { + DLPPacketHeader head; + u64_be child_title_id; // title id of the child being broadcasted + u64 unk1; + u64 unk2; + u64 unk3; + u64 unk4; // all 0s + u32_be size; // size minus broad_title_size_diff + u32 unk5; + std::array title_short; + std::array title_long; + std::array icon_part; + u64 unk; +}; +#pragma pack(pop) + +static_assert(sizeof(DLPBroadcastPacket1) == 768); + +struct DLPBroadcastPacket2 { + DLPPacketHeader head; + std::array icon_part; +}; + +static_assert(sizeof(DLPBroadcastPacket2) == 1448); + +struct DLPBroadcastPacket3 { + DLPPacketHeader head; + std::array icon_part; +}; + +static_assert(sizeof(DLPBroadcastPacket3) == 1448); + +struct DLPBroadcastPacket4 { + DLPPacketHeader head; + std::array icon_part; +}; + +static_assert(sizeof(DLPBroadcastPacket4) == 1448); + +struct DLPBroadcastPacket5 { + DLPPacketHeader head; + std::array unk1; + std::array unk2; + std::array unk3; +}; + +static_assert(sizeof(DLPBroadcastPacket5) == 1464); + +// auth session +struct DLPSrvr_Auth { + DLPPacketHeader head; + u32 unk1; // 0x0 +}; + +static_assert(sizeof(DLPSrvr_Auth) == 0x14); + +struct DLPClt_AuthAck { + DLPPacketHeader head; + DLPPacketBool initialized; // true + std::array padding; + std::array resp_id; // very important! game specific? +}; + +static_assert(sizeof(DLPClt_AuthAck) == 0x18); + +// start distribution +struct DLPSrvr_StartDistribution { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 +}; + +static_assert(sizeof(DLPSrvr_StartDistribution) == 0x14); + +struct DLPClt_StartDistributionAck_NoContentNeeded { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u32 unk2; // 0x0 +}; + +static_assert(sizeof(DLPClt_StartDistributionAck_NoContentNeeded) == 0x18); + +struct DLPClt_StartDistributionAck_ContentNeeded { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u16_be unk2; // BE 0x20 unk important! + u16_be unk3; // 0x0 + DLPPacketBool unk4; // 0x1 + u32_be unk5; // 0x0 + std::array unk_body; +}; + +static_assert(sizeof(DLPClt_StartDistributionAck_ContentNeeded) == 0x38); + +// perform distribution of content +// packet_index is 1 +struct DLPSrvr_ContentDistributionFragment { + DLPPacketHeader head; + u32_be content_magic; // extra magic value + u32_be unk1; // 0x1 BE + u16_be frag_index; // BE % dlp_content_block_length + u16_be frag_size; // BE + u8 content_fragment[]; +}; + +static_assert(sizeof(DLPSrvr_ContentDistributionFragment) == 28); + +// finish receiving content +struct DLPSrvr_FinishContentUpload { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u32_be seq_num; // BE starts at 0x0 and copies whatever number the ack gives it +}; + +static_assert(sizeof(DLPSrvr_FinishContentUpload) == 0x18); + +// it sends this to clients during distribution +#pragma pack(push, 2) +struct DLPClt_FinishContentUploadAck { + DLPPacketHeader head; + DLPPacketBool initialized; // 0x1 + u8 unk2; // 0x1 + u8 needs_content; // 0x1 if downloading conetnt + u32_be seq_ack; // BE client increments this every ack + u16 unk4; // 0x0 +}; +#pragma pack(pop) + +static_assert(sizeof(DLPClt_FinishContentUploadAck) == 0x1C); + +// start game +// these will keep sending until +// the final command is given +struct DLPSrvr_BeginGame { + DLPPacketHeader head; + u32_le unk1; // 0x1 + u32_le unk2; // 0x9 could be DLP_Srvr_State +}; + +static_assert(sizeof(DLPSrvr_BeginGame) == 0x18); + +struct DLPClt_BeginGameAck { + DLPPacketHeader head; + u32_le unk1; // 0x1 + u32_le unk2; // 0x9 could be DLP_Clt_State +}; + +static_assert(sizeof(DLPClt_BeginGameAck) == 0x18); + +// packet_index is 1. this is not acked +struct DLPSrvr_BeginGameFinal { + DLPPacketHeader head; + u32_le unk1; // 0x1 + std::array wireless_reboot_passphrase; + u8 unk2; // 0x09 could be server state + u16 padding; // 0x00 0x00 +}; + +static_assert(sizeof(DLPSrvr_BeginGameFinal) == 0x20); + +// END BIG ENDIAN + +class DLP_Base { +protected: + DLP_Base(Core::System& s); + virtual ~DLP_Base() = default; + + virtual std::shared_ptr GetServiceFrameworkSharedPtr() = 0; + virtual bool IsHost() = 0; + + Core::System& system; + + std::shared_ptr dlp_sharedmem; + std::shared_ptr uds_sharedmem; + + std::shared_ptr dlp_status_event; // out + std::shared_ptr uds_status_event; // in + + bool should_verify_checksum = false; + + const u32 uds_sharedmem_size = 0x4000; + const u32 uds_version = 0x400; + const u32 recv_buffer_size = 0x3c00; + const u32 dlp_channel = 0x10; + const u8 num_broadcast_packets = 5; + u32 dlp_sharedmem_size{}; + + DLP_Username username; + // stubbed as HLE NWM_UDS does not check this. Should be: 0km@tsa$uhmy1a0sa + nul + std::vector dlp_password_buf{}; + std::array wireless_reboot_passphrase; + + const u32 dlp_content_block_length = 182; + + std::shared_ptr GetCFG(); + std::shared_ptr GetUDS(); + + void GetEventDescription(Kernel::HLERequestContext& ctx); + + void InitializeDlpBase(u32 shared_mem_size, std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username username); + void FinalizeDlpBase(); + + bool ConnectToNetworkAsync(NWM::NetworkInfo net_info, NWM::ConnectionType conn_type, + std::vector passphrase); + int RecvFrom(u16 node_id, std::vector& buffer); + bool SendTo(u16 node_id, u8 data_channel, std::vector& buffer, u8 flags = 0); + + static std::u16string DLPUsernameAsString16(DLP_Username uname); + static DLP_Username String16AsDLPUsername(std::u16string str); + static DLPNodeInfo UDSToDLPNodeInfo(NWM::NodeInfo node_info); + template + static T* GetPacketBody(std::vector& b) { + if (b.size() < sizeof(T)) { + LOG_CRITICAL(Service_DLP, "Packet size is too small to fit content {} < {}", b.size(), + sizeof(T)); + return nullptr; + } + return reinterpret_cast(b.data()); + } + static DLPPacketHeader* GetPacketHead(std::vector& b) { + if (b.size() < sizeof(DLPPacketHeader)) { + LOG_CRITICAL(Service_DLP, "Packet is too small to fit a DLP header"); + return nullptr; + } + return reinterpret_cast(b.data()); + } + + static u32 GeneratePKChecksum(u32 aes_value, void* input_buffer, u32 packet_size); + + template + T* PGen_SetPK(std::array magic, u8 packet_index, std::array resp_id) { + if (!sm_packet_sender_session.try_acquire()) { + LOG_ERROR(Service_DLP, + "Tried to send 2 packets concurrently, causing blocking on this thread"); + sm_packet_sender_session.acquire(); + } + send_packet_ctx.resize(sizeof(T)); + auto ph = GetPacketHead(send_packet_ctx); + ph->magic = magic; + ph->size = sizeof(T); + ph->unk1 = {0x02, 0x00}; + ph->resp_id = resp_id; + ph->packet_index = packet_index; + return GetPacketBody(send_packet_ctx); + } + void PGen_SendPK(u32 aes, u16 node_id, u8 data_channel, u8 flags = 0) { + ASSERT(send_packet_ctx.size() >= sizeof(DLPPacketHeader)); + auto ph = GetPacketHead(send_packet_ctx); + ASSERT(ph->size == send_packet_ctx.size()); + ph->checksum = 0; + ph->checksum = GeneratePKChecksum(aes, ph, ph->size); + SendTo(node_id, data_channel, send_packet_ctx, flags); + send_packet_ctx.clear(); + sm_packet_sender_session.release(); + } + // input the host mac address + u32 GenDLPChecksumKey(Network::MacAddress mac_addr); + static void DLPEncryptCTR(void* out, size_t size, const u8* iv_ctr); + static bool ValidatePacket(u32 aes, void* pk, size_t sz, bool checksum = true); + + static u32 GetNumFragmentsFromTitleSize(u32 tsize); + +private: + std::binary_semaphore sm_packet_sender_session{1}; + std::vector send_packet_ctx; +}; + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_clnt.cpp b/src/core/hle/service/dlp/dlp_clnt.cpp index 8d79e9709..c4121a27c 100644 --- a/src/core/hle/service/dlp/dlp_clnt.cpp +++ b/src/core/hle/service/dlp/dlp_clnt.cpp @@ -1,4 +1,4 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -10,29 +10,150 @@ SERIALIZE_EXPORT_IMPL(Service::DLP::DLP_CLNT) namespace Service::DLP { -DLP_CLNT::DLP_CLNT() : ServiceFramework("dlp:CLNT", 1) { +std::shared_ptr DLP_CLNT::GetServiceFrameworkSharedPtr() { + return shared_from_this(); +} + +u32 DLP_CLNT::ClientNeedsDup() { + [[maybe_unused]] constexpr u32 res_needs_system_update = 0x1; + constexpr u32 res_does_not_need_update = 0x0; + return res_does_not_need_update; +} + +void DLP_CLNT::Initialize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 shared_mem_size = rp.Pop(); + u32 max_beacons = rp.Pop(); + u32 constant_mem_size = rp.Pop(); + auto [shared_mem, event] = rp.PopObjects(); + + InitializeCltBase(shared_mem_size, max_beacons, constant_mem_size, shared_mem, event, + String16AsDLPUsername(GetCFG()->GetUsername())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_CLNT::Finalize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + FinalizeCltBase(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +// returns the version of the currently joined server +void DLP_CLNT::GetCupVersion(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + [[maybe_unused]] auto mac_addr = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.PopRaw(); + [[maybe_unused]] u32 tid_high = rp.PopRaw(); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 0); + + // TODO: someone decipher this version code + u64 version_num = 0x0; + + rb.Push(ResultSuccess); + rb.Push(version_num); +} + +// tells us which server to connect to and download an update from +// the dlp app uses this to check whether or not we need the update data +void DLP_CLNT::PrepareForSystemDownload(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + mac_addr_update = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.PopRaw(); + [[maybe_unused]] u32 tid_high = rp.PopRaw(); + + if (ClientNeedsDup()) { + is_preparing_for_update = true; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + + rb.Push(ResultSuccess); + rb.Push(ClientNeedsDup()); +} + +// runs after the user accepts the license agreement to +// download the update +void DLP_CLNT::StartSystemDownload(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + if (!is_preparing_for_update) { + // error + LOG_ERROR(Service_DLP, "Called without preparing first. We don't have a mac address!"); + // TODO: verify this on hw + rb.Push(0xD960AC02); + return; + } + + is_preparing_for_update = false; + is_updating = true; + + // TODO: figure out what comes after when + // hw starts downloading update data via dlp. + // it could set some missing client states + // in GetCltState + + rb.Push(ResultSuccess); +} + +// i'm assuming this is a secondary check whether or not we +// can download the update data? +void DLP_CLNT::GetDupAvailability(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + mac_addr_update = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.PopRaw(); + [[maybe_unused]] u32 tid_high = rp.PopRaw(); + + LOG_WARNING(Service_DLP, "(STUBBED) called"); + + [[maybe_unused]] constexpr u32 dup_is_available = 0x1; + constexpr u32 dup_is_not_available = 0x0; + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + + rb.Push(ResultSuccess); + rb.Push(dup_is_not_available); +} + +DLP_CLNT::DLP_CLNT() + : ServiceFramework("dlp:CLNT", 1), DLP_Clt_Base(Core::System::GetInstance(), "CLNT") { static const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "Initialize"}, - {0x0002, nullptr, "Finalize"}, - {0x0003, nullptr, "GetEventDesc"}, - {0x0004, nullptr, "GetChannel"}, - {0x0005, nullptr, "StartScan"}, - {0x0006, nullptr, "StopScan"}, - {0x0007, nullptr, "GetServerInfo"}, - {0x0008, nullptr, "GetTitleInfo"}, - {0x0009, nullptr, "GetTitleInfoInOrder"}, - {0x000A, nullptr, "DeleteScanInfo"}, - {0x000B, nullptr, "PrepareForSystemDownload"}, - {0x000C, nullptr, "StartSystemDownload"}, - {0x000D, nullptr, "StartTitleDownload"}, - {0x000E, nullptr, "GetMyStatus"}, - {0x000F, nullptr, "GetConnectingNodes"}, - {0x0010, nullptr, "GetNodeInfo"}, - {0x0011, nullptr, "GetWirelessRebootPassphrase"}, - {0x0012, nullptr, "StopSession"}, - {0x0013, nullptr, "GetCupVersion"}, - {0x0014, nullptr, "GetDupAvailability"}, + {0x0001, &DLP_CLNT::Initialize, "Initialize"}, + {0x0002, &DLP_CLNT::Finalize, "Finalize"}, + {0x0003, &DLP_CLNT::GetEventDescription, "GetEventDescription"}, + {0x0004, &DLP_CLNT::GetChannels, "GetChannel"}, + {0x0005, &DLP_CLNT::StartScan, "StartScan"}, + {0x0006, &DLP_CLNT::StopScan, "StopScan"}, + {0x0007, &DLP_CLNT::GetServerInfo, "GetServerInfo"}, + {0x0008, &DLP_CLNT::GetTitleInfo, "GetTitleInfo"}, + {0x0009, &DLP_CLNT::GetTitleInfoInOrder, "GetTitleInfoInOrder"}, + {0x000A, &DLP_CLNT::DeleteScanInfo, "DeleteScanInfo"}, + {0x000B, &DLP_CLNT::PrepareForSystemDownload, "PrepareForSystemDownload"}, + {0x000C, &DLP_CLNT::StartSystemDownload, "StartSystemDownload"}, + {0x000D, &DLP_CLNT::StartSession, "StartTitleDownload"}, + {0x000E, &DLP_CLNT::GetMyStatus, "GetMyStatus"}, + {0x000F, &DLP_CLNT::GetConnectingNodes, "GetConnectingNodes"}, + {0x0010, &DLP_CLNT::GetNodeInfo, "GetNodeInfo"}, + {0x0011, &DLP_CLNT::GetWirelessRebootPassphrase, "GetWirelessRebootPassphrase"}, + {0x0012, &DLP_CLNT::StopSession, "StopSession"}, + {0x0013, &DLP_CLNT::GetCupVersion, "GetCupVersion"}, + {0x0014, &DLP_CLNT::GetDupAvailability, "GetDupAvailability"}, // clang-format on }; diff --git a/src/core/hle/service/dlp/dlp_clnt.h b/src/core/hle/service/dlp/dlp_clnt.h index ac6933e7e..8b9a56300 100644 --- a/src/core/hle/service/dlp/dlp_clnt.h +++ b/src/core/hle/service/dlp/dlp_clnt.h @@ -1,20 +1,41 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "core/hle/service/service.h" +#include "dlp_clt_base.h" namespace Service::DLP { -class DLP_CLNT final : public ServiceFramework { +class DLP_CLNT final : public ServiceFramework, public DLP_Clt_Base { public: DLP_CLNT(); - ~DLP_CLNT() = default; + virtual ~DLP_CLNT() = default; + + virtual std::shared_ptr GetServiceFrameworkSharedPtr(); private: SERVICE_SERIALIZATION_SIMPLE + + virtual bool IsFKCL() { + return false; + } + + bool is_preparing_for_update = false; + bool is_updating = false; + Network::MacAddress mac_addr_update; + + u32 ClientNeedsDup(); + + void Initialize(Kernel::HLERequestContext& ctx); + void Finalize(Kernel::HLERequestContext& ctx); + void GetCupVersion(Kernel::HLERequestContext& ctx); + void StartTitleDownload(Kernel::HLERequestContext& ctx); + void PrepareForSystemDownload(Kernel::HLERequestContext& ctx); + void StartSystemDownload(Kernel::HLERequestContext& ctx); + void GetDupAvailability(Kernel::HLERequestContext& ctx); }; } // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_clt_base.cpp b/src/core/hle/service/dlp/dlp_clt_base.cpp new file mode 100644 index 000000000..d127dbd36 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_clt_base.cpp @@ -0,0 +1,856 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "dlp_clt_base.h" + +#include "common/alignment.h" +#include "common/string_util.h" +#include "common/timer.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/am/am.h" +#include "core/hle/service/nwm/uds_beacon.h" + +namespace Service::DLP { + +DLP_Clt_Base::DLP_Clt_Base(Core::System& s, std::string unique_string_id) : DLP_Base(s) { + std::string unique_scan_event_id = fmt::format("DLP::{}::BeaconScanCallback", unique_string_id); + beacon_scan_event = system.CoreTiming().RegisterEvent( + unique_scan_event_id, [this](std::uintptr_t user_data, s64 cycles_late) { + BeaconScanCallback(user_data, cycles_late); + }); +} + +DLP_Clt_Base::~DLP_Clt_Base() { + { + std::scoped_lock lock(beacon_mutex); + is_scanning = false; + system.CoreTiming().UnscheduleEvent(beacon_scan_event, 0); + } + + DisconnectFromServer(); +} + +void DLP_Clt_Base::InitializeCltBase(u32 shared_mem_size, u32 max_beacons, u32 constant_mem_size, + std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username username) { + InitializeDlpBase(shared_mem_size, shared_mem, event, username); + + clt_state = DLP_Clt_State::Initialized; + max_title_info = max_beacons; + + LOG_INFO(Service_DLP, + "shared mem size: 0x{:x}, max beacons: {}, constant mem size: 0x{:x}, username: {}", + shared_mem_size, max_beacons, constant_mem_size, + Common::UTF16ToUTF8(DLPUsernameAsString16(username)).c_str()); +} + +void DLP_Clt_Base::FinalizeCltBase() { + clt_state = DLP_Clt_State::Initialized; + + if (is_connected) { + DisconnectFromServer(); + } + + FinalizeDlpBase(); + + LOG_INFO(Service_DLP, "called"); +} + +void DLP_Clt_Base::GenerateChannelHandle() { + dlp_channel_handle = 0x0421; // it seems to always be this value on hardware +} + +u32 DLP_Clt_Base::GetCltState() { + std::scoped_lock lock(clt_state_mutex); + u16 node_id = 0x0; + if (is_connected) { + node_id = GetUDS()->GetConnectionStatusHLE().network_node_id; + } + return static_cast(clt_state) << 24 | is_connected << 16 | node_id; +} + +void DLP_Clt_Base::GetChannels(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + GenerateChannelHandle(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); + rb.Push(ResultSuccess); + rb.Push(dlp_channel_handle); +} + +void DLP_Clt_Base::GetMyStatus(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(6, 0); + rb.Push(ResultSuccess); + rb.Push(GetCltState()); + rb.Push(dlp_units_total); + rb.Push(dlp_units_downloaded); + // TODO: find out what these are + rb.Push(0x0); + rb.Push(0x0); +} + +int DLP_Clt_Base::GetCachedTitleInfoIdx(Network::MacAddress mac_addr) { + std::scoped_lock lock(title_info_mutex); + + for (int i = 0; auto& t : scanned_title_info) { + if (t.first.mac_addr == mac_addr) { + return i; + } + i++; + } + return -1; +} + +bool DLP_Clt_Base::TitleInfoIsCached(Network::MacAddress mac_addr) { + return GetCachedTitleInfoIdx(mac_addr) != -1; +} + +void DLP_Clt_Base::StartScan(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u16 scan_handle = rp.Pop(); + scan_title_id_filter = rp.Pop(); + scan_mac_address_filter = rp.PopRaw(); + ASSERT_MSG( + scan_handle == dlp_channel_handle, + "Scan handle and dlp channel handle do not match. Did you input the wrong ipc params?"); + [[maybe_unused]] u32 unk1 = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + // start beacon worker + if (!IsIdling()) { + rb.Push(Result(0x1, ErrorModule::DLP, ErrorSummary::InvalidState, ErrorLevel::Usage)); + return; + } + + std::scoped_lock lock{beacon_mutex, title_info_mutex, clt_state_mutex}; + + // reset scan dependent variables + scanned_title_info.clear(); + ignore_servers_list.clear(); + title_info_index = 0; + + clt_state = DLP_Clt_State::Scanning; + is_scanning = true; + + // clear out received beacons + GetUDS()->GetReceivedBeacons(Network::BroadcastMac); + + LOG_INFO(Service_DLP, "Starting scan worker"); + + constexpr int first_scan_delay_ms = 0; + + system.CoreTiming().ScheduleEvent(msToCycles(first_scan_delay_ms), beacon_scan_event, 0); + + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::StopScan(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + // end beacon worker + { + std::scoped_lock lock{beacon_mutex, clt_state_mutex}; + clt_state = DLP_Clt_State::Initialized; + is_scanning = false; + + LOG_INFO(Service_DLP, "Ending scan worker"); + + system.CoreTiming().UnscheduleEvent(beacon_scan_event, 0); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::GetTitleInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + auto mac_addr = rp.PopRaw(); + [[maybe_unused]] u32 tid_low = rp.Pop(); + [[maybe_unused]] u32 tid_high = rp.Pop(); + + std::scoped_lock lock(title_info_mutex); + + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + auto c_title_idx = GetCachedTitleInfoIdx(mac_addr); + std::vector buffer = scanned_title_info[c_title_idx].first.ToBuffer(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushStaticBuffer(std::move(buffer), 0); +} + +void DLP_Clt_Base::GetTitleInfoInOrder(Kernel::HLERequestContext& ctx) { + constexpr u8 cmd_reset_iterator = 0x1; + + IPC::RequestParser rp(ctx); + + u8 command = rp.Pop(); + if (command == cmd_reset_iterator) { + title_info_index = 0; + } + + std::scoped_lock lock(title_info_mutex); + + if (title_info_index >= scanned_title_info.size()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + std::vector buffer = scanned_title_info[title_info_index].first.ToBuffer(); + + ++title_info_index; + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushStaticBuffer(std::move(buffer), 0); +} + +void DLP_Clt_Base::DeleteScanInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_DLP, "Called"); + + auto mac_addr = rp.PopRaw(); + + std::scoped_lock lock(title_info_mutex); + + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + scanned_title_info.erase(scanned_title_info.begin() + GetCachedTitleInfoIdx(mac_addr)); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::GetServerInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + auto mac_addr = rp.PopRaw(); + + std::scoped_lock lock(title_info_mutex); + + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + + auto buffer = scanned_title_info[GetCachedTitleInfoIdx(mac_addr)].second.ToBuffer(); + + rb.Push(ResultSuccess); + rb.PushStaticBuffer(std::move(buffer), 0); +} + +class DLP_Clt_Base::ThreadCallback : public Kernel::HLERequestContext::WakeupCallback { +public: + explicit ThreadCallback(std::shared_ptr p) : p_obj(p) {} + + void WakeUp(std::shared_ptr thread, Kernel::HLERequestContext& ctx, + Kernel::ThreadWakeupReason reason) { + IPC::RequestBuilder rb(ctx, 1, 0); + + if (!p_obj->OnConnectCallback()) { + rb.Push(Result(ErrorDescription::Timeout, ErrorModule::DLP, ErrorSummary::Canceled, + ErrorLevel::Status)); + return; + } + rb.Push(ResultSuccess); + } + +private: + ThreadCallback() = default; + std::shared_ptr p_obj; + + template + void serialize(Archive& ar, const unsigned int) { + ar& boost::serialization::base_object(*this); + } + friend class boost::serialization::access; +}; + +bool DLP_Clt_Base::OnConnectCallback() { + auto uds = GetUDS(); + if (uds->GetConnectionStatusHLE().status != NWM::NetworkStatus::ConnectedAsClient) { + LOG_ERROR(Service_DLP, "Could not connect to dlp server (timed out)"); + return false; + } + + is_connected = true; + + client_connection_worker = std::thread([this] { ClientConnectionManager(); }); + + return true; +} + +void DLP_Clt_Base::StartSession(Kernel::HLERequestContext& ctx) { + std::scoped_lock lock(clt_state_mutex); + IPC::RequestParser rp(ctx); + + auto mac_addr = rp.PopRaw(); + + LOG_INFO(Service_DLP, "called"); + + // tells us which child we want to use for this session + // only used for dlp::CLNT + u32 dlp_child_low = rp.Pop(); + u32 dlp_child_high = rp.Pop(); + + if (!IsIdling()) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(0x1, ErrorModule::DLP, ErrorSummary::InvalidState, ErrorLevel::Usage)); + return; + } + if (!TitleInfoIsCached(mac_addr)) { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + dlp_download_child_tid = static_cast(dlp_child_high) << 32 | dlp_child_low; + + // ConnectToNetworkAsync won't work here beacuse this is + // synchronous + + auto shared_this = std::dynamic_pointer_cast(GetServiceFrameworkSharedPtr()); + if (!shared_this) { + LOG_CRITICAL(Service_DLP, + "Could not dynamic_cast service framework shared_ptr to DLP_Clt_Base"); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(-1); + return; + } + + host_mac_address = mac_addr; + clt_state = DLP_Clt_State::Joined; + + auto uds = GetUDS(); + NWM::NetworkInfo net_info; + net_info.host_mac_address = mac_addr; + net_info.channel = dlp_net_info_channel; + net_info.initialized = true; + net_info.oui_value = NWM::NintendoOUI; + + uds->ConnectToNetworkHLE(net_info, static_cast(NWM::ConnectionType::Client), + dlp_password_buf); + + // 3 second timeout + constexpr std::chrono::nanoseconds UDSConnectionTimeout{3000000000}; + uds->connection_event = + ctx.SleepClientThread("DLP_Clt_Base::StartSession", UDSConnectionTimeout, + std::make_shared(shared_this)); +} + +void DLP_Clt_Base::StopSession(Kernel::HLERequestContext& ctx) { + LOG_INFO(Service_DLP, "called"); + std::scoped_lock lock(clt_state_mutex); + IPC::RequestParser rp(ctx); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + if (is_connected) { + DisconnectFromServer(); + } + + // this call returns success no matter what + rb.Push(ResultSuccess); +} + +void DLP_Clt_Base::GetConnectingNodes(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u16 node_array_len = rp.Pop(); + + IPC::RequestBuilder rb = rp.MakeBuilder(2, 2); + + auto conn_status = GetUDS()->GetConnectionStatusHLE(); + + if (!is_connected || conn_status.status != NWM::NetworkStatus::ConnectedAsClient) { + LOG_ERROR(Service_DLP, "called when we are not connected to a server"); + } + + std::vector connected_nodes_buffer; + connected_nodes_buffer.resize(node_array_len * sizeof(u16)); + memcpy(connected_nodes_buffer.data(), conn_status.nodes, + std::min(connected_nodes_buffer.size(), conn_status.total_nodes) * sizeof(u16)); + + rb.Push(ResultSuccess); + rb.Push(conn_status.total_nodes); + rb.PushStaticBuffer(std::move(connected_nodes_buffer), 0); +} + +void DLP_Clt_Base::GetNodeInfo(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u16 network_node_id = rp.Pop(); + + auto node_info = GetUDS()->GetNodeInformationHLE(network_node_id); + if (!node_info) { + LOG_ERROR(Service_DLP, "Could not get node info for network node id 0x{:x}", + network_node_id); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NoData, ErrorModule::DLP, ErrorSummary::NotFound, + ErrorLevel::Status)); + return; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(11, 0); + + rb.Push(ResultSuccess); + rb.PushRaw(UDSToDLPNodeInfo(*node_info)); +} + +void DLP_Clt_Base::GetWirelessRebootPassphrase(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_INFO(Service_DLP, "called"); + + std::scoped_lock lock(clt_state_mutex); + if (clt_state != DLP_Clt_State::Complete) { + LOG_WARNING(Service_DLP, "we have not gotten the passphrase yet"); + } + + IPC::RequestBuilder rb = rp.MakeBuilder(4, 0); + rb.Push(ResultSuccess); + rb.PushRaw(wireless_reboot_passphrase); +} + +void DLP_Clt_Base::BeaconScanCallback(std::uintptr_t user_data, s64 cycles_late) { + std::scoped_lock lock{beacon_mutex, title_info_mutex}; + + if (!is_scanning) { + return; + } + + auto uds = GetUDS(); + Common::Timer beacon_parse_timer_total; + + // sadly, we have to impl the scan code ourselves + // because the nwm recvbeaconbroadcastdata function + // has a timeout in it, which won't work here because + // we don't have a uds server/client session + auto beacons = uds->GetReceivedBeacons(Network::BroadcastMac); + + beacon_parse_timer_total.Start(); + + for (auto& beacon : beacons) { + if (auto idx = GetCachedTitleInfoIdx(beacon.transmitter_address); idx != -1) { + // update server info from beacon + auto b = GetDLPServerInfoFromRawBeacon(beacon); + scanned_title_info[idx].second.clients_joined = + b.clients_joined; // we only want to update clients joined + continue; + } + if (scanned_title_info.size() >= max_title_info) { + break; + } + if (ignore_servers_list[beacon.transmitter_address]) { + continue; + } + + CacheBeaconTitleInfo(beacon); + } + + // set our next scan interval + system.CoreTiming().ScheduleEvent( + msToCycles(std::max(0, beacon_scan_interval_ms - + beacon_parse_timer_total.GetTimeElapsed().count())) - + cycles_late, + beacon_scan_event, 0); +} + +void DLP_Clt_Base::CacheBeaconTitleInfo(Network::WifiPacket& beacon) { + // connect to the network as a spectator + // and receive dlp data + + auto uds = GetUDS(); + + NWM::NetworkInfo net_info; + net_info.host_mac_address = beacon.transmitter_address; + net_info.channel = dlp_net_info_channel; + net_info.initialized = true; + net_info.oui_value = NWM::NintendoOUI; + + if (!ConnectToNetworkAsync(net_info, NWM::ConnectionType::Spectator, dlp_password_buf)) { + LOG_ERROR(Service_DLP, "Could not connect to network."); + return; + } + + LOG_INFO(Service_DLP, "Connected to spec to network"); + + auto [ret, data_available_event] = + uds->BindHLE(dlp_bind_node_id, dlp_recv_buffer_size, dlp_broadcast_data_channel, + dlp_host_network_node_id); + if (ret != NWM::ResultStatus::ResultSuccess) { + LOG_ERROR(Service_DLP, "Could not bind on node id 0x{:x}", dlp_bind_node_id); + return; + } + + auto aes = GenDLPChecksumKey(beacon.transmitter_address); + + constexpr u32 max_beacon_recv_time_out_ms = 1000; + + Common::Timer beacon_parse_timer; + beacon_parse_timer.Start(); + + std::unordered_map got_broadcast_packet; + std::unordered_map> broadcast_packet_idx_buf; + DLP_Username server_username; // workaround before I decrypt the beacon data + std::vector recv_buf; + bool got_all_packets = false; + while (beacon_parse_timer.GetTimeElapsed().count() < max_beacon_recv_time_out_ms) { + if (int sz = RecvFrom(dlp_host_network_node_id, recv_buf)) { + auto p_head = reinterpret_cast(recv_buf.data()); + if (!ValidatePacket(aes, p_head, sz, should_verify_checksum) || + p_head->packet_index >= num_broadcast_packets) { + ignore_servers_list[beacon.transmitter_address] = true; + break; // corrupted info + } + got_broadcast_packet[p_head->packet_index] = true; + broadcast_packet_idx_buf[p_head->packet_index] = recv_buf; + if (got_broadcast_packet.size() == num_broadcast_packets) { + got_all_packets = true; + constexpr u16 nwm_host_node_network_id = 0x1; + server_username = uds->GetNodeInformationHLE(nwm_host_node_network_id)->username; + break; // we got all 5! + } + } + } + + uds->UnbindHLE(dlp_bind_node_id); + uds->DisconnectNetworkHLE(); + + if (!got_all_packets) { + if (!got_broadcast_packet.size()) { + // we didn't get ANY packet info from this server + // so we add it to the ignore list + ignore_servers_list[beacon.transmitter_address] = true; + } + LOG_ERROR(Service_DLP, "Connected to beacon, but could not receive all dlp packets"); + return; + } + + // parse packets into cached DLPServerInfo and DLPTitleInfo + auto broad_pk1 = reinterpret_cast(broadcast_packet_idx_buf[0].data()); + auto broad_pk2 = reinterpret_cast(broadcast_packet_idx_buf[1].data()); + auto broad_pk3 = reinterpret_cast(broadcast_packet_idx_buf[2].data()); + auto broad_pk4 = reinterpret_cast(broadcast_packet_idx_buf[3].data()); + [[maybe_unused]] auto broad_pk5 = + reinterpret_cast(broadcast_packet_idx_buf[4].data()); + + // apply title filter + if (scan_title_id_filter && broad_pk1->child_title_id != scan_title_id_filter) { + LOG_WARNING(Service_DLP, "Got title info, but it did not match title id filter"); + return; + } + + DLPServerInfo c_server_info = GetDLPServerInfoFromRawBeacon(beacon); + { + // workaround: load username in host node manually + c_server_info.node_info[0].username = server_username; + } + + DLPTitleInfo c_title_info{}; + c_title_info.mac_addr = beacon.transmitter_address; + + // copy over title string data + std::copy(broad_pk1->title_short.begin(), broad_pk1->title_short.end(), + c_title_info.short_description.begin()); + std::copy(broad_pk1->title_long.begin(), broad_pk1->title_long.end(), + c_title_info.long_description.begin()); + + // unique id should be the title id without the tid high shifted 1 byte right + c_title_info.unique_id = (broad_pk1->child_title_id & 0xFFFFFFFF) >> 8; + + c_title_info.size = broad_pk1->size + broad_title_size_diff; + + // copy over the icon data + auto icon_copy_loc = c_title_info.icon.begin(); + icon_copy_loc = + std::copy(broad_pk1->icon_part.begin(), broad_pk1->icon_part.end(), icon_copy_loc); + icon_copy_loc = + std::copy(broad_pk2->icon_part.begin(), broad_pk2->icon_part.end(), icon_copy_loc); + icon_copy_loc = + std::copy(broad_pk3->icon_part.begin(), broad_pk3->icon_part.end(), icon_copy_loc); + icon_copy_loc = + std::copy(broad_pk4->icon_part.begin(), broad_pk4->icon_part.end(), icon_copy_loc); + + LOG_INFO(Service_DLP, "Got title info"); + + scanned_title_info.emplace_back(c_title_info, c_server_info); + + dlp_status_event->Signal(); +} + +DLPServerInfo DLP_Clt_Base::GetDLPServerInfoFromRawBeacon(Network::WifiPacket& beacon) { + // get networkinfo from beacon + auto p_beacon = beacon.data.data(); + + bool found_net_info = false; + NWM::NetworkInfo net_info; + + // find networkinfo tag + for (auto place = p_beacon + sizeof(NWM::BeaconFrameHeader); place < place + beacon.data.size(); + place += reinterpret_cast(place)->length + sizeof(NWM::TagHeader)) { + auto th = reinterpret_cast(place); + if (th->tag_id == static_cast(NWM::TagId::VendorSpecific) && + th->length <= sizeof(NWM::NetworkInfoTag) - sizeof(NWM::TagHeader)) { + // cast to network info and check if correct + auto ni_tag = reinterpret_cast(place); + memcpy(&net_info.oui_value, ni_tag->network_info.data(), ni_tag->network_info.size()); + // make sure this is really a network info tag + if (net_info.oui_value == NWM::NintendoOUI && + net_info.oui_type == static_cast(NWM::NintendoTagId::NetworkInfo)) { + found_net_info = true; + break; + } + } + } + + if (!found_net_info) { + LOG_ERROR(Service_DLP, "Unable to find network info in beacon payload"); + return DLPServerInfo{}; + } + + DLPServerInfo srv_info{}; + srv_info.mac_addr = beacon.transmitter_address; + srv_info.max_clients = net_info.max_nodes; + srv_info.clients_joined = net_info.total_nodes; + srv_info.signal_strength = DLPSignalStrength::Strong; + srv_info.unk5 = 0x6; + // TODO: decrypt node info and load it in here + return srv_info; +} + +void DLP_Clt_Base::ClientConnectionManager() { + auto uds = GetUDS(); + + auto [ret, data_available_event] = uds->BindHLE( + dlp_bind_node_id, dlp_recv_buffer_size, dlp_client_data_channel, dlp_host_network_node_id); + if (ret != NWM::ResultStatus::ResultSuccess) { + LOG_ERROR(Service_DLP, "Could not bind on node id 0x{:x}", dlp_bind_node_id); + return; + } + + auto aes = GenDLPChecksumKey(host_mac_address); + + auto sleep_poll = [](size_t poll_rate) -> void { + std::this_thread::sleep_for(std::chrono::milliseconds(poll_rate)); + }; + + constexpr u32 dlp_poll_rate_normal = 100; + constexpr u32 dlp_poll_rate_distribute = 0; + + u32 dlp_poll_rate_ms = dlp_poll_rate_normal; + bool got_corrupted_packets = false; + + std::set received_fragments; + + while (sleep_poll(dlp_poll_rate_ms), is_connected) { + std::vector recv_buf; + + if (int sz = RecvFrom(dlp_host_network_node_id, recv_buf)) { + auto p_head = GetPacketHead(recv_buf); + // validate packet header + if (!ValidatePacket(aes, p_head, sz, should_verify_checksum)) { + got_corrupted_packets = true; + LOG_ERROR(Service_DLP, "Could not validate DLP packet header"); + break; + } + + // now we can parse the packet + std::scoped_lock lock{clt_state_mutex, title_info_mutex}; + if (p_head->type == dl_pk_type_auth) { + auto s_body = + PGen_SetPK(dl_pk_head_auth_header, 0, p_head->resp_id); + s_body->initialized = true; + // TODO: find out what this is. this changes each session. + // placeholder + s_body->resp_id = {0x01, 0x02}; + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else if (p_head->type == dl_pk_type_start_dist) { + // poll rate on non-downloading clients still needs to + // be quick enough to eat broadcast content frag packets + dlp_poll_rate_ms = dlp_poll_rate_distribute; + + if (IsFKCL() || !NeedsContentDownload(host_mac_address)) { + auto s_body = PGen_SetPK( + dl_pk_head_start_dist_header, 0, p_head->resp_id); + s_body->initialized = true; + s_body->unk2 = 0x0; + is_downloading_content = false; + clt_state = DLP_Clt_State::WaitingForServerReady; + } else { + // send content needed ack + auto s_body = PGen_SetPK( + dl_pk_head_start_dist_header, 0, p_head->resp_id); + s_body->initialized = true; + // TODO: figure out what these are. seems like magic values + s_body->unk2 = 0x20; + s_body->unk3 = 0x0; + s_body->unk4 = true; + s_body->unk5 = 0x0; + s_body->unk_body = {}; // all zeros + is_downloading_content = true; + clt_state = DLP_Clt_State::Downloading; + + if (!TitleInfoIsCached(host_mac_address)) { + LOG_CRITICAL( + Service_DLP, + "Tried to request content download, but title info was not cached"); + break; + } + + auto tinfo = scanned_title_info[GetCachedTitleInfoIdx(host_mac_address)].first; + + dlp_units_downloaded = 0; + dlp_units_total = GetNumFragmentsFromTitleSize(tinfo.size); + current_content_block = 0; + LOG_INFO(Service_DLP, "Requesting game content"); + } + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else if (p_head->type == dl_pk_type_distribute) { + if (is_downloading_content) { + auto r_pbody = GetPacketBody(recv_buf); + if (r_pbody->frag_size > sz - sizeof(DLPSrvr_ContentDistributionFragment)) { + LOG_CRITICAL(Service_DLP, + "Embedded fragment size is too large. Ignoring fragment."); + continue; + } + std::span cf(r_pbody->content_fragment, + static_cast(r_pbody->frag_size)); + ReceivedFragment frag{ + .index = static_cast(r_pbody->frag_index + + dlp_content_block_length * current_content_block), + .content{cf.begin(), cf.end()}}; + received_fragments.insert(frag); + dlp_units_downloaded++; + if (dlp_units_downloaded == dlp_units_total) { + is_downloading_content = false; + LOG_INFO(Service_DLP, "Finished downloading content. Installing..."); + + if (!InstallEncryptedCIAFromFragments(received_fragments)) { + LOG_ERROR(Service_DLP, "Could not install DLP encrypted content"); + } else { + LOG_INFO(Service_DLP, "Successfully installed DLP encrypted content"); + } + + clt_state = DLP_Clt_State::WaitingForServerReady; + } + } + } else if (p_head->type == dl_pk_type_finish_dist) { + if (p_head->packet_index == 1) { + auto r_pbody = GetPacketBody(recv_buf); + auto s_body = PGen_SetPK( + dl_pk_head_finish_dist_header, 0, p_head->resp_id); + if (is_downloading_content) { + current_content_block++; + } + s_body->initialized = true; + s_body->unk2 = 0x1; + s_body->needs_content = is_downloading_content; + s_body->seq_ack = r_pbody->seq_num + 1; + s_body->unk4 = 0x0; + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else { + LOG_ERROR(Service_DLP, "Received finish dist packet, but packet index was {}", + p_head->packet_index); + } + } else if (p_head->type == dl_pk_type_start_game) { + if (p_head->packet_index == 0) { + dlp_poll_rate_ms = dlp_poll_rate_normal; + auto s_body = PGen_SetPK(dl_pk_head_start_game_header, 0, + p_head->resp_id); + s_body->unk1 = 0x1; + s_body->unk2 = 0x9; + PGen_SendPK(aes, dlp_host_network_node_id, dlp_client_data_channel); + } else if (p_head->packet_index == 1) { + clt_state = DLP_Clt_State::Complete; + auto r_pbody = GetPacketBody(recv_buf); + wireless_reboot_passphrase = r_pbody->wireless_reboot_passphrase; + } else { + LOG_ERROR(Service_DLP, "Unknown packet index {}", p_head->packet_index); + } + } else { + LOG_ERROR(Service_DLP, "Unknown DLP Magic 0x{:x} 0x{:x} 0x{:x} 0x{:x}", + p_head->magic[0], p_head->magic[1], p_head->magic[2], p_head->magic[3]); + } + } + } + + uds->UnbindHLE(dlp_host_network_node_id); + uds->DisconnectNetworkHLE(); +} + +bool DLP_Clt_Base::NeedsContentDownload(Network::MacAddress mac_addr) { + std::scoped_lock lock(title_info_mutex); + if (!TitleInfoIsCached(mac_addr)) { + LOG_ERROR(Service_DLP, "title info was not cached"); + return false; + } + auto tinfo = scanned_title_info[GetCachedTitleInfoIdx(mac_addr)].first; + u64 title_id = DLP_CHILD_TID_HIGH | (tinfo.unique_id << 8); + return !FileUtil::Exists(AM::GetTitleContentPath(FS::MediaType::NAND, title_id)); +} + +// DLP Fragments contain encrypted CIA content by design. +// It is required to decrypt them in order to achieve +// interoperability between HLE & LLE service modules. +bool DLP_Clt_Base::InstallEncryptedCIAFromFragments(std::set& frags) { + auto cia_file = std::make_unique(system, FS::MediaType::NAND); + cia_file->AuthorizeDecryptionFromHLE(); + bool install_errored = false; + for (u64 nb = 0; auto& frag : frags) { + constexpr bool flush_data = true; + constexpr bool update_timestamp = false; + auto res = cia_file->Write(nb, frag.content.size(), flush_data, update_timestamp, + frag.content.data()); + + if (res.Failed()) { + LOG_ERROR(Service_DLP, "Could not install CIA. Error code {:08x}", res.Code().raw); + install_errored = true; + break; + } + + nb += frag.content.size(); + } + cia_file->Close(); + return !install_errored; +} + +void DLP_Clt_Base::DisconnectFromServer() { + is_connected = false; + if (client_connection_worker.joinable()) { + client_connection_worker.join(); + } +} + +bool DLP_Clt_Base::IsIdling() { + std::scoped_lock lock(beacon_mutex); + return !is_scanning && !is_connected; +} + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_clt_base.h b/src/core/hle/service/dlp/dlp_clt_base.h new file mode 100644 index 000000000..10da159b3 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_clt_base.h @@ -0,0 +1,144 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "core/hle/kernel/event.h" +#include "core/hle/kernel/shared_memory.h" +#include "dlp_base.h" + +namespace Service::DLP { + +enum class DLP_Clt_State : u32 { + NotInitialized = 0, // TODO: check on hardware. it probably just errors + Initialized = 1, + Scanning = 2, + Joined = 5, + Downloading = 6, + WaitingForServerReady = 7, + Complete = 9, +}; + +// number of bars +enum class DLPSignalStrength : u8 { + VeryWeak = 0, + Weak = 1, + Medium = 2, + Strong = 3, +}; + +// info from a server that +// can be obtained from its beacon only +struct DLPServerInfo { + Network::MacAddress mac_addr; + u8 unk1; + DLPSignalStrength signal_strength; + u8 max_clients; + u8 clients_joined; + u16 unk3; // node bitmask? + u32 padding; // all zeros + std::array node_info; + u32 unk4; + u32 unk5; + std::vector ToBuffer() { + std::vector out; + out.resize(sizeof(DLPServerInfo)); + memcpy(out.data(), this, sizeof(DLPServerInfo)); + return out; + } +}; + +static_assert(sizeof(DLPServerInfo) == 0x298); + +class DLP_Clt_Base : public DLP_Base { +protected: + DLP_Clt_Base(Core::System& s, std::string unique_string_id); + virtual ~DLP_Clt_Base(); + + virtual bool IsHost() { + return false; + } + + class ThreadCallback; + bool OnConnectCallback(); + void ClientConnectionManager(); + + virtual bool IsFKCL() = 0; + bool IsCLNT() { + return !IsFKCL(); + } + + DLP_Clt_State clt_state = DLP_Clt_State::NotInitialized; + u16 dlp_channel_handle{}; + std::atomic_bool is_connected = false; + u32 dlp_units_downloaded = 0x0, dlp_units_total = 0x0; + u64 dlp_download_child_tid = 0x0; + u32 title_info_index = 0; + u32 max_title_info = 0; ///< once we receive x beacons, we will no longer parse any other + ///< beacons until at least one tinfo buf element is cleared + bool is_scanning = false; + constexpr static inline int beacon_scan_interval_ms = 1000; + std::vector> scanned_title_info; + std::map + ignore_servers_list; // ignore servers which give us bad broadcast data + u64 scan_title_id_filter; + Network::MacAddress scan_mac_address_filter; + Network::MacAddress host_mac_address; + constexpr static inline u16 dlp_net_info_channel = 0x1; + constexpr static inline u16 dlp_bind_node_id = 0x1; + constexpr static inline u32 dlp_recv_buffer_size = 0x3c00; + constexpr static inline u8 dlp_broadcast_data_channel = 0x1; + constexpr static inline u8 dlp_client_data_channel = 0x2; + constexpr static inline u8 dlp_host_network_node_id = 0x1; + + Core::TimingEventType* beacon_scan_event; + + std::mutex beacon_mutex; + std::recursive_mutex title_info_mutex; + std::recursive_mutex clt_state_mutex; + + std::thread client_connection_worker; + + bool is_downloading_content; + struct ReceivedFragment { + u32 index; + std::vector content; + bool operator<(const ReceivedFragment& o) const { + return index < o.index; + } + }; + u16 current_content_block; + + void InitializeCltBase(u32 shared_mem_size, u32 max_beacons, u32 constant_mem_size, + std::shared_ptr shared_mem, + std::shared_ptr event, DLP_Username username); + void FinalizeCltBase(); + void GenerateChannelHandle(); + u32 GetCltState(); + void BeaconScanCallback(std::uintptr_t user_data, s64 cycles_late); + void CacheBeaconTitleInfo(Network::WifiPacket& beacon); + int GetCachedTitleInfoIdx(Network::MacAddress mac_addr); + bool TitleInfoIsCached(Network::MacAddress mac_addr); + DLPServerInfo GetDLPServerInfoFromRawBeacon(Network::WifiPacket& beacon); + bool NeedsContentDownload(Network::MacAddress mac_addr); + bool InstallEncryptedCIAFromFragments(std::set& frags); + void DisconnectFromServer(); + bool IsIdling(); + + void GetMyStatus(Kernel::HLERequestContext& ctx); + void GetChannels(Kernel::HLERequestContext& ctx); + void GetTitleInfo(Kernel::HLERequestContext& ctx); + void GetTitleInfoInOrder(Kernel::HLERequestContext& ctx); + void StartScan(Kernel::HLERequestContext& ctx); + void StopScan(Kernel::HLERequestContext& ctx); + void DeleteScanInfo(Kernel::HLERequestContext& ctx); + void GetServerInfo(Kernel::HLERequestContext& ctx); + void StartSession(Kernel::HLERequestContext& ctx); + void StopSession(Kernel::HLERequestContext& ctx); + void GetConnectingNodes(Kernel::HLERequestContext& ctx); + void GetNodeInfo(Kernel::HLERequestContext& ctx); + void GetWirelessRebootPassphrase(Kernel::HLERequestContext& ctx); +}; + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_crypto.cpp b/src/core/hle/service/dlp/dlp_crypto.cpp new file mode 100644 index 000000000..4cefd3516 --- /dev/null +++ b/src/core/hle/service/dlp/dlp_crypto.cpp @@ -0,0 +1,31 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include "common/archives.h" +#include "common/logging/log.h" +#include "core/core.h" +#include "core/hle/ipc_helpers.h" +#include "core/hle/service/dlp/dlp_base.h" +#include "core/hle/service/ssl/ssl_c.h" +#include "core/hw/aes/arithmetic128.h" +#include "core/hw/aes/key.h" + +namespace Service::DLP { + +void DLP_Base::DLPEncryptCTR(void* _out, size_t size, const u8* iv_ctr) { + auto out = reinterpret_cast(_out); + memset(out, 0, size); + + HW::AES::SelectDlpNfcKeyYIndex(HW::AES::DlpNfcKeyY::Dlp); + HW::AES::AESKey key = HW::AES::GetNormalKey(HW::AES::DLPNFCDataKey); + + // AlgorithmType::CTR_Encrypt + CryptoPP::CTR_Mode::Encryption aes; + aes.SetKeyWithIV(key.data(), CryptoPP::AES::BLOCKSIZE, iv_ctr); + aes.ProcessData(out, out, size); +} + +} // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_fkcl.cpp b/src/core/hle/service/dlp/dlp_fkcl.cpp index ed247427d..8576d80a3 100644 --- a/src/core/hle/service/dlp/dlp_fkcl.cpp +++ b/src/core/hle/service/dlp/dlp_fkcl.cpp @@ -1,8 +1,9 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" +#include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/service/dlp/dlp_fkcl.h" @@ -10,26 +11,71 @@ SERIALIZE_EXPORT_IMPL(Service::DLP::DLP_FKCL) namespace Service::DLP { -DLP_FKCL::DLP_FKCL() : ServiceFramework("dlp:FKCL", 1) { +std::shared_ptr DLP_FKCL::GetServiceFrameworkSharedPtr() { + return shared_from_this(); +} + +void DLP_FKCL::Initialize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 shared_mem_size = rp.Pop(); + u32 max_beacons = rp.Pop(); + constexpr u32 constant_mem_size = 0; + auto [shared_mem, event] = rp.PopObjects(); + + InitializeCltBase(shared_mem_size, max_beacons, constant_mem_size, shared_mem, event, + String16AsDLPUsername(GetCFG()->GetUsername())); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_FKCL::InitializeWithName(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 shared_mem_size = rp.Pop(); + u32 max_beacons = rp.Pop(); + constexpr u32 constant_mem_size = 0; + auto username = rp.PopRaw>(); + rp.Skip(1, false); // possible null terminator or unk flags + auto [shared_mem, event] = rp.PopObjects(); + + InitializeCltBase(shared_mem_size, max_beacons, constant_mem_size, shared_mem, event, username); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +void DLP_FKCL::Finalize(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + FinalizeCltBase(); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); +} + +DLP_FKCL::DLP_FKCL() + : ServiceFramework("dlp:FKCL", 1), DLP_Clt_Base(Core::System::GetInstance(), "FKCL") { static const FunctionInfo functions[] = { // clang-format off - {0x0001, nullptr, "Initialize"}, - {0x0002, nullptr, "Finalize"}, - {0x0003, nullptr, "GetEventDesc"}, - {0x0004, nullptr, "GetChannels"}, - {0x0005, nullptr, "StartScan"}, - {0x0006, nullptr, "StopScan"}, - {0x0007, nullptr, "GetServerInfo"}, - {0x0008, nullptr, "GetTitleInfo"}, - {0x0009, nullptr, "GetTitleInfoInOrder"}, - {0x000A, nullptr, "DeleteScanInfo"}, - {0x000B, nullptr, "StartFakeSession"}, - {0x000C, nullptr, "GetMyStatus"}, - {0x000D, nullptr, "GetConnectingNodes"}, - {0x000E, nullptr, "GetNodeInfo"}, - {0x000F, nullptr, "GetWirelessRebootPassphrase"}, - {0x0010, nullptr, "StopSession"}, - {0x0011, nullptr, "Initialize2"}, + {0x0001, &DLP_FKCL::Initialize, "Initialize"}, + {0x0002, &DLP_FKCL::Finalize, "Finalize"}, + {0x0003, &DLP_FKCL::GetEventDescription, "GetEventDescription"}, + {0x0004, &DLP_FKCL::GetChannels, "GetChannels"}, + {0x0005, &DLP_FKCL::StartScan, "StartScan"}, + {0x0006, &DLP_FKCL::StopScan, "StopScan"}, + {0x0007, &DLP_FKCL::GetServerInfo, "GetServerInfo"}, + {0x0008, &DLP_FKCL::GetTitleInfo, "GetTitleInfo"}, + {0x0009, &DLP_FKCL::GetTitleInfoInOrder, "GetTitleInfoInOrder"}, + {0x000A, &DLP_FKCL::DeleteScanInfo, "DeleteScanInfo"}, + {0x000B, &DLP_FKCL::StartSession, "StartFakeSession"}, + {0x000C, &DLP_FKCL::GetMyStatus, "GetMyStatus"}, + {0x000D, &DLP_FKCL::GetConnectingNodes, "GetConnectingNodes"}, + {0x000E, &DLP_FKCL::GetNodeInfo, "GetNodeInfo"}, + {0x000F, &DLP_FKCL::GetWirelessRebootPassphrase, "GetWirelessRebootPassphrase"}, + {0x0010, &DLP_FKCL::StopSession, "StopSession"}, + {0x0011, &DLP_FKCL::InitializeWithName, "InitializeWithName"}, // clang-format on }; diff --git a/src/core/hle/service/dlp/dlp_fkcl.h b/src/core/hle/service/dlp/dlp_fkcl.h index c05a77b49..74d41f4ac 100644 --- a/src/core/hle/service/dlp/dlp_fkcl.h +++ b/src/core/hle/service/dlp/dlp_fkcl.h @@ -1,20 +1,31 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "core/hle/service/service.h" +#include "dlp_clt_base.h" namespace Service::DLP { -class DLP_FKCL final : public ServiceFramework { +class DLP_FKCL final : public ServiceFramework, public DLP_Clt_Base { public: DLP_FKCL(); ~DLP_FKCL() = default; + virtual std::shared_ptr GetServiceFrameworkSharedPtr(); + private: SERVICE_SERIALIZATION_SIMPLE + + virtual bool IsFKCL() { + return true; + } + + void Initialize(Kernel::HLERequestContext& ctx); + void Finalize(Kernel::HLERequestContext& ctx); + void InitializeWithName(Kernel::HLERequestContext& ctx); }; } // namespace Service::DLP diff --git a/src/core/hle/service/dlp/dlp_srvr.cpp b/src/core/hle/service/dlp/dlp_srvr.cpp index b82d3e286..65632ca53 100644 --- a/src/core/hle/service/dlp/dlp_srvr.cpp +++ b/src/core/hle/service/dlp/dlp_srvr.cpp @@ -1,30 +1,55 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include "common/archives.h" #include "common/common_types.h" #include "common/logging/log.h" +#include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/result.h" #include "core/hle/service/dlp/dlp_srvr.h" +#include "core/hle/service/fs/fs_user.h" SERIALIZE_EXPORT_IMPL(Service::DLP::DLP_SRVR) namespace Service::DLP { +std::shared_ptr DLP_SRVR::GetServiceFrameworkSharedPtr() { + return shared_from_this(); +} + void DLP_SRVR::IsChild(Kernel::HLERequestContext& ctx) { + auto fs = system.ServiceManager().GetService("fs:USER"); + IPC::RequestParser rp(ctx); - rp.Skip(1, false); + u32 process_id = rp.Pop(); + + bool child; + if (!fs) { + LOG_CRITICAL(Service_DLP, "Could not get direct pointer fs:USER (sm returned null)"); + } + auto title_info = fs->GetProgramLaunchInfo(process_id); + + if (title_info) { + // check if tid corresponds to dlp filter + u32 tid[2]; + memcpy(tid, &title_info->program_id, sizeof(tid)); + LOG_INFO(Service_DLP, "Checked on tid high: {:x} (low {:x})", tid[1], tid[0]); + child = (tid[1] & 0xFFFFC000) == 0x40000 && (tid[1] & 0xFFFF) == 0x1; + } else { // child not found + child = false; + LOG_ERROR(Service_DLP, + "Could not determine program id from process id. (process id not found: {:x})", + process_id); + } IPC::RequestBuilder rb = rp.MakeBuilder(2, 0); rb.Push(ResultSuccess); - rb.Push(false); - - LOG_WARNING(Service_DLP, "(STUBBED) called"); + rb.Push(child); } -DLP_SRVR::DLP_SRVR() : ServiceFramework("dlp:SRVR", 1) { +DLP_SRVR::DLP_SRVR() : ServiceFramework("dlp:SRVR", 1), DLP_Base(Core::System::GetInstance()) { static const FunctionInfo functions[] = { // clang-format off {0x0001, nullptr, "Initialize"}, diff --git a/src/core/hle/service/dlp/dlp_srvr.h b/src/core/hle/service/dlp/dlp_srvr.h index 625740d2f..d9b4fdd42 100644 --- a/src/core/hle/service/dlp/dlp_srvr.h +++ b/src/core/hle/service/dlp/dlp_srvr.h @@ -1,18 +1,24 @@ -// Copyright 2016 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #pragma once #include "core/hle/service/service.h" +#include "dlp_base.h" namespace Service::DLP { -class DLP_SRVR final : public ServiceFramework { +class DLP_SRVR final : public ServiceFramework, public DLP_Base { public: DLP_SRVR(); ~DLP_SRVR() = default; + virtual std::shared_ptr GetServiceFrameworkSharedPtr(); + virtual bool IsHost() { + return true; + } + private: void IsChild(Kernel::HLERequestContext& ctx); diff --git a/src/core/hle/service/http/http_c.cpp b/src/core/hle/service/http/http_c.cpp index 3a1dc1410..b1f47373b 100644 --- a/src/core/hle/service/http/http_c.cpp +++ b/src/core/hle/service/http/http_c.cpp @@ -1773,9 +1773,8 @@ void HTTP_C::SetClientCertContext(Kernel::HLERequestContext& ctx) { void HTTP_C::GetSSLError(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); const u32 context_handle = rp.Pop(); - const u32 unk = rp.Pop(); - LOG_WARNING(Service_HTTP, "(STUBBED) called, context_handle={}, unk={}", context_handle, unk); + LOG_WARNING(Service_HTTP, "(STUBBED) called, context_handle={}", context_handle); [[maybe_unused]] Context& http_context = GetContext(context_handle); diff --git a/src/core/hle/service/nwm/nwm_uds.cpp b/src/core/hle/service/nwm/nwm_uds.cpp index 43547ee42..fadbe3f3e 100644 --- a/src/core/hle/service/nwm/nwm_uds.cpp +++ b/src/core/hle/service/nwm/nwm_uds.cpp @@ -226,7 +226,7 @@ void NWM_UDS::HandleEAPoLPacket(const Network::WifiPacket& packet) { auto node = DeserializeNodeInfo(eapol_start.node); - if (eapol_start.conn_type == ConnectionType::Client) { + if (eapol_start.connection_type == ConnectionType::Client) { // Get an unused network node id u16 node_id = GetNextAvailableNodeId(); node.network_node_id = node_id; @@ -244,13 +244,13 @@ void NWM_UDS::HandleEAPoLPacket(const Network::WifiPacket& packet) { node_map[packet.transmitter_address].spec = false; BroadcastNodeMap(); - } else if (eapol_start.conn_type == ConnectionType::Spectator) { + } else if (eapol_start.connection_type == ConnectionType::Spectator) { node_map[packet.transmitter_address].node_id = NodeIDSpec; node_map[packet.transmitter_address].connected = true; node_map[packet.transmitter_address].spec = true; } else { LOG_ERROR(Service_NWM, "Client tried connecting with unknown connection type: 0x{:x}", - static_cast(eapol_start.conn_type)); + static_cast(eapol_start.connection_type)); } // Send the EAPoL-Logoff packet. @@ -595,9 +595,7 @@ boost::optional NWM_UDS::GetNodeMacAddress(u16 dest_node_id return destination->first; } -void NWM_UDS::Shutdown(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - +void NWM_UDS::ShutdownHLE() { initialized = false; for (auto& bind_node : channel_data) { @@ -607,12 +605,92 @@ void NWM_UDS::Shutdown(Kernel::HLERequestContext& ctx) { node_map.clear(); recv_buffer_memory.reset(); +} + +void NWM_UDS::Shutdown(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + ShutdownHLE(); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); LOG_DEBUG(Service_NWM, "called"); } +void NWM_UDS::RecvBeaconBroadcastData(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 out_buffer_size = rp.Pop(); + + // scan input struct + u32 unk1 = rp.Pop(); + u32 unk2 = rp.Pop(); + + MacAddress mac_address; + rp.PopRaw(mac_address); + + // uninitialized data in scan input struct + rp.Skip(9, false); + + // end scan input struct + + u32 wlan_comm_id = rp.Pop(); + u32 id = rp.Pop(); + // From 3dbrew: + // 'Official user processes create a new event handle which is then passed to this command. + // However, those user processes don't save that handle anywhere afterwards.' + // So we don't save/use that event too. + std::shared_ptr input_event = rp.PopObject(); + + Kernel::MappedBuffer& out_buffer = rp.PopMappedBuffer(); + ASSERT(out_buffer.GetSize() == out_buffer_size); + + std::size_t cur_buffer_size = sizeof(BeaconDataReplyHeader); + + auto beacons = GetReceivedBeacons(mac_address); + + BeaconDataReplyHeader data_reply_header{}; + data_reply_header.total_entries = static_cast(beacons.size()); + data_reply_header.max_output_size = out_buffer_size; + + // Write each of the received beacons into the buffer + for (const auto& beacon : beacons) { + BeaconEntryHeader entry{}; + // TODO(Subv): Figure out what this size is used for. + entry.unk_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); + entry.total_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); + entry.wifi_channel = beacon.channel; + entry.header_size = sizeof(BeaconEntryHeader); + entry.mac_address = beacon.transmitter_address; + + ASSERT(cur_buffer_size < out_buffer_size); + + out_buffer.Write(&entry, cur_buffer_size, sizeof(BeaconEntryHeader)); + cur_buffer_size += sizeof(BeaconEntryHeader); + const unsigned char* beacon_data = beacon.data.data(); + out_buffer.Write(beacon_data, cur_buffer_size, beacon.data.size()); + cur_buffer_size += beacon.data.size(); + } + + // Update the total size in the structure and write it to the buffer again. + data_reply_header.total_size = static_cast(cur_buffer_size); + out_buffer.Write(&data_reply_header, 0, sizeof(BeaconDataReplyHeader)); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); + rb.Push(ResultSuccess); + rb.PushMappedBuffer(out_buffer); + + // on a real 3ds this is about 0.38 seconds + static constexpr std::chrono::nanoseconds UDSBeaconScanInterval{300000000}; + + ctx.SleepClientThread("uds::RecvBeaconBroadcastData", UDSBeaconScanInterval, nullptr); + + LOG_DEBUG(Service_NWM, + "called out_buffer_size=0x{:08X}, wlan_comm_id=0x{:08X}, id=0x{:08X}," + "unk1=0x{:08X}, unk2=0x{:08X}, offset={}", + out_buffer_size, wlan_comm_id, id, unk1, unk2, cur_buffer_size); +} + ResultVal> NWM_UDS::Initialize( u32 sharedmem_size, const NodeInfo& node, u16 version, std::shared_ptr sharedmem) { @@ -671,25 +749,41 @@ void NWM_UDS::InitializeDeprecated(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NWM, "called sharedmem_size=0x{:08X}", sharedmem_size); } +ConnectionStatus NWM_UDS::GetConnectionStatusHLE() { + std::scoped_lock lock(connection_status_mutex); + ConnectionStatus cs_out = connection_status; + + // Reset the bitmask of changed nodes after each call to this + // function to prevent falsely informing games of outstanding + // changes in subsequent calls. + // TODO(Subv): Find exactly where the NWM module resets this value. + connection_status.changed_nodes = 0; + + return cs_out; +} + void NWM_UDS::GetConnectionStatus(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); IPC::RequestBuilder rb = rp.MakeBuilder(13, 0); rb.Push(ResultSuccess); - { - std::scoped_lock lock(connection_status_mutex); - rb.PushRaw(connection_status); - - // Reset the bitmask of changed nodes after each call to this - // function to prevent falsely informing games of outstanding - // changes in subsequent calls. - // TODO(Subv): Find exactly where the NWM module resets this value. - connection_status.changed_nodes = 0; - } + rb.PushRaw(GetConnectionStatusHLE()); LOG_DEBUG(Service_NWM, "called"); } +std::unique_ptr NWM_UDS::GetNodeInformationHLE(u16 network_node_id) { + std::scoped_lock lock(connection_status_mutex); + auto itr = + std::find_if(node_info.begin(), node_info.end(), [network_node_id](const NodeInfo& node) { + return node.network_node_id == network_node_id; + }); + if (itr == node_info.end()) { + return nullptr; + } + return std::make_unique(*itr); +} + void NWM_UDS::GetNodeInformation(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); u16 network_node_id = rp.Pop(); @@ -702,12 +796,8 @@ void NWM_UDS::GetNodeInformation(Kernel::HLERequestContext& ctx) { } { - std::scoped_lock lock(connection_status_mutex); - auto itr = std::find_if(node_info.begin(), node_info.end(), - [network_node_id](const NodeInfo& node) { - return node.network_node_id == network_node_id; - }); - if (itr == node_info.end()) { + auto node = GetNodeInformationHLE(network_node_id); + if (!node) { IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, ErrorLevel::Status)); @@ -716,46 +806,31 @@ void NWM_UDS::GetNodeInformation(Kernel::HLERequestContext& ctx) { IPC::RequestBuilder rb = rp.MakeBuilder(11, 0); rb.Push(ResultSuccess); - rb.PushRaw(*itr); + rb.PushRaw(*node); } LOG_DEBUG(Service_NWM, "called"); } -void NWM_UDS::Bind(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); - - u32 bind_node_id = rp.Pop(); - u32 recv_buffer_size = rp.Pop(); - u8 data_channel = rp.Pop(); - u16 network_node_id = rp.Pop(); - - LOG_DEBUG(Service_NWM, "called"); - +std::pair> NWM_UDS::BindHLE(u32 bind_node_id, + u32 recv_buffer_size, + u8 data_channel, + u16 network_node_id) { if (data_channel == 0 || bind_node_id == 0) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::WrongArgument, ErrorLevel::Usage)); LOG_WARNING(Service_NWM, "data_channel = {}, bind_node_id = {}", data_channel, bind_node_id); - return; + return std::make_pair(ResultStatus::BindError_ArgsZero, nullptr); } constexpr std::size_t MaxBindNodes = 16; if (channel_data.size() >= MaxBindNodes) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::OutOfMemory, ErrorModule::UDS, ErrorSummary::OutOfResource, - ErrorLevel::Status)); LOG_WARNING(Service_NWM, "max bind nodes"); - return; + return std::make_pair(ResultStatus::BindError_MaxBinds, nullptr); } constexpr u32 MinRecvBufferSize = 0x5F4; if (recv_buffer_size < MinRecvBufferSize) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Usage)); LOG_WARNING(Service_NWM, "MinRecvBufferSize"); - return; + return std::make_pair(ResultStatus::BindError_RecvBufferTooLarge, nullptr); } // Create a new event for this bind node. @@ -766,12 +841,61 @@ void NWM_UDS::Bind(Kernel::HLERequestContext& ctx) { ASSERT(channel_data.find(data_channel) == channel_data.end()); // TODO(B3N30): Support more than one bind node per channel. channel_data[data_channel] = {bind_node_id, data_channel, network_node_id, event}; + return std::make_pair(ResultStatus::ResultSuccess, std::move(event)); +} + +void NWM_UDS::Bind(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + u32 bind_node_id = rp.Pop(); + u32 recv_buffer_size = rp.Pop(); + u8 data_channel = rp.Pop(); + u16 network_node_id = rp.Pop(); + + auto [ret, event] = BindHLE(bind_node_id, recv_buffer_size, data_channel, network_node_id); + + switch (ret) { + case ResultStatus::BindError_ArgsZero: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::WrongArgument, ErrorLevel::Usage)); + return; + } + case ResultStatus::BindError_MaxBinds: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::OutOfMemory, ErrorModule::UDS, ErrorSummary::OutOfResource, + ErrorLevel::Status)); + return; + } + case ResultStatus::BindError_RecvBufferTooLarge: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Usage)); + return; + } + default:; + } IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); rb.Push(ResultSuccess); rb.PushCopyObjects(event); } +void NWM_UDS::UnbindHLE(u32 bind_node_id) { + std::scoped_lock lock(connection_status_mutex); + + auto itr = + std::find_if(channel_data.begin(), channel_data.end(), [bind_node_id](const auto& data) { + return data.second.bind_node_id == bind_node_id; + }); + + if (itr != channel_data.end()) { + // TODO(B3N30): Check out what Unbind does if the bind_node_id wasn't in the map + itr->second.event->Signal(); + channel_data.erase(itr); + } +} + void NWM_UDS::Unbind(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -783,18 +907,7 @@ void NWM_UDS::Unbind(Kernel::HLERequestContext& ctx) { return; } - std::scoped_lock lock(connection_status_mutex); - - auto itr = - std::find_if(channel_data.begin(), channel_data.end(), [bind_node_id](const auto& data) { - return data.second.bind_node_id == bind_node_id; - }); - - if (itr != channel_data.end()) { - // TODO(B3N30): Check out what Unbind does if the bind_node_id wasn't in the map - itr->second.event->Signal(); - channel_data.erase(itr); - } + UnbindHLE(bind_node_id); IPC::RequestBuilder rb = rp.MakeBuilder(5, 0); rb.Push(ResultSuccess); @@ -959,10 +1072,22 @@ void NWM_UDS::EjectClient(Kernel::HLERequestContext& ctx) { } } +Result NWM_UDS::UpdateNetworkAttributeHLE(u16 node_bitmask, u8 flag) { + [[maybe_unused]] constexpr u8 flag_disconnect_and_block_non_bitmasked_nodes = 0x1; + + // stubbed + + return ResultSuccess; +} + void NWM_UDS::UpdateNetworkAttribute(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); - rp.Skip(2, false); - LOG_WARNING(Service_NWM, "stubbed"); + + u16 bitmask = rp.Pop(); + u8 flag = rp.Pop(); + + auto res = UpdateNetworkAttributeHLE(bitmask, flag); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); rb.Push(ResultSuccess); } @@ -1005,52 +1130,6 @@ void NWM_UDS::DestroyNetwork(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NWM, "called"); } -void NWM_UDS::DisconnectNetwork(Kernel::HLERequestContext& ctx) { - LOG_DEBUG(Service_NWM, "disconnecting from network"); - IPC::RequestParser rp(ctx); - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - - using Network::WifiPacket; - WifiPacket deauth; - { - std::scoped_lock lock(connection_status_mutex); - if (connection_status.status == NetworkStatus::ConnectedAsHost) { - // A real 3ds makes strange things here. We do the same - u16_le tmp_node_id = connection_status.network_node_id; - connection_status = {}; - connection_status.status = NetworkStatus::ConnectedAsHost; - connection_status.network_node_id = tmp_node_id; - node_map.clear(); - LOG_DEBUG(Service_NWM, "called as a host"); - rb.Push(Result(ErrCodes::WrongStatus, ErrorModule::UDS, ErrorSummary::InvalidState, - ErrorLevel::Status)); - return; - } - u16_le tmp_node_id = connection_status.network_node_id; - connection_status = {}; - connection_status.status = NetworkStatus::NotConnected; - connection_status.network_node_id = tmp_node_id; - node_map.clear(); - connection_status_event->Signal(); - - deauth.channel = network_channel; - // TODO(B3N30): Add disconnect reason - deauth.data = {}; - deauth.destination_address = network_info.host_mac_address; - deauth.type = WifiPacket::PacketType::Deauthentication; - } - - SendPacket(deauth); - - for (auto& bind_node : channel_data) { - bind_node.second.event->Signal(); - } - channel_data.clear(); - - rb.Push(ResultSuccess); - LOG_DEBUG(Service_NWM, "called"); -} - void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -1062,32 +1141,54 @@ void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { u8 flags = rp.Pop(); std::vector input_buffer = rp.PopStaticBuffer(); - ASSERT(input_buffer.size() >= data_size); - input_buffer.resize(data_size); IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + auto res = SendToHLE(dest_node_id, data_channel, data_size, flags, input_buffer); + + switch (res) { + case ResultStatus::SendError_PacketSizeTooLarge: + rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Usage)); + return; + case ResultStatus::SendError_NotConnected: + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::InvalidState, ErrorLevel::Status)); + return; + case ResultStatus::SendError_BadNode: + case ResultStatus::SendError_BadMacAddress: + rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Status)); + return; + default:; + } + + rb.Push(ResultSuccess); +} + +ResultStatus NWM_UDS::SendToHLE(u32 dest_node_id, u8 data_channel, u32 data_size, u8 flags, + std::vector input_buffer) { + ASSERT(input_buffer.size() >= data_size); + input_buffer.resize(data_size); + std::scoped_lock lock(connection_status_mutex); if (connection_status.status != NetworkStatus::ConnectedAsClient && connection_status.status != NetworkStatus::ConnectedAsHost) { - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::InvalidState, ErrorLevel::Status)); - return; + LOG_ERROR(Service_NWM, + "You are not connected as a client or a host. (you are connected as type {})", + connection_status.status); + return ResultStatus::SendError_NotConnected; } // There should never be a dest_node_id of 0 if (dest_node_id == 0) { - rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Status)); LOG_ERROR(Service_NWM, "dest_node_id is 0"); - return; + return ResultStatus::SendError_BadNode; } if (dest_node_id == connection_status.network_node_id) { LOG_ERROR(Service_NWM, "tried to send packet to itself"); - rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Status)); - return; + return ResultStatus::SendError_BadNode; } if (flags >> 2) { @@ -1096,18 +1197,16 @@ void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { auto dest_address = GetNodeMacAddress(dest_node_id, flags); if (!dest_address) { - rb.Push(Result(ErrorDescription::NotFound, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Status)); - return; + LOG_ERROR(Service_NWM, "Destination address was 0"); + return ResultStatus::SendError_BadMacAddress; } constexpr std::size_t MaxSize = 0x5C6; if (data_size > MaxSize) { - rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Usage)); - return; + LOG_ERROR(Service_NWM, "Data size was greater than the max packet size {} > {}", data_size, + MaxSize); + return ResultStatus::SendError_PacketSizeTooLarge; } - // TODO(B3N30): Increment the sequence number after each sent packet. u16 sequence_number = 0; std::vector data_payload = @@ -1126,7 +1225,7 @@ void NWM_UDS::SendTo(Kernel::HLERequestContext& ctx) { SendPacket(packet); - rb.Push(ResultSuccess); + return ResultStatus::ResultSuccess; } void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { @@ -1135,7 +1234,47 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { u32 bind_node_id = rp.Pop(); u32 max_out_buff_size_aligned = rp.Pop(); u32 max_out_buff_size = rp.Pop(); + std::vector output_buffer; + SecureDataHeader secure_data; + + auto ret = PullPacketHLE(bind_node_id, max_out_buff_size, max_out_buff_size_aligned, + output_buffer, &secure_data); + + switch (ret.error()) { + case ResultStatus::RecvError_NotConnected: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::InvalidState, ErrorLevel::Status)); + return; + } + case ResultStatus::RecvError_BadNode: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, + ErrorSummary::InvalidState, ErrorLevel::Status)); + return; + } + case ResultStatus::RecvError_PacketSizeTooLarge: { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, + ErrorLevel::Usage)); + return; + } + default:; + } + + IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); + + rb.Push(ResultSuccess); + rb.Push(*ret); // return is data size if gt/eq to 0 + rb.Push(secure_data.src_node_id); + rb.PushStaticBuffer(std::move(output_buffer), 0); +} + +Common::Expected NWM_UDS::PullPacketHLE(u32 bind_node_id, u32 max_out_buff_size, + u32 max_out_buff_size_aligned, + std::vector& output_buffer, + void* secure_data_out) { // This size is hard coded into the uds module. We don't know the meaning yet. u32 buff_size = std::min(max_out_buff_size_aligned, 0x172) << 2; @@ -1143,10 +1282,8 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { if (connection_status.status != NetworkStatus::ConnectedAsHost && connection_status.status != NetworkStatus::ConnectedAsClient && connection_status.status != NetworkStatus::ConnectedAsSpectator) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::InvalidState, ErrorLevel::Status)); - return; + LOG_ERROR(Service_NWM, "Not connected yet."); + return Common::Unexpected(ResultStatus::RecvError_NotConnected); } auto channel = @@ -1155,20 +1292,13 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { }); if (channel == channel_data.end()) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::NotAuthorized, ErrorModule::UDS, - ErrorSummary::WrongArgument, ErrorLevel::Usage)); - return; + LOG_ERROR(Service_NWM, "Could not find channel bn 0x{:x}.", bind_node_id); + return Common::Unexpected(ResultStatus::RecvError_BadNode); } if (channel->second.received_packets.empty()) { - std::vector output_buffer(buff_size); - IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); - rb.Push(ResultSuccess); - rb.Push(0); - rb.Push(0); - rb.PushStaticBuffer(std::move(output_buffer), 0); - return; + output_buffer.resize(buff_size); + return int(0); } const auto& next_packet = channel->second.received_packets.front(); @@ -1176,26 +1306,22 @@ void NWM_UDS::PullPacket(Kernel::HLERequestContext& ctx) { auto secure_data = ParseSecureDataHeader(next_packet); auto data_size = secure_data.GetActualDataSize(); - if (data_size > max_out_buff_size) { - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(Result(ErrorDescription::TooLarge, ErrorModule::UDS, ErrorSummary::WrongArgument, - ErrorLevel::Usage)); - return; + if (secure_data_out) { + *reinterpret_cast(secure_data_out) = secure_data; } - IPC::RequestBuilder rb = rp.MakeBuilder(3, 2); + if (data_size > max_out_buff_size) { + LOG_ERROR(Service_NWM, "Data size was too large."); + return Common::Unexpected(ResultStatus::RecvError_PacketSizeTooLarge); + } + output_buffer.resize(buff_size); - std::vector output_buffer(buff_size); // Write the actual data. std::memcpy(output_buffer.data(), next_packet.data() + sizeof(LLCHeader) + sizeof(SecureDataHeader), data_size); - rb.Push(ResultSuccess); - rb.Push(data_size); - rb.Push(secure_data.src_node_id); - rb.PushStaticBuffer(std::move(output_buffer), 0); - channel->second.received_packets.pop_front(); + return int(data_size); } void NWM_UDS::GetChannel(Kernel::HLERequestContext& ctx) { @@ -1219,8 +1345,13 @@ public: void WakeUp(std::shared_ptr thread, Kernel::HLERequestContext& ctx, Kernel::ThreadWakeupReason reason) { - // TODO(B3N30): Add error handling for host full and timeout IPC::RequestBuilder rb(ctx, command_id, 1, 0); + if (reason == Kernel::ThreadWakeupReason::Timeout) { + LOG_ERROR(Service_NWM, "timed out when trying to connect to UDS server"); + rb.Push(Result(ErrorDescription::Timeout, ErrorModule::UDS, ErrorSummary::Canceled, + ErrorLevel::Status)); + return; + } rb.Push(ResultSuccess); LOG_DEBUG(Service_NWM, "connection sequence finished"); } @@ -1237,95 +1368,25 @@ private: friend class boost::serialization::access; }; -void NWM_UDS::RecvBeaconBroadcastData(Kernel::HLERequestContext& ctx) { - IPC::RequestParser rp(ctx); +void NWM_UDS::ConnectToNetworkHLE(NetworkInfo net_info, u8 connection_type, + std::vector passphrase) { + network_info = net_info; - u32 out_buffer_size = rp.Pop(); + conn_type = static_cast(connection_type); - // scan input struct - u32 unk1 = rp.Pop(); - u32 unk2 = rp.Pop(); - - MacAddress mac_address; - rp.PopRaw(mac_address); - - // uninitialized data in scan input struct - rp.Skip(9, false); - - // end scan input struct - - u32 wlan_comm_id = rp.Pop(); - u32 id = rp.Pop(); - // From 3dbrew: - // 'Official user processes create a new event handle which is then passed to this command. - // However, those user processes don't save that handle anywhere afterwards.' - // So we don't save/use that event too. - std::shared_ptr input_event = rp.PopObject(); - - Kernel::MappedBuffer out_buffer = rp.PopMappedBuffer(); - ASSERT(out_buffer.GetSize() == out_buffer_size); - - std::size_t cur_buffer_size = sizeof(BeaconDataReplyHeader); - - // on a real 3ds this is about 0.38 seconds - static constexpr std::chrono::nanoseconds UDSBeaconScanInterval{300000000}; - - ctx.SleepClientThread("uds::RecvBeaconBroadcastData", UDSBeaconScanInterval, - std::make_shared(0xF)); - - // Retrieve all beacon frames that were received from the desired mac address. - auto beacons = GetReceivedBeacons(mac_address); - - BeaconDataReplyHeader data_reply_header{}; - data_reply_header.total_entries = static_cast(beacons.size()); - data_reply_header.max_output_size = out_buffer_size; - - // Write each of the received beacons into the buffer - for (const auto& beacon : beacons) { - BeaconEntryHeader entry{}; - // TODO(Subv): Figure out what this size is used for. - entry.unk_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); - entry.total_size = static_cast(sizeof(BeaconEntryHeader) + beacon.data.size()); - entry.wifi_channel = beacon.channel; - entry.header_size = sizeof(BeaconEntryHeader); - entry.mac_address = beacon.transmitter_address; - - ASSERT(cur_buffer_size < out_buffer_size); - - out_buffer.Write(&entry, cur_buffer_size, sizeof(BeaconEntryHeader)); - cur_buffer_size += sizeof(BeaconEntryHeader); - const unsigned char* beacon_data = beacon.data.data(); - out_buffer.Write(beacon_data, cur_buffer_size, beacon.data.size()); - cur_buffer_size += beacon.data.size(); - } - - // Update the total size in the structure and write it to the buffer again. - data_reply_header.total_size = static_cast(cur_buffer_size); - out_buffer.Write(&data_reply_header, 0, sizeof(BeaconDataReplyHeader)); - - IPC::RequestBuilder rb = rp.MakeBuilder(1, 2); - rb.Push(ResultSuccess); - rb.PushMappedBuffer(out_buffer); - - LOG_DEBUG(Service_NWM, - "called out_buffer_size=0x{:08X}, wlan_comm_id=0x{:08X}, id=0x{:08X}," - "unk1=0x{:08X}, unk2=0x{:08X}, offset={}", - out_buffer_size, wlan_comm_id, id, unk1, unk2, cur_buffer_size); + // Start the connection sequence + StartConnectionSequence(network_info.host_mac_address); } void NWM_UDS::ConnectToNetwork(Kernel::HLERequestContext& ctx, u16 command_id, std::span network_info_buffer, u8 connection_type, std::vector passphrase) { - network_info = {}; - std::memcpy(&network_info, network_info_buffer.data(), network_info_buffer.size()); - conn_type = static_cast(connection_type); - - // Start the connection sequence - StartConnectionSequence(network_info.host_mac_address); - - // 300 ms + NetworkInfo net_info; + std::memcpy(&net_info, network_info_buffer.data(), network_info_buffer.size()); + ConnectToNetworkHLE(net_info, connection_type, passphrase); + // Originally 300 ms, but was changed to 5s to accommodate high ping // Since this timing is handled by core_timing it could differ from the 'real world' time - static constexpr std::chrono::nanoseconds UDSConnectionTimeout{300000000}; + static constexpr std::chrono::nanoseconds UDSConnectionTimeout{5000000000}; connection_event = ctx.SleepClientThread("uds::ConnectToNetwork", UDSConnectionTimeout, std::make_shared(command_id)); @@ -1364,6 +1425,60 @@ void NWM_UDS::ConnectToNetworkDeprecated(Kernel::HLERequestContext& ctx) { LOG_DEBUG(Service_NWM, "called"); } +ResultStatus NWM_UDS::DisconnectNetworkHLE() { + using Network::WifiPacket; + WifiPacket deauth; + { + std::scoped_lock lock(connection_status_mutex); + if (connection_status.status == NetworkStatus::ConnectedAsHost) { + // A real 3ds makes strange things here. We do the same + u16_le tmp_node_id = connection_status.network_node_id; + connection_status = {}; + connection_status.status = NetworkStatus::ConnectedAsHost; + connection_status.network_node_id = tmp_node_id; + node_map.clear(); + return ResultStatus::DisconError_CalledAsHost; + } + u16_le tmp_node_id = connection_status.network_node_id; + connection_status = {}; + connection_status.status = NetworkStatus::NotConnected; + connection_status.network_node_id = tmp_node_id; + node_map.clear(); + connection_status_event->Signal(); + + deauth.channel = network_channel; + // TODO(B3N30): Add disconnect reason + deauth.data = {}; + deauth.destination_address = network_info.host_mac_address; + deauth.type = WifiPacket::PacketType::Deauthentication; + } + + SendPacket(deauth); + + for (auto& bind_node : channel_data) { + bind_node.second.event->Signal(); + } + channel_data.clear(); + + return ResultStatus::ResultSuccess; +} + +void NWM_UDS::DisconnectNetwork(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_NWM, "disconnecting from network"); + IPC::RequestParser rp(ctx); + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + auto res = DisconnectNetworkHLE(); + if (res == ResultStatus::DisconError_CalledAsHost) { + LOG_DEBUG(Service_NWM, "called as a host"); + rb.Push(Result(ErrCodes::WrongStatus, ErrorModule::UDS, ErrorSummary::InvalidState, + ErrorLevel::Status)); + return; + } + + rb.Push(ResultSuccess); +} + void NWM_UDS::SetApplicationData(Kernel::HLERequestContext& ctx) { IPC::RequestParser rp(ctx); @@ -1473,6 +1588,16 @@ void NWM_UDS::DecryptBeaconData(Kernel::HLERequestContext& ctx) { rb.PushStaticBuffer(std::move(output_buffer), 0); } +void NWM_UDS::EjectSpectators(Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp(ctx); + + LOG_WARNING(Service_NWM, "(STUBBED) called"); + + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + + rb.Push(ResultSuccess); +} + // Sends a 802.11 beacon frame with information about the current network. void NWM_UDS::BeaconBroadcastCallback(std::uintptr_t user_data, s64 cycles_late) { // Don't do anything if we're not actually hosting a network @@ -1504,7 +1629,7 @@ NWM_UDS::NWM_UDS(Core::System& system) : ServiceFramework("nwm::UDS"), system(sy {0x0003, &NWM_UDS::Shutdown, "Shutdown"}, {0x0004, &NWM_UDS::BeginHostingNetworkDeprecated, "BeginHostingNetwork (deprecated)"}, {0x0005, &NWM_UDS::EjectClient, "EjectClient"}, - {0x0006, nullptr, "EjectSpectator"}, + {0x0006, &NWM_UDS::EjectSpectators, "EjectSpectators"}, {0x0007, &NWM_UDS::UpdateNetworkAttribute, "UpdateNetworkAttribute"}, {0x0008, &NWM_UDS::DestroyNetwork, "DestroyNetwork"}, {0x0009, &NWM_UDS::ConnectToNetworkDeprecated, "ConnectToNetwork (deprecated)"}, @@ -1542,8 +1667,7 @@ NWM_UDS::NWM_UDS(Core::System& system) : ServiceFramework("nwm::UDS"), system(sy MacAddress mac; - auto cfg = system.ServiceManager().GetService("cfg:u"); - if (cfg.get()) { + if (auto cfg = system.ServiceManager().GetService("cfg:u")) { auto cfg_module = cfg->GetModule(); mac = Service::CFG::MacToArray(cfg_module->GetMacAddress()); } diff --git a/src/core/hle/service/nwm/nwm_uds.h b/src/core/hle/service/nwm/nwm_uds.h index 045d4ba96..feefe8a86 100644 --- a/src/core/hle/service/nwm/nwm_uds.h +++ b/src/core/hle/service/nwm/nwm_uds.h @@ -31,10 +31,31 @@ class Event; class SharedMemory; } // namespace Kernel +namespace Service::DLP { +class DLP_Base; +class DLP_Clt_Base; +class DLP_SRVR; +} // namespace Service::DLP + // Local-WLAN service namespace Service::NWM { +enum class ResultStatus { + ResultSuccess = 0, + BindError_ArgsZero, + BindError_MaxBinds, + BindError_RecvBufferTooLarge, + DisconError_CalledAsHost, + SendError_NotConnected, + SendError_BadNode, + SendError_BadMacAddress, + SendError_PacketSizeTooLarge, + RecvError_NotConnected, + RecvError_BadNode, + RecvError_PacketSizeTooLarge, +}; + using MacAddress = std::array; const std::size_t ApplicationDataSize = 0xC8; @@ -429,6 +450,16 @@ private: */ void EjectClient(Kernel::HLERequestContext& ctx); + /** + * NWM_UDS::EjectSpectators Disconnects all spectators and prevents them from rejoining. + * Inputs: + * 0 : Command header + * Outputs: + * 0 : Return header + * 1 : Result of function, 0 on success, otherwise error code + */ + void EjectSpectators(Kernel::HLERequestContext& ctx); + /** * NWM_UDS::DecryptBeaconData service function. * Decrypts the encrypted data tags contained in the 802.11 beacons. @@ -452,12 +483,31 @@ private: u32 sharedmem_size, const NodeInfo& node, u16 version, std::shared_ptr sharedmem); + void ShutdownHLE(); + Common::Expected PullPacketHLE(u32 bind_node_id, u32 max_out_buff_size, + u32 max_out_buff_size_aligned, + std::vector& output_buffer, + void* secure_data_out); + ConnectionStatus GetConnectionStatusHLE(); + ResultStatus DisconnectNetworkHLE(); + std::pair> BindHLE(u32 bind_node_id, + u32 recv_buffer_size, + u8 data_channel, + u16 network_node_id); + void UnbindHLE(u32 bind_node_id); + std::unique_ptr GetNodeInformationHLE(u16 network_node_id); + ResultStatus SendToHLE(u32 dest_node_id, u8 data_channel, u32 data_size, u8 flags, + std::vector input_buffer); + Result UpdateNetworkAttributeHLE(u16 bitmask, u8 flag); + Result BeginHostingNetwork(std::span network_info_buffer, std::vector passphrase); void ConnectToNetwork(Kernel::HLERequestContext& ctx, u16 command_id, std::span network_info_buffer, u8 connection_type, std::vector passphrase); + void ConnectToNetworkHLE(NetworkInfo net_info, u8 connection_type, std::vector passphrase); + void BeaconBroadcastCallback(std::uintptr_t user_data, s64 cycles_late); /** @@ -576,7 +626,7 @@ private: // Mutex to synchronize access to the connection status between the emulation thread and the // network thread. - std::mutex connection_status_mutex; + std::recursive_mutex connection_status_mutex; std::shared_ptr connection_event; @@ -590,6 +640,9 @@ private: template void serialize(Archive& ar, const unsigned int); friend class boost::serialization::access; + friend class Service::DLP::DLP_Base; + friend class Service::DLP::DLP_Clt_Base; + friend class Service::DLP::DLP_SRVR; }; } // namespace Service::NWM diff --git a/src/core/hle/service/nwm/uds_data.cpp b/src/core/hle/service/nwm/uds_data.cpp index 305aec843..1606d0286 100644 --- a/src/core/hle/service/nwm/uds_data.cpp +++ b/src/core/hle/service/nwm/uds_data.cpp @@ -290,7 +290,7 @@ std::vector GenerateEAPoLStartFrame(u16 association_id, ConnectionType conn_ const NodeInfo& node_info) { EAPoLStartPacket eapol_start{}; eapol_start.association_id = association_id; - eapol_start.conn_type = conn_type; + eapol_start.connection_type = conn_type; eapol_start.node.friend_code_seed = node_info.friend_code_seed; std::copy(node_info.username.begin(), node_info.username.end(), diff --git a/src/core/hle/service/nwm/uds_data.h b/src/core/hle/service/nwm/uds_data.h index faaf53448..50dbda15a 100644 --- a/src/core/hle/service/nwm/uds_data.h +++ b/src/core/hle/service/nwm/uds_data.h @@ -91,7 +91,7 @@ constexpr u16 EAPoLStartMagic = 0x201; struct EAPoLStartPacket { u16_be magic = EAPoLStartMagic; u16_be association_id; - enum_le conn_type; + enum_le connection_type; INSERT_PADDING_BYTES(3); EAPoLNodeInfo node; }; diff --git a/src/core/hw/aes/key.cpp b/src/core/hw/aes/key.cpp index 776eab670..2e46de838 100644 --- a/src/core/hw/aes/key.cpp +++ b/src/core/hw/aes/key.cpp @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -133,6 +133,9 @@ AESIV nfc_iv; AESKey otp_key; AESIV otp_iv; +// gets xor'd with the mac address to produce the final iv +AESIV dlp_checksum_mod_iv; + KeySlot movable_key; KeySlot movable_cmac; @@ -249,6 +252,11 @@ void LoadPresetKeys() { continue; } + if (name == "dlpChecksumModIv") { + dlp_checksum_mod_iv = key; + continue; + } + const auto key_slot = ParseKeySlotName(name); if (!key_slot) { LOG_ERROR(HW_AES, "Invalid key name '{}'", name); @@ -371,4 +379,8 @@ const AESKey& GetMovableKey(bool cmac_key) { return cmac_key ? movable_cmac.normal.value() : movable_key.normal.value(); } +const AESIV& GetDlpChecksumModIv() { + return dlp_checksum_mod_iv; +} + } // namespace HW::AES diff --git a/src/core/hw/aes/key.h b/src/core/hw/aes/key.h index c9d0d4f09..2920e7b86 100644 --- a/src/core/hw/aes/key.h +++ b/src/core/hw/aes/key.h @@ -1,4 +1,4 @@ -// Copyright 2017 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -102,4 +102,6 @@ std::pair GetOTPKeyIV(); const AESKey& GetMovableKey(bool cmac_key); +const AESIV& GetDlpChecksumModIv(); + } // namespace HW::AES diff --git a/src/core/hw/default_keys.h b/src/core/hw/default_keys.h index 019d5cfd0..554b74ae1 100644 --- a/src/core/hw/default_keys.h +++ b/src/core/hw/default_keys.h @@ -1,8 +1,8 @@ -/* Generated by bin2c, do not edit manually */ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. -/* Contents of file keys.enc */ -const long int default_keys_enc_size = 7360; -const unsigned char default_keys_enc[7360] = { +unsigned char default_keys_enc[] = { 0x4E, 0x81, 0xE9, 0x54, 0xCC, 0xDE, 0xFD, 0x56, 0x7D, 0xD2, 0x72, 0xE6, 0xD9, 0xCD, 0x8E, 0x11, 0xE1, 0x7F, 0x74, 0xF4, 0xFC, 0x54, 0xA6, 0xA4, 0x27, 0xC2, 0xD7, 0x50, 0xEA, 0xE7, 0xBE, 0xC9, 0xA7, 0x5E, 0xE0, 0x2E, 0x4A, 0xBE, 0xF5, 0xD5, 0x0D, 0x22, 0x76, 0x2E, 0xB6, 0x80, 0xD8, 0x54, @@ -279,188 +279,193 @@ const unsigned char default_keys_enc[7360] = { 0x2A, 0xE5, 0x39, 0x2D, 0xA3, 0x4B, 0xD1, 0x82, 0xF5, 0x68, 0x1F, 0x42, 0xE4, 0x0B, 0xB0, 0x2E, 0x37, 0x3C, 0x2A, 0x12, 0x61, 0xEC, 0x54, 0x1D, 0xA2, 0xA3, 0x89, 0x54, 0x25, 0xAD, 0x17, 0xE0, 0x8A, 0xFB, 0xA8, 0xF4, 0x6D, 0xAF, 0xF0, 0x84, 0x12, 0xE1, 0x92, 0x72, 0x9B, 0x41, 0x99, 0xA6, - 0x3E, 0x12, 0x74, 0x28, 0x6F, 0x9C, 0xA3, 0x63, 0xC6, 0x88, 0x76, 0xC6, 0x22, 0x76, 0xEC, 0x48, - 0x2B, 0xB5, 0x41, 0x81, 0x45, 0xBB, 0xCB, 0x4D, 0x9D, 0x77, 0x95, 0x49, 0x5F, 0x43, 0x27, 0x40, - 0xD1, 0x4E, 0xEB, 0x2B, 0xD9, 0x0D, 0x7B, 0xD3, 0x36, 0xAE, 0x18, 0x9E, 0x45, 0x36, 0xA1, 0xA0, - 0xB2, 0x30, 0xF1, 0x82, 0xE5, 0x73, 0x5F, 0xC4, 0x75, 0x43, 0xE9, 0xD7, 0x16, 0xE1, 0x98, 0xB6, - 0x60, 0xEB, 0x43, 0x4D, 0x5C, 0xBE, 0x0C, 0xC1, 0x92, 0x8D, 0x9E, 0x25, 0x3A, 0x55, 0xF6, 0x66, - 0x7C, 0x7F, 0xB4, 0x3F, 0x08, 0xA3, 0x1F, 0xC4, 0xCE, 0x05, 0x0F, 0xBF, 0x99, 0x4D, 0x40, 0x5A, - 0x56, 0xC1, 0x50, 0x87, 0x07, 0xDF, 0xED, 0xA2, 0x43, 0x2A, 0xB1, 0x69, 0x36, 0x44, 0xD3, 0x42, - 0x48, 0x53, 0xF6, 0xD8, 0xA9, 0xD7, 0x61, 0xCB, 0x12, 0x8B, 0xCC, 0x5A, 0xE2, 0x47, 0xCD, 0x8C, - 0xCB, 0xBC, 0x2A, 0x56, 0xE0, 0x00, 0x0F, 0x99, 0x61, 0xE4, 0x4C, 0xAE, 0x83, 0x7D, 0xFB, 0xEF, - 0x61, 0x49, 0x40, 0xF9, 0x37, 0x27, 0x12, 0x34, 0xF4, 0x85, 0xAB, 0x27, 0xB6, 0x96, 0xBF, 0xFE, - 0x00, 0xD1, 0x5A, 0xF6, 0x55, 0x90, 0x64, 0x8C, 0x95, 0xCF, 0x15, 0x02, 0x31, 0x45, 0x4D, 0x70, - 0xC4, 0xF9, 0xFC, 0x57, 0x06, 0x93, 0x46, 0x03, 0xE3, 0x94, 0x3C, 0x94, 0x52, 0x54, 0xED, 0x02, - 0x44, 0x9D, 0x61, 0xB9, 0x74, 0x84, 0xA4, 0x06, 0x9F, 0x1D, 0x38, 0x26, 0xE2, 0x8E, 0x09, 0x11, - 0xA9, 0xAF, 0xA7, 0xFE, 0xE8, 0xFF, 0xFB, 0xF2, 0x07, 0xC7, 0xFF, 0x00, 0xC0, 0x9D, 0xDD, 0x91, - 0x38, 0x4B, 0x65, 0x0F, 0xE5, 0xB2, 0xD1, 0xF2, 0x20, 0x91, 0x19, 0x8C, 0x44, 0xB3, 0x71, 0x6A, - 0x68, 0x3D, 0xE4, 0x4F, 0x56, 0x16, 0xFD, 0x25, 0x61, 0x95, 0x5B, 0xA4, 0xE7, 0xED, 0x8B, 0x07, - 0x25, 0x69, 0xDA, 0xFB, 0xED, 0x6C, 0x60, 0x62, 0x5E, 0x9A, 0x3F, 0x8C, 0xC8, 0xE7, 0xF6, 0xC0, - 0x3F, 0xF1, 0x0E, 0x6A, 0xFE, 0x65, 0xC3, 0x04, 0xCC, 0xBD, 0x6E, 0x01, 0x5A, 0xE5, 0xF1, 0x00, - 0xA6, 0xDC, 0xE7, 0x19, 0x23, 0x6D, 0xB3, 0xB2, 0x72, 0x93, 0x8F, 0x83, 0xFC, 0x1F, 0x3E, 0x8F, - 0x1D, 0x20, 0x68, 0x05, 0x2F, 0x53, 0x7F, 0xE1, 0x71, 0x7F, 0xFC, 0x80, 0x71, 0x8D, 0x51, 0xFF, - 0xE1, 0x57, 0x77, 0x13, 0xDA, 0x99, 0x77, 0x27, 0xF7, 0xDB, 0xAD, 0x43, 0xB7, 0xC1, 0x13, 0x65, - 0x0E, 0xE6, 0x54, 0xCB, 0xC4, 0x3E, 0x3B, 0x49, 0x8E, 0x3F, 0xFE, 0xDC, 0x8D, 0x17, 0x10, 0x72, - 0x12, 0xC2, 0x65, 0xB3, 0x16, 0x05, 0xB2, 0xAC, 0xB5, 0x7D, 0xC8, 0x40, 0x72, 0xC7, 0xCF, 0xC5, - 0x7B, 0xA2, 0x54, 0xA3, 0xC6, 0x7C, 0x2C, 0x24, 0x13, 0x33, 0xEF, 0x8C, 0xC3, 0x5A, 0x07, 0xD1, - 0x18, 0xF8, 0x18, 0xEF, 0x06, 0xC6, 0x5A, 0x78, 0xC6, 0x63, 0x11, 0x09, 0xC2, 0xFC, 0x73, 0x75, - 0xF0, 0x92, 0x7A, 0x90, 0xD9, 0xE4, 0xDB, 0xCF, 0x4F, 0x0A, 0x8D, 0x04, 0x5E, 0xF2, 0x8A, 0x7D, - 0xBD, 0x9E, 0xFD, 0x59, 0x62, 0x0D, 0xE5, 0x53, 0x6B, 0xE0, 0xD5, 0x24, 0xB4, 0x53, 0xD4, 0xAD, - 0xBF, 0xC0, 0x26, 0x86, 0x95, 0x82, 0x80, 0x21, 0x86, 0x18, 0xED, 0xE4, 0xDE, 0x94, 0x9B, 0x5F, - 0x6C, 0x41, 0x90, 0x9E, 0x7B, 0x9C, 0x5F, 0x37, 0x11, 0xD1, 0xFF, 0x17, 0xA3, 0x90, 0xFE, 0x87, - 0xE8, 0x0C, 0x2D, 0x44, 0xD3, 0x7C, 0x2D, 0x63, 0xA4, 0xD0, 0x7D, 0xC3, 0x69, 0x6A, 0x44, 0x65, - 0xD3, 0xB3, 0xEB, 0x77, 0xAA, 0x4E, 0x88, 0xCE, 0xDB, 0x3F, 0x71, 0x9C, 0x67, 0xA6, 0x72, 0xA9, - 0xFD, 0x3E, 0x70, 0x94, 0xB0, 0xA8, 0x82, 0x41, 0xB6, 0x19, 0x87, 0xD8, 0x47, 0x3A, 0x02, 0xCA, - 0x25, 0x17, 0x11, 0x2F, 0xEB, 0xDA, 0x6C, 0x2A, 0x76, 0xA9, 0x19, 0x33, 0xB6, 0x80, 0xBA, 0x88, - 0xDD, 0xAE, 0xCF, 0x31, 0x7F, 0x8C, 0x9B, 0x28, 0x5D, 0x5A, 0xE8, 0xE1, 0x09, 0x3F, 0xF0, 0x25, - 0x88, 0xAF, 0xBC, 0xBB, 0x07, 0x1F, 0x16, 0xCA, 0x74, 0xB3, 0xF0, 0xEE, 0x24, 0x51, 0x80, 0xDD, - 0x21, 0xE0, 0x8A, 0xC7, 0xA4, 0x26, 0x42, 0xFA, 0x0B, 0x4A, 0x7F, 0x8D, 0x41, 0xED, 0x05, 0x1D, - 0x0F, 0xE6, 0xF2, 0x33, 0xF3, 0xA8, 0x27, 0x0E, 0x11, 0x15, 0xED, 0x59, 0x1A, 0x02, 0x8E, 0xCA, - 0x87, 0xCA, 0x09, 0x50, 0x59, 0xC8, 0x1F, 0xA6, 0xC9, 0x60, 0xB3, 0x4D, 0x60, 0x82, 0x12, 0x3F, - 0x83, 0x1B, 0x69, 0x6D, 0xCB, 0x43, 0x39, 0x20, 0x93, 0xDF, 0x53, 0xCD, 0xCA, 0x8F, 0x9F, 0x15, - 0x01, 0xE7, 0xDA, 0x60, 0xAD, 0x2F, 0xCC, 0xBE, 0x09, 0x4E, 0x0F, 0x35, 0x20, 0x6D, 0xCD, 0x32, - 0xB0, 0x51, 0x78, 0x17, 0xEE, 0x06, 0x72, 0x9D, 0x66, 0xB0, 0x0D, 0x09, 0x78, 0xA4, 0x9D, 0x9C, - 0x16, 0x4A, 0x3B, 0xBE, 0x89, 0x6B, 0x1B, 0xD8, 0xBD, 0xAF, 0x00, 0xAE, 0x13, 0x86, 0xE3, 0x38, - 0xB9, 0x15, 0x30, 0xFC, 0x5E, 0x74, 0x5F, 0xFE, 0xB1, 0xEC, 0xCF, 0xB6, 0xE2, 0xBF, 0x63, 0x28, - 0xA7, 0x3E, 0x64, 0xC6, 0xC1, 0x72, 0x86, 0x72, 0xFE, 0x03, 0xB1, 0x60, 0x03, 0x16, 0x6F, 0xDA, - 0xE7, 0x48, 0x33, 0x58, 0x66, 0x07, 0x6C, 0x96, 0x5A, 0x29, 0x7D, 0xBF, 0x1C, 0xA1, 0x1A, 0xC6, - 0x1C, 0x90, 0x6D, 0x5C, 0x94, 0xB4, 0x4A, 0x94, 0x5C, 0x4D, 0xAC, 0x13, 0x07, 0x5A, 0xF8, 0x68, - 0x37, 0x3C, 0x9C, 0x33, 0xF8, 0x7E, 0x85, 0x38, 0x57, 0x28, 0xA9, 0xE3, 0xEE, 0xD8, 0x15, 0x4F, - 0xB7, 0x02, 0x0D, 0x6B, 0xB7, 0xF0, 0x27, 0x1E, 0x23, 0x95, 0x46, 0x50, 0x75, 0x4A, 0x87, 0x0D, - 0x2E, 0xCA, 0xC0, 0x37, 0x6E, 0x20, 0x19, 0x1D, 0xD6, 0x1C, 0xFE, 0x66, 0x67, 0xBB, 0x61, 0x91, - 0xC8, 0x0D, 0x77, 0xB9, 0xE1, 0x03, 0x82, 0x73, 0x40, 0xB5, 0x61, 0x94, 0xC9, 0x71, 0x38, 0xFE, - 0x88, 0x25, 0x3B, 0xD6, 0x67, 0x78, 0x79, 0xBA, 0xE1, 0xBD, 0x12, 0x93, 0x57, 0x91, 0xB8, 0x2C, - 0xEE, 0x4E, 0x48, 0xEC, 0x43, 0x18, 0xFC, 0x33, 0xF3, 0x80, 0x7E, 0xB0, 0x1B, 0xAC, 0xA4, 0x6A, - 0x08, 0xF9, 0x7A, 0x72, 0x7F, 0x0D, 0xAE, 0x5D, 0x80, 0x29, 0xA0, 0x9C, 0x73, 0xF1, 0xD5, 0xB5, - 0x56, 0x5F, 0xF7, 0xDD, 0xDE, 0x51, 0xB2, 0x40, 0xDA, 0x13, 0x8B, 0x57, 0x34, 0xDA, 0x52, 0x6E, - 0xCF, 0x2D, 0x12, 0x60, 0x80, 0x48, 0x84, 0x23, 0xB0, 0x8A, 0x5D, 0x34, 0xA1, 0x29, 0x70, 0x7A, - 0xED, 0x01, 0x44, 0x3B, 0xFC, 0x73, 0x22, 0x37, 0x08, 0x95, 0xD8, 0x7D, 0x07, 0xEF, 0x6B, 0xCD, - 0x46, 0x73, 0x6A, 0xD3, 0xAE, 0x13, 0x7D, 0xAE, 0xAF, 0x30, 0xA6, 0xD6, 0x68, 0x10, 0x1E, 0x1C, - 0x2D, 0x64, 0xF3, 0xA9, 0xE6, 0xE1, 0x10, 0x46, 0xC3, 0x12, 0xFB, 0x29, 0x6A, 0x15, 0xA6, 0x06, - 0x5C, 0x9E, 0xD3, 0x32, 0xE4, 0xCD, 0x96, 0x29, 0xDD, 0xB3, 0x05, 0xEA, 0x27, 0xA5, 0x95, 0x66, - 0xB0, 0x79, 0x64, 0x70, 0x26, 0x7C, 0x46, 0x39, 0x18, 0x79, 0x8D, 0xDC, 0x83, 0x6F, 0x11, 0xF0, - 0x05, 0xC5, 0xDC, 0x54, 0xEB, 0xB7, 0xA1, 0x13, 0x4B, 0x13, 0x28, 0x49, 0x91, 0xCC, 0x2A, 0x26, - 0xB5, 0xF0, 0x25, 0x8E, 0xF4, 0xC0, 0xFF, 0x2F, 0x37, 0xD5, 0x57, 0x7E, 0x25, 0xBF, 0xBE, 0x91, - 0x5C, 0x83, 0x6D, 0xC5, 0x4A, 0xDE, 0x7C, 0xC8, 0x37, 0xEA, 0x0F, 0x51, 0xEE, 0x18, 0xDE, 0xC8, - 0xCC, 0xA6, 0xCE, 0x07, 0xB9, 0x12, 0x1A, 0xB9, 0x7F, 0x08, 0x44, 0x34, 0x15, 0x58, 0x32, 0x69, - 0x8C, 0xEA, 0x44, 0x8B, 0x05, 0x15, 0x7A, 0xD7, 0xBB, 0xD2, 0x0B, 0x57, 0x25, 0x11, 0x75, 0x3A, - 0x47, 0xD4, 0xA7, 0x37, 0x40, 0x0C, 0x0A, 0x25, 0x21, 0x85, 0x2E, 0x95, 0x5F, 0x15, 0x15, 0x8D, - 0x8E, 0xBB, 0x0B, 0x6D, 0xE8, 0x58, 0x3F, 0x0C, 0x1D, 0xAD, 0xE0, 0x61, 0x7F, 0xE5, 0x58, 0xD5, - 0x11, 0x95, 0x90, 0x2B, 0x1D, 0x28, 0x58, 0xE2, 0xB9, 0x9D, 0x52, 0x65, 0x98, 0xE4, 0xF7, 0x4C, - 0x3B, 0x63, 0x3F, 0x9A, 0xBC, 0x17, 0x38, 0xA5, 0xAC, 0x28, 0x37, 0x66, 0x38, 0xC3, 0x79, 0xC1, - 0x43, 0x2E, 0xAB, 0x26, 0x22, 0xA6, 0xCF, 0xB3, 0x9F, 0xBE, 0xAC, 0x7A, 0xF4, 0x31, 0xCA, 0x8D, - 0x03, 0xDD, 0xE6, 0xCC, 0xB3, 0x08, 0x57, 0x9C, 0x1E, 0xC2, 0xC6, 0x73, 0xB5, 0x54, 0x95, 0xD8, - 0xC6, 0xC2, 0xF1, 0x69, 0x4F, 0x1E, 0xE6, 0xB1, 0x77, 0x3E, 0x02, 0x86, 0x26, 0x06, 0x2B, 0x34, - 0xA1, 0x51, 0xC0, 0x3D, 0x8D, 0xC6, 0xD6, 0x4A, 0xD8, 0x29, 0x18, 0x4C, 0x0B, 0xEB, 0x74, 0x1C, - 0xDC, 0x13, 0x62, 0xB4, 0xEE, 0x96, 0xF4, 0x48, 0x09, 0x5D, 0x60, 0xD3, 0xAC, 0x85, 0xDF, 0x9A, - 0x10, 0xAB, 0x1A, 0x94, 0x31, 0x74, 0xF3, 0x1D, 0x76, 0x87, 0xB2, 0x3A, 0xD1, 0xF7, 0x93, 0x79, - 0x67, 0x37, 0x6A, 0x97, 0x1F, 0xB9, 0x9E, 0xE4, 0xA1, 0xD3, 0xEF, 0xB4, 0xE6, 0xB2, 0x76, 0x9B, - 0xAA, 0x68, 0x2E, 0xA3, 0x18, 0x23, 0xE4, 0xA9, 0xAF, 0xFC, 0x8D, 0xBE, 0x19, 0x18, 0xB6, 0xF9, - 0xCE, 0xD2, 0xE2, 0xB5, 0xEC, 0x06, 0x43, 0xDD, 0x5C, 0x19, 0xE9, 0xC0, 0x63, 0x08, 0x87, 0xAE, - 0xDE, 0x34, 0xAB, 0xDA, 0xE8, 0xA3, 0x47, 0x85, 0xD5, 0xB1, 0x7A, 0xC2, 0x0F, 0x6A, 0x82, 0x5A, - 0xA1, 0xA1, 0x1D, 0xCF, 0xC3, 0xB9, 0xA4, 0x87, 0x79, 0x01, 0x4A, 0x59, 0xB0, 0xBB, 0xB0, 0x01, - 0x1E, 0x3C, 0x7A, 0xB7, 0xAF, 0xAF, 0xF7, 0x5A, 0x53, 0x17, 0x4B, 0xEB, 0x83, 0x15, 0xB2, 0x9F, - 0x02, 0x3B, 0x19, 0xDA, 0x8C, 0x91, 0x00, 0xF3, 0x78, 0x52, 0xF6, 0xB5, 0x7A, 0x90, 0x1D, 0x77, - 0x33, 0x35, 0x85, 0x06, 0x7D, 0x5F, 0x0D, 0xF1, 0xC6, 0x2B, 0x9D, 0x6E, 0xEA, 0x9F, 0xC3, 0xC6, - 0x87, 0xCD, 0x65, 0x74, 0xEB, 0x42, 0x5B, 0x21, 0x5F, 0xE3, 0xAA, 0x66, 0x44, 0xE2, 0xBE, 0x91, - 0xC8, 0xF9, 0xDA, 0xEC, 0x04, 0x8C, 0x5B, 0x5B, 0x1F, 0x52, 0x09, 0x57, 0x26, 0x2E, 0x95, 0xFD, - 0xCC, 0xF1, 0xDD, 0xB1, 0xBB, 0x98, 0x33, 0xE6, 0x1A, 0xBE, 0x44, 0x1C, 0xC2, 0xD1, 0x33, 0xB7, - 0xE2, 0x26, 0xDE, 0xC5, 0x0E, 0x28, 0x6A, 0x30, 0xD3, 0x81, 0x42, 0xDA, 0x48, 0x0E, 0x6F, 0xB2, - 0x33, 0xF3, 0xB3, 0x0C, 0xE1, 0x29, 0xB0, 0x77, 0xA0, 0x6C, 0xCB, 0x17, 0xE1, 0x81, 0x66, 0x8F, - 0x77, 0xDD, 0x3E, 0x5D, 0x46, 0x84, 0xF2, 0x9B, 0x7F, 0xAE, 0x2A, 0x8A, 0xCF, 0xB6, 0xB5, 0x92, - 0xF0, 0x8B, 0x1C, 0xF4, 0x38, 0x7F, 0x14, 0x26, 0x00, 0x4B, 0x90, 0x55, 0x11, 0x35, 0xE5, 0x84, - 0x50, 0x5A, 0x79, 0x53, 0x5B, 0xB3, 0xB2, 0xD7, 0xAB, 0x6D, 0x87, 0xC5, 0x94, 0xF9, 0x6F, 0xEA, - 0x74, 0xA0, 0x6F, 0x72, 0x76, 0x66, 0x76, 0xBB, 0x74, 0xF4, 0x4A, 0x69, 0x37, 0x7C, 0xC9, 0x0D, - 0x3F, 0xDE, 0x47, 0xE9, 0x56, 0xC0, 0x97, 0x9A, 0x3A, 0xB0, 0x05, 0xDC, 0x99, 0xA4, 0x9C, 0x1C, - 0x49, 0x57, 0xBE, 0xB0, 0xE1, 0xB0, 0xDC, 0xAD, 0xC1, 0xCC, 0x31, 0xB1, 0x4B, 0xE2, 0x63, 0x7F, - 0x7A, 0xB4, 0x84, 0x55, 0x5E, 0xEF, 0x85, 0x4B, 0xD7, 0x5D, 0x60, 0x9B, 0x82, 0x47, 0x3C, 0x45, - 0x68, 0x5F, 0xCB, 0x59, 0x0E, 0xA3, 0x62, 0x61, 0xE6, 0x1B, 0x7D, 0x29, 0x36, 0x04, 0x57, 0x7E, - 0x73, 0xC6, 0x92, 0x52, 0x1C, 0x07, 0x47, 0xA1, 0x46, 0x9C, 0x55, 0x68, 0xFB, 0xC1, 0x11, 0x5A, - 0x85, 0x08, 0x09, 0xE7, 0xF9, 0x81, 0xC8, 0x0A, 0xEC, 0x5A, 0x46, 0x49, 0x5A, 0x84, 0xB0, 0xEF, - 0x90, 0x79, 0xD8, 0xC5, 0x26, 0xBE, 0x1E, 0xF5, 0x5E, 0xBE, 0x6F, 0x39, 0xD0, 0x96, 0xD1, 0x3B, - 0xAD, 0xD7, 0x2A, 0x91, 0xB5, 0x48, 0x58, 0x10, 0x79, 0x9B, 0x05, 0x98, 0x66, 0xAD, 0xF8, 0x38, - 0xC4, 0xF6, 0x56, 0xCC, 0xF3, 0x7D, 0x4B, 0xB6, 0x97, 0xD6, 0xE8, 0x8B, 0xC5, 0xFB, 0x83, 0x69, - 0x46, 0xE1, 0x4E, 0xF5, 0x67, 0xD5, 0x7B, 0x06, 0x8C, 0x5A, 0x82, 0x5C, 0x60, 0x33, 0xB1, 0xD3, - 0x50, 0x30, 0x52, 0x4E, 0xDA, 0x85, 0xBA, 0x98, 0x30, 0xB4, 0xAB, 0x22, 0x49, 0xC9, 0xD8, 0xB2, - 0xE0, 0x63, 0x1F, 0x12, 0x32, 0x1E, 0xD6, 0x05, 0x21, 0x86, 0x58, 0x53, 0x4F, 0xEA, 0x2A, 0x59, - 0x75, 0x35, 0x2D, 0x1A, 0x82, 0xEF, 0x8C, 0x71, 0x3B, 0xCD, 0x78, 0x32, 0xE8, 0xD2, 0x30, 0x12, - 0x79, 0x22, 0x4F, 0x4D, 0xAE, 0xFF, 0xA5, 0x48, 0x3C, 0xCA, 0x5F, 0x6A, 0x14, 0xB4, 0x96, 0xB7, - 0x6C, 0xD9, 0xC1, 0xD7, 0x24, 0xF7, 0xDE, 0x14, 0x70, 0x70, 0x14, 0xEE, 0x68, 0x4F, 0x39, 0x36, - 0xA9, 0xE0, 0x27, 0xEC, 0xFE, 0x03, 0x5D, 0x2D, 0xBD, 0x66, 0x8E, 0xA7, 0xB4, 0x1F, 0xA0, 0x94, - 0xBE, 0x51, 0xDE, 0x44, 0xEC, 0xE4, 0x6D, 0xFD, 0xAA, 0xAF, 0x9C, 0x3A, 0x41, 0x87, 0xF8, 0x4E, - 0x8A, 0xF4, 0x08, 0xC5, 0xB3, 0xD0, 0xB9, 0x1B, 0x23, 0x77, 0x7F, 0x39, 0x9D, 0xAE, 0x0C, 0xAA, - 0x63, 0xB3, 0x99, 0x84, 0xFB, 0xCE, 0x79, 0x7A, 0x34, 0x78, 0x36, 0xA2, 0x38, 0xF8, 0xF7, 0x2E, - 0x0C, 0x7D, 0xBD, 0xBB, 0xF8, 0x5B, 0x5F, 0x33, 0x92, 0x9E, 0x01, 0xEA, 0x85, 0x77, 0xA7, 0xB0, - 0xA5, 0x06, 0xEB, 0xF3, 0x75, 0x5A, 0x2E, 0xDD, 0xD6, 0x7A, 0x07, 0xE4, 0x24, 0x24, 0xC7, 0x76, - 0x52, 0xAD, 0x7B, 0x3C, 0x45, 0x29, 0xB8, 0x01, 0x32, 0xE5, 0x85, 0x0F, 0x2F, 0x50, 0x19, 0x54, - 0x20, 0x67, 0x58, 0x8B, 0x65, 0xFA, 0x4D, 0x4E, 0xA9, 0x70, 0xC4, 0x9A, 0x3C, 0xF3, 0xB0, 0x35, - 0x02, 0x13, 0x5B, 0xD6, 0xB2, 0x63, 0xBF, 0x2E, 0xB4, 0xB6, 0xC1, 0x7C, 0x8B, 0x75, 0xF6, 0x99, - 0x67, 0xBC, 0xC6, 0xA1, 0xB1, 0x58, 0xF1, 0x72, 0x3A, 0x92, 0x31, 0x5C, 0x2C, 0x2E, 0x1D, 0xF3, - 0x09, 0x9E, 0xD3, 0x18, 0xDB, 0x39, 0x11, 0x3D, 0xE1, 0x9D, 0x7B, 0xE3, 0x4D, 0x16, 0xCF, 0x4F, - 0x2B, 0xF9, 0x97, 0xEF, 0x6A, 0x3A, 0x30, 0xAE, 0x6B, 0x90, 0x85, 0x51, 0x14, 0x78, 0xD9, 0xDF, - 0xDC, 0x65, 0x3C, 0x83, 0xF5, 0xE9, 0xE6, 0xB5, 0x8B, 0x42, 0x8C, 0xBE, 0x05, 0x78, 0xE4, 0x9A, - 0xBA, 0x21, 0xD4, 0x30, 0x48, 0x89, 0x92, 0xE4, 0x7E, 0xF9, 0x43, 0x4D, 0x2C, 0xDF, 0xDE, 0x8E, - 0x63, 0xFB, 0x40, 0xDD, 0x0E, 0x2C, 0x34, 0x4F, 0x44, 0xAE, 0x29, 0xA2, 0x48, 0x58, 0x60, 0xC8, - 0xC7, 0x64, 0x1F, 0x69, 0x99, 0xD1, 0x01, 0x91, 0x81, 0x42, 0x54, 0x10, 0xBE, 0x82, 0x18, 0x39, - 0x78, 0xE4, 0x4E, 0xFA, 0xB6, 0xE6, 0x48, 0xB8, 0x36, 0x65, 0xDF, 0x00, 0xF0, 0x12, 0x60, 0xB3, - 0x74, 0x28, 0x1E, 0x68, 0xF1, 0x40, 0x9A, 0x29, 0xA1, 0xBB, 0x21, 0x9D, 0x96, 0x61, 0x8F, 0x85, - 0x6C, 0x88, 0x58, 0x91, 0x79, 0xF6, 0x88, 0x3A, 0x9A, 0x54, 0xC0, 0xE5, 0x13, 0x88, 0x30, 0x4A, - 0x65, 0xE1, 0x8D, 0x0D, 0x10, 0x61, 0xD8, 0xA5, 0x90, 0x02, 0xED, 0xA6, 0xE9, 0x49, 0xD9, 0xC7, - 0x86, 0x2C, 0xFF, 0xAC, 0xD6, 0x4E, 0xED, 0x5C, 0x4F, 0xA2, 0x8E, 0xF9, 0x18, 0x3B, 0xDE, 0x16, - 0x04, 0xD2, 0x75, 0xEC, 0x15, 0x9F, 0xF0, 0x01, 0xB5, 0xE7, 0x0C, 0x96, 0xBE, 0xC4, 0xBE, 0xEA, - 0xDB, 0xB7, 0x2B, 0xFC, 0x73, 0x6A, 0x1D, 0x0B, 0x74, 0xD8, 0x64, 0x57, 0xD0, 0xB9, 0x4F, 0x9A, - 0x72, 0x74, 0x07, 0xC5, 0x8D, 0xDB, 0x81, 0x4C, 0x13, 0x77, 0xCD, 0xDA, 0x01, 0x8E, 0xDF, 0xFF, - 0xCA, 0x11, 0x62, 0x37, 0xC9, 0xAC, 0xFD, 0x94, 0xF4, 0xCC, 0x42, 0xC7, 0x9B, 0xD1, 0xF9, 0x4D, - 0x85, 0x3B, 0xDC, 0xBF, 0xFC, 0x20, 0x4D, 0xE1, 0x52, 0xCD, 0x29, 0xF1, 0x7D, 0x2A, 0x54, 0xA1, - 0x2E, 0x18, 0x7B, 0xDD, 0x05, 0xE0, 0x36, 0x7D, 0x7C, 0x40, 0x11, 0x9A, 0xC8, 0xE1, 0x63, 0x39, - 0x7D, 0x72, 0x54, 0xB2, 0x1C, 0xE1, 0x40, 0x13, 0x6A, 0x1F, 0x76, 0xB1, 0xAD, 0x75, 0xE3, 0x24, - 0x7B, 0x3F, 0xA9, 0xCA, 0xFD, 0x28, 0x76, 0x6F, 0x65, 0x63, 0xA7, 0xCC, 0x71, 0x84, 0xE3, 0x04, - 0xC5, 0x05, 0x17, 0x5A, 0x1F, 0xD0, 0xEA, 0x69, 0xBB, 0x7A, 0xE1, 0xA1, 0xB0, 0xFB, 0xE0, 0xD2, - 0x70, 0x1F, 0x6B, 0x5C, 0x86, 0xE4, 0xDE, 0x8C, 0x5C, 0xC8, 0x36, 0xA9, 0xDD, 0x5D, 0x13, 0x82, - 0xDB, 0x6E, 0x93, 0x00, 0x77, 0x8C, 0xE1, 0xD3, 0x9A, 0x0C, 0x4D, 0xF4, 0x5A, 0x10, 0xDB, 0xBF, - 0x3D, 0xD0, 0x6C, 0x4E, 0xEC, 0x64, 0xA2, 0xF4, 0x5D, 0x29, 0x80, 0x4B, 0xE7, 0xA1, 0x14, 0xAE, - 0xB4, 0x78, 0x8B, 0x6E, 0xCB, 0xA1, 0xB2, 0x02, 0x35, 0xC7, 0x4E, 0x58, 0x7C, 0x98, 0x46, 0x05, - 0xCE, 0x56, 0x83, 0xB4, 0x5E, 0x82, 0x65, 0xF1, 0xC9, 0x9B, 0x29, 0xAC, 0x42, 0xEB, 0xE5, 0xF1, - 0x1D, 0x1A, 0x11, 0x0C, 0x63, 0xAD, 0xFD, 0xCF, 0x40, 0x68, 0xF1, 0xB3, 0xF4, 0x62, 0xB9, 0x9B, - 0x6C, 0x6C, 0x13, 0x94, 0xAF, 0x82, 0x80, 0xE0, 0xBA, 0x6C, 0xCB, 0x84, 0x0D, 0xA0, 0xE4, 0xAA, - 0x15, 0x8F, 0xAD, 0x29, 0xCE, 0x79, 0x7B, 0xF6, 0x70, 0x3F, 0xCC, 0x8B, 0x92, 0xA9, 0xC5, 0x17, - 0xBA, 0xE0, 0xF0, 0x9B, 0x96, 0x8E, 0x7F, 0xA0, 0x2F, 0x2C, 0x5F, 0x54, 0xF6, 0x5A, 0x9E, 0xBB, - 0xAC, 0x6C, 0xE3, 0xBF, 0xC4, 0x1C, 0xAB, 0x14, 0xEB, 0x12, 0xCE, 0xD8, 0xA5, 0x44, 0xCC, 0x4F, - 0x4B, 0x08, 0x4F, 0x2C, 0x00, 0x81, 0xD5, 0x17, 0x22, 0x07, 0x42, 0xCA, 0xFD, 0x49, 0xAD, 0x06, - 0x95, 0xC8, 0xD1, 0xC7, 0x3E, 0x39, 0x34, 0x1C, 0x41, 0x99, 0xC2, 0xAB, 0x8A, 0xED, 0x50, 0x12, - 0xE8, 0xC7, 0x75, 0x52, 0x19, 0x2B, 0xD4, 0xCC, 0xD3, 0xFA, 0x84, 0xE7, 0x0C, 0xCE, 0xE3, 0x93, - 0xCA, 0x60, 0xE5, 0xB7, 0x06, 0xDB, 0x84, 0xEE, 0x79, 0xA7, 0x54, 0x76, 0xE9, 0x46, 0x85, 0xEA, - 0x4F, 0xF3, 0xA1, 0xEF, 0x10, 0xC1, 0x4C, 0x12, 0xB0, 0xEE, 0x23, 0xDD, 0x81, 0x3A, 0x6E, 0x9F, - 0x01, 0x03, 0x04, 0x7C, 0x6D, 0x47, 0x84, 0xB7, 0xE7, 0x19, 0xE3, 0x4E, 0xCF, 0x23, 0x3A, 0xB2, - 0x23, 0x33, 0x00, 0xCB, 0x07, 0x78, 0x51, 0x8B, 0x0C, 0x30, 0x7A, 0x1F, 0x41, 0x14, 0x75, 0xFF, - 0x9E, 0x43, 0x81, 0xF4, 0x15, 0x89, 0x8B, 0xB7, 0x2B, 0xBC, 0x62, 0x48, 0x64, 0xD9, 0x26, 0xBE, - 0xEA, 0x22, 0xB6, 0x22, 0xB8, 0x6F, 0x2B, 0xB6, 0x9B, 0x8F, 0xC7, 0x63, 0x03, 0x83, 0xA7, 0x22, - 0xF8, 0x5C, 0x08, 0x87, 0x70, 0xA2, 0xCB, 0x6B, 0xD6, 0xC9, 0xF6, 0x59, 0x60, 0x8C, 0x10, 0xFA, - 0x3C, 0xAD, 0x15, 0x1F, 0x8F, 0x18, 0x01, 0x2F, 0xB9, 0x2C, 0x01, 0x59, 0x76, 0x18, 0xE6, 0x55, - 0x98, 0x23, 0x33, 0xA9, 0x05, 0xFC, 0x4C, 0xF3, 0x9A, 0xCB, 0xBA, 0x42, 0x60, 0x0C, 0x50, 0xEB, - 0x69, 0xF1, 0x22, 0x73, 0x03, 0x4B, 0x38, 0x74, 0xBF, 0xBB, 0x7B, 0x4C, 0x7F, 0x30, 0xF0, 0x21, - 0x8C, 0x73, 0x24, 0x69, 0x1F, 0x7F, 0xF4, 0x18, 0x0E, 0x4F, 0xB7, 0x99, 0x8D, 0x5A, 0xE9, 0xF7, - 0x79, 0x8B, 0x25, 0xCB, 0xC7, 0xEA, 0x8C, 0xA7, 0x36, 0x33, 0x72, 0x78, 0x2B, 0x9B, 0x4F, 0xA6, - 0x53, 0x10, 0xFA, 0xF7, 0x1C, 0x66, 0xBB, 0x7C, 0x72, 0x18, 0xBE, 0x91, 0x5C, 0x8C, 0xED, 0x75, - 0x3C, 0x35, 0xF4, 0x49, 0xAD, 0xB0, 0x49, 0x67, 0x05, 0x6B, 0x96, 0x46, 0xF9, 0x2D, 0xAF, 0x8E, - 0x90, 0x3A, 0x3C, 0x35, 0xF5, 0x66, 0x7E, 0xE8, 0x08, 0x04, 0x0D, 0xBF, 0x6E, 0x4E, 0x3F, 0xAD, - 0x0A, 0xB9, 0x06, 0xDF, 0x4B, 0xD1, 0x9E, 0x5D, 0x69, 0x13, 0x4B, 0xCD, 0xB6, 0xE7, 0x4A, 0x12, - 0xF2, 0x94, 0xE7, 0xBF, 0xE3, 0x48, 0x65, 0xF2, 0xD4, 0x2A, 0xCF, 0x17, 0xC2, 0xF8, 0x50, 0xC5, - 0xF9, 0x3B, 0x11, 0x04, 0x5D, 0x84, 0xE6, 0x9D, 0x97, 0x31, 0xDB, 0x72, 0xD0, 0x0F, 0x3E, 0x9D, - 0xE2, 0x5B, 0x81, 0xBF, 0xA3, 0xD1, 0xB3, 0x5D, 0xCF, 0x1D, 0x70, 0xDA, 0x96, 0xA3, 0x67, 0xA5, - 0xF0, 0x5E, 0xA8, 0xD0, 0x15, 0x47, 0x3D, 0xAE, 0xA4, 0x33, 0x80, 0xF4, 0x90, 0x1B, 0x27, 0xD1, - 0x4F, 0x5F, 0x2B, 0x1B, 0xA4, 0xEE, 0x5F, 0x3F, 0x0F, 0xC2, 0x70, 0x13, 0x42, 0x42, 0x98, 0xA1, - 0xC4, 0x0C, 0x2B, 0xB1, 0x4E, 0x69, 0xF2, 0xBC, 0xB5, 0x21, 0xD2, 0x11, 0xA4, 0xC0, 0x3D, 0xB5, - 0xBE, 0x4D, 0x0E, 0x84, 0x9B, 0xF0, 0xB0, 0x0F, 0xE2, 0x23, 0xCB, 0x66, 0x1D, 0x71, 0x4F, 0x20, - 0x2F, 0x3C, 0x9D, 0xC8, 0x3C, 0x11, 0xA7, 0x47, 0xA4, 0x3A, 0x46, 0xB0, 0x83, 0xA1, 0xD5, 0x34, - 0x71, 0xA9, 0x8B, 0xDB, 0xBA, 0x6F, 0x49, 0xDC, 0x69, 0xC9, 0xBF, 0xFE, 0x6C, 0x12, 0x58, 0x3B, - 0xDB, 0x42, 0x39, 0xF7, 0x63, 0xB2, 0xA4, 0xFD, 0x30, 0x48, 0x45, 0x4E, 0x02, 0x15, 0xBD, 0x4B, - 0xC3, 0x59, 0xF9, 0x0B, 0x87, 0x1F, 0xEB, 0x90, 0x0E, 0x69, 0x0C, 0xDE, 0x15, 0xD8, 0x7C, 0xA6, - 0xD2, 0xFC, 0xFA, 0x5F, 0xAE, 0x9B, 0x7D, 0x76, 0x0B, 0xE8, 0x53, 0xE4, 0x10, 0xC6, 0xF0, 0x3D, - 0x33, 0x7A, 0x9E, 0x4E, 0x57, 0x0F, 0x58, 0xB6, 0x13, 0x95, 0x89, 0x6D, 0x84, 0xC6, 0xB2, 0x22, - 0x37, 0xF9, 0x99, 0x27, 0xCC, 0x10, 0xA2, 0x14, 0x84, 0x0F, 0x9A, 0xE7, 0xB9, 0x52, 0xFC, 0xB3, - 0x3F, 0x97, 0x07, 0x67, 0xB5, 0xE5, 0x00, 0x32, 0x4D, 0x90, 0x4D, 0x6A, 0xFE, 0x17, 0xE1, 0xAC, - 0x58, 0x00, 0x69, 0xC8, 0x7A, 0x87, 0x60, 0x10, 0xB5, 0x9C, 0x64, 0xFC, 0xAE, 0xD8, 0x86, 0x88, - 0x5A, 0x76, 0xED, 0x72, 0x97, 0x19, 0x90, 0xD1, 0xE3, 0xAF, 0x6E, 0x07, 0x2F, 0x7A, 0xBB, 0xC6, - 0x0D, 0x63, 0x53, 0x23, 0x00, 0xA3, 0x8A, 0x88, 0x2B, 0x21, 0x9C, 0x3A, 0x9D, 0x86, 0x16, 0xE9, -}; + 0x4F, 0x1C, 0xE7, 0x11, 0xCB, 0x53, 0xD9, 0x1D, 0x46, 0x7F, 0xA5, 0x69, 0xFE, 0x2E, 0x02, 0x2C, + 0x39, 0xFC, 0xEC, 0x2E, 0xB5, 0x3D, 0x92, 0x03, 0x94, 0xDE, 0x72, 0x94, 0x0C, 0x48, 0xA1, 0x85, + 0xDC, 0x84, 0x7E, 0xE0, 0x0C, 0xDD, 0x9B, 0xC4, 0x13, 0x13, 0x01, 0x38, 0x11, 0x46, 0xF7, 0xEB, + 0x2D, 0x6F, 0x30, 0x50, 0x09, 0x99, 0x09, 0x28, 0x6A, 0x76, 0xD9, 0xAC, 0x1A, 0x4E, 0xA2, 0xEE, + 0xD9, 0x26, 0x05, 0xDB, 0x7E, 0x48, 0x9E, 0xC8, 0xEE, 0x6E, 0xE0, 0xE1, 0xED, 0x5C, 0x8A, 0x2A, + 0xC7, 0x4B, 0x78, 0xD8, 0x4B, 0x59, 0x9D, 0x53, 0x48, 0x8E, 0x7F, 0xE8, 0xC0, 0xA9, 0x4C, 0x65, + 0x48, 0x38, 0xDB, 0xE7, 0xBD, 0xBA, 0xC2, 0x45, 0xA3, 0x2E, 0x09, 0x2E, 0xCC, 0xBE, 0x26, 0x92, + 0x31, 0x0C, 0x66, 0xA4, 0xE8, 0xE7, 0x0B, 0x4A, 0x55, 0x37, 0xED, 0xFF, 0x95, 0xE0, 0xE2, 0xBC, + 0x63, 0x74, 0x89, 0x65, 0xCF, 0x9B, 0x5E, 0xCA, 0xF9, 0x4B, 0x52, 0x8A, 0xF9, 0x3F, 0xC0, 0x95, + 0xA9, 0xD6, 0xF5, 0xDD, 0xF9, 0x6E, 0xD8, 0x0C, 0x42, 0x76, 0xFD, 0xE5, 0x7A, 0xA6, 0xD0, 0x8E, + 0x5A, 0x2C, 0x56, 0x52, 0x45, 0x3A, 0x86, 0xAD, 0x94, 0xD2, 0x31, 0x62, 0x62, 0xFF, 0xFF, 0xF8, + 0x72, 0x29, 0x5D, 0x64, 0xC3, 0xEF, 0xC5, 0x98, 0xF6, 0x3D, 0xCC, 0x8A, 0xBF, 0xF8, 0xA4, 0x43, + 0x9F, 0x7C, 0x72, 0xAA, 0x2F, 0xA6, 0xDB, 0x69, 0xAD, 0x32, 0x70, 0xF4, 0xA4, 0x35, 0x84, 0x5D, + 0xFF, 0x3B, 0x4B, 0x83, 0x51, 0x44, 0xE0, 0x97, 0xF3, 0x3C, 0xFE, 0x42, 0x40, 0x0D, 0xBF, 0xAB, + 0x7D, 0x55, 0xC5, 0xCD, 0xA6, 0xDD, 0x07, 0xC7, 0xCD, 0xEC, 0x76, 0x14, 0x65, 0xD5, 0xC2, 0x62, + 0x69, 0xDC, 0x95, 0x26, 0x54, 0x29, 0x1F, 0x68, 0x46, 0x95, 0x40, 0x7D, 0xED, 0x45, 0x12, 0xD2, + 0x5B, 0x9E, 0xF5, 0x32, 0x4C, 0x84, 0xF7, 0xF4, 0x05, 0x93, 0x62, 0xC5, 0x43, 0xED, 0xF6, 0x8E, + 0x17, 0x55, 0x82, 0x26, 0x2C, 0xC8, 0x30, 0x90, 0x10, 0x50, 0x98, 0x14, 0x14, 0x2A, 0xF0, 0xC7, + 0x54, 0xA1, 0x29, 0xF3, 0xDA, 0x41, 0x42, 0x38, 0x91, 0xCB, 0x3D, 0x8F, 0x5C, 0xCA, 0xB7, 0xC6, + 0x56, 0x5C, 0x07, 0x79, 0xFF, 0xA5, 0x8B, 0x90, 0x49, 0x0B, 0xE2, 0x12, 0x9C, 0xAA, 0x7F, 0xF9, + 0x5F, 0x8E, 0x4C, 0xD2, 0xA2, 0xC8, 0x5E, 0x0E, 0xC4, 0xB9, 0x43, 0xE0, 0x57, 0x60, 0x1F, 0x68, + 0xED, 0x76, 0xA2, 0xD9, 0xE7, 0x78, 0x1C, 0x7A, 0xB5, 0x31, 0xED, 0x1D, 0x05, 0x2E, 0xA6, 0x15, + 0x14, 0x22, 0xEF, 0xA6, 0xB7, 0x98, 0xAF, 0x42, 0x47, 0x75, 0xAB, 0xEA, 0x5B, 0x91, 0x43, 0x5C, + 0x2E, 0xE7, 0x62, 0x1A, 0xF1, 0x6E, 0x9D, 0xB3, 0xAC, 0xA4, 0x4A, 0x5F, 0xFE, 0x14, 0x91, 0x02, + 0x3B, 0xEB, 0x3C, 0x5C, 0x9D, 0x08, 0x34, 0xAF, 0x9F, 0x5F, 0x8D, 0xDE, 0xD7, 0x13, 0x15, 0x99, + 0xAD, 0x7C, 0x21, 0x3E, 0x5C, 0x73, 0xED, 0xB1, 0x9F, 0x90, 0x37, 0xDF, 0x6F, 0xD2, 0xCB, 0xEE, + 0x8C, 0x3D, 0x13, 0x64, 0x6B, 0x57, 0xCA, 0xB0, 0xCB, 0x04, 0xAC, 0x05, 0x89, 0x29, 0xCB, 0x41, + 0x8C, 0x86, 0x98, 0x79, 0x89, 0x74, 0x7B, 0xAB, 0x59, 0xC5, 0x77, 0x31, 0xB3, 0x25, 0x7A, 0x72, + 0x44, 0xBF, 0x23, 0xB9, 0x1C, 0xC7, 0xD0, 0x75, 0xD8, 0x20, 0xC3, 0xD1, 0x62, 0x98, 0x6B, 0xE8, + 0x6E, 0xE5, 0x67, 0xEC, 0x08, 0xD8, 0x84, 0x33, 0x0B, 0xB4, 0x9E, 0xAE, 0xA4, 0xD9, 0x28, 0xA0, + 0xDF, 0xEF, 0xA6, 0xFA, 0x5F, 0x5B, 0x2A, 0x27, 0x9B, 0x8F, 0xF4, 0x88, 0x8E, 0x72, 0x16, 0xFF, + 0x8E, 0x00, 0xA9, 0x3D, 0x77, 0x8B, 0x11, 0xCD, 0xF9, 0xA2, 0xC2, 0xF0, 0xE0, 0xB6, 0xFD, 0x3F, + 0x16, 0x89, 0x50, 0xF9, 0x07, 0xCF, 0x13, 0xF8, 0x78, 0xEE, 0x8D, 0x76, 0x3F, 0xDC, 0xE0, 0x1D, + 0x0D, 0xAD, 0x67, 0xE4, 0x5B, 0x90, 0x49, 0x12, 0xDF, 0x70, 0xDF, 0xCA, 0xEC, 0xDA, 0x4A, 0xC8, + 0x5E, 0xA2, 0x6F, 0xAC, 0x0E, 0x11, 0x9A, 0x2C, 0x80, 0x94, 0x3B, 0xD3, 0xDB, 0xC0, 0x26, 0x29, + 0x91, 0x4F, 0x19, 0xB3, 0xE7, 0xCD, 0x76, 0xB5, 0xEE, 0x4E, 0x35, 0xCC, 0x7F, 0x62, 0x66, 0xAB, + 0xB9, 0x50, 0x96, 0x1A, 0xDB, 0x79, 0xB2, 0x1A, 0x4C, 0xE0, 0x5D, 0x32, 0xE0, 0x45, 0x71, 0x8C, + 0xB0, 0xFD, 0x74, 0xA8, 0x5D, 0x99, 0xA9, 0x56, 0x67, 0x3B, 0x29, 0x0C, 0x70, 0xD0, 0x5A, 0xB8, + 0xF7, 0x9C, 0xF7, 0x9F, 0x32, 0x91, 0xB8, 0x29, 0x0F, 0xCF, 0x4A, 0xA1, 0xD9, 0xFE, 0xFF, 0x90, + 0xF9, 0x5C, 0x1D, 0x4C, 0xB4, 0x33, 0xCE, 0xA5, 0xEF, 0x22, 0x97, 0xF4, 0x40, 0xCF, 0xB5, 0x84, + 0x93, 0xB7, 0x8F, 0xDB, 0xE3, 0x70, 0x17, 0xE1, 0x0E, 0x70, 0x0E, 0x6A, 0x5D, 0x41, 0xA5, 0xAD, + 0x78, 0xAF, 0xEC, 0x50, 0xC3, 0xF8, 0x80, 0xBB, 0x07, 0x3F, 0xAA, 0xBB, 0x02, 0xF3, 0x51, 0x64, + 0xF2, 0x3D, 0x36, 0x69, 0xAE, 0x54, 0x8B, 0xFA, 0x13, 0xE8, 0x12, 0x51, 0x5A, 0x2C, 0x89, 0x8A, + 0x43, 0x5F, 0x9B, 0xDE, 0x33, 0xD2, 0x2C, 0xCF, 0xAC, 0x92, 0x65, 0x37, 0x47, 0x96, 0x55, 0x43, + 0xB7, 0x64, 0x52, 0x99, 0xEC, 0x91, 0x77, 0x80, 0x05, 0x8C, 0x59, 0x53, 0x22, 0x79, 0x47, 0xD0, + 0x1B, 0x01, 0xD6, 0x97, 0xF5, 0x4A, 0x02, 0x90, 0xE6, 0xA5, 0xE9, 0x25, 0x8C, 0x94, 0x55, 0x60, + 0x09, 0x0C, 0x2D, 0xF0, 0xA5, 0xFF, 0x97, 0x07, 0x84, 0x24, 0x9A, 0x9A, 0x5D, 0xEC, 0xA3, 0x2C, + 0xF4, 0xB7, 0x91, 0xD2, 0x59, 0x54, 0x39, 0x51, 0xFA, 0x9A, 0x96, 0x1B, 0xD1, 0xC7, 0x30, 0xBF, + 0x0E, 0x00, 0xA6, 0xBC, 0x56, 0x27, 0xC8, 0x66, 0x8E, 0x07, 0xE1, 0x02, 0xB6, 0xEF, 0x68, 0xDF, + 0x52, 0x2C, 0x6B, 0xF1, 0xE7, 0xB6, 0xC4, 0x38, 0x0F, 0x31, 0xBC, 0x02, 0x2B, 0x66, 0x6D, 0x6E, + 0x15, 0xB4, 0x01, 0xC8, 0xD1, 0xEF, 0x3F, 0x73, 0x4B, 0xE3, 0x9A, 0x09, 0x96, 0x8C, 0x65, 0x72, + 0x99, 0x88, 0xC6, 0x1E, 0x67, 0x97, 0x4D, 0xE6, 0x7F, 0x76, 0x8B, 0x9D, 0x1A, 0xAC, 0xBD, 0xB4, + 0xCF, 0x05, 0x32, 0xA4, 0x28, 0x90, 0x34, 0xA4, 0x05, 0x40, 0xF6, 0x58, 0x45, 0xCC, 0x59, 0x50, + 0xAF, 0xFF, 0x27, 0x87, 0x02, 0x26, 0xF6, 0xC6, 0xCF, 0xFF, 0x0D, 0x2E, 0xCC, 0x8D, 0x50, 0x3E, + 0x68, 0xCD, 0x4D, 0xB1, 0xF5, 0xCF, 0x0D, 0xCA, 0x63, 0x10, 0x9F, 0x43, 0xD0, 0x86, 0xA4, 0xA4, + 0x36, 0xD4, 0xC2, 0x7D, 0x06, 0xA3, 0x8F, 0x1A, 0xCE, 0xFD, 0x46, 0x90, 0xFE, 0x95, 0xDD, 0xCE, + 0xBE, 0x6F, 0x6A, 0x9B, 0x40, 0xCC, 0x2D, 0xEF, 0xF2, 0xFE, 0x26, 0x98, 0x23, 0x5A, 0x0D, 0xAD, + 0xF3, 0x46, 0xB0, 0x91, 0x30, 0x6B, 0xCA, 0x2B, 0xB5, 0x1C, 0xE7, 0x61, 0x1C, 0x3F, 0x6F, 0x5B, + 0xB0, 0xEE, 0xF0, 0xF5, 0x42, 0x6A, 0x74, 0xB3, 0x5D, 0x13, 0x32, 0x30, 0x99, 0x51, 0x53, 0x0E, + 0xC3, 0x4E, 0xFA, 0x56, 0x0A, 0x02, 0x38, 0xA8, 0x06, 0x8F, 0xD8, 0x4B, 0x21, 0xF5, 0xFC, 0x0F, + 0x36, 0x09, 0x35, 0x50, 0x37, 0x77, 0xD2, 0x1A, 0x65, 0xED, 0x89, 0xEF, 0x89, 0x9F, 0xB9, 0x5B, + 0x2D, 0x6F, 0xF5, 0x2E, 0xF2, 0x4F, 0xEA, 0x93, 0xA5, 0x7E, 0xB9, 0x16, 0xB1, 0xA2, 0x68, 0x2C, + 0x93, 0xEA, 0x06, 0x28, 0x37, 0x7B, 0xDE, 0x9B, 0x1A, 0x75, 0x5D, 0x02, 0x8A, 0xB8, 0x9F, 0xE0, + 0x65, 0x6B, 0x15, 0x19, 0x89, 0x7F, 0xBD, 0x6D, 0xA3, 0xAB, 0x36, 0x50, 0xBA, 0x99, 0xAA, 0xE6, + 0x7E, 0x29, 0x4C, 0x01, 0x28, 0x4C, 0xBB, 0xD5, 0x15, 0x51, 0x50, 0xDE, 0x17, 0x2C, 0xFF, 0x90, + 0x9D, 0x2F, 0xFA, 0xED, 0x41, 0x23, 0xA2, 0x70, 0x42, 0xE4, 0x51, 0xE9, 0x1E, 0x32, 0x38, 0xA4, + 0x63, 0x05, 0xF5, 0x70, 0x16, 0x56, 0x95, 0x3C, 0xC8, 0x36, 0xDB, 0xCC, 0xA8, 0xF7, 0x70, 0x2F, + 0x5D, 0x15, 0x65, 0x01, 0x96, 0xC3, 0xC9, 0x67, 0x54, 0xEC, 0x40, 0xA4, 0xA1, 0x09, 0x46, 0x22, + 0x7D, 0x8F, 0x35, 0xC9, 0xB6, 0x15, 0xB5, 0x18, 0x4D, 0x3A, 0x43, 0xCF, 0x9F, 0x90, 0x9B, 0x56, + 0x5E, 0x48, 0x54, 0x2D, 0x82, 0x78, 0x0A, 0x0E, 0x29, 0x8B, 0x98, 0x03, 0xAC, 0x73, 0x97, 0x39, + 0xDB, 0xA0, 0x7D, 0x31, 0x12, 0xA8, 0xDC, 0x91, 0x56, 0xA4, 0xC0, 0x92, 0xB0, 0x94, 0xD0, 0xD9, + 0x5F, 0x6E, 0xF7, 0xCE, 0x55, 0xBF, 0xC7, 0x35, 0x47, 0x3B, 0x83, 0x90, 0xFE, 0x83, 0x29, 0xCC, + 0xD9, 0x28, 0xAC, 0x13, 0x86, 0x9D, 0x27, 0x71, 0xE6, 0xFE, 0xFC, 0xC9, 0xE6, 0xF5, 0x1E, 0xB5, + 0xE7, 0x30, 0xA5, 0x58, 0x03, 0xD0, 0xA3, 0x87, 0x60, 0x0A, 0x01, 0x3F, 0xE4, 0x2C, 0x7B, 0x4C, + 0x42, 0xBC, 0xF8, 0x8E, 0x9D, 0x01, 0x09, 0x0F, 0x85, 0x0B, 0x82, 0x0D, 0x7C, 0x76, 0xF2, 0x8D, + 0xB8, 0x8E, 0x27, 0xA9, 0xAD, 0x8A, 0x95, 0x38, 0x44, 0xC9, 0x1B, 0x96, 0x75, 0xD0, 0x6D, 0x51, + 0x93, 0x44, 0x25, 0x5A, 0x37, 0x84, 0x37, 0x16, 0x81, 0xF9, 0xF9, 0x11, 0x15, 0x1D, 0xB8, 0xFE, + 0x23, 0x59, 0xAC, 0xF5, 0xB2, 0x91, 0x30, 0xB3, 0x61, 0x86, 0x1B, 0xAD, 0x9F, 0xA7, 0xEE, 0x92, + 0x37, 0xF1, 0xE3, 0x88, 0x4C, 0x3D, 0x3A, 0x78, 0xC9, 0x16, 0x71, 0x4D, 0x99, 0x46, 0x68, 0x00, + 0xA4, 0x85, 0x0F, 0x3B, 0x0C, 0x2A, 0xDC, 0x93, 0x23, 0x1A, 0x38, 0x60, 0x28, 0x6A, 0x41, 0x59, + 0x57, 0x6E, 0xBA, 0x38, 0x72, 0x1F, 0x65, 0x3D, 0x22, 0xD0, 0x6C, 0xE9, 0x37, 0xE2, 0x51, 0xCA, + 0x2B, 0x43, 0x3D, 0x63, 0x3C, 0x58, 0x2F, 0x5D, 0x98, 0x24, 0x15, 0x79, 0x16, 0x76, 0xBF, 0x3F, + 0x20, 0xC3, 0xF2, 0x4C, 0x53, 0x72, 0xB8, 0x91, 0x6B, 0x01, 0xE1, 0x1D, 0xD2, 0x27, 0xB0, 0xA1, + 0xA1, 0x1D, 0x32, 0x08, 0x7D, 0xC4, 0x99, 0x86, 0x6C, 0x3A, 0x77, 0x0E, 0x3B, 0xCE, 0xC4, 0x6E, + 0x09, 0x44, 0xF7, 0x33, 0xBA, 0xA7, 0x4A, 0xF4, 0xAD, 0x26, 0xC6, 0x00, 0x42, 0x0B, 0xAE, 0x4A, + 0x21, 0x68, 0x93, 0x86, 0x66, 0xA2, 0xCF, 0xB5, 0x85, 0xE3, 0x72, 0xE7, 0x3B, 0xBA, 0xA6, 0x9F, + 0xE6, 0x2D, 0x01, 0xCB, 0x62, 0x7A, 0x5D, 0x3F, 0x97, 0xCA, 0x26, 0xEC, 0x1B, 0xB7, 0x28, 0x9D, + 0x49, 0xEE, 0xE9, 0xA1, 0x2E, 0x2F, 0xE5, 0x1E, 0x8F, 0xCB, 0x38, 0xD6, 0xA7, 0x3B, 0xAD, 0x63, + 0xF1, 0x7A, 0x6D, 0x99, 0x7F, 0x4F, 0x0F, 0xE8, 0x13, 0x21, 0xAA, 0x97, 0x2D, 0x8F, 0x54, 0x89, + 0x31, 0x44, 0xDA, 0x5F, 0x00, 0x2F, 0x1C, 0x25, 0x19, 0xA2, 0x54, 0xA6, 0xF7, 0x2A, 0x0A, 0xAA, + 0xD6, 0xF9, 0xD1, 0x3C, 0x1B, 0x23, 0xAC, 0x92, 0x4B, 0x18, 0xC3, 0x36, 0xB7, 0xDF, 0xFE, 0x10, + 0xEC, 0xF6, 0x96, 0xA4, 0x33, 0x6D, 0xD8, 0xB7, 0x9A, 0xE5, 0xF8, 0x93, 0xA1, 0x88, 0xC4, 0xAD, + 0x61, 0x15, 0xC7, 0xC0, 0x4D, 0xAD, 0x3B, 0x9E, 0xA3, 0x5E, 0x84, 0x4B, 0x26, 0x7F, 0xC6, 0x3A, + 0xDB, 0x90, 0x27, 0x26, 0xFA, 0x42, 0x9D, 0x31, 0xB3, 0x67, 0x94, 0x5A, 0x76, 0x6F, 0xFE, 0x27, + 0x52, 0x64, 0x3F, 0x60, 0xB8, 0xF5, 0xF0, 0x95, 0x0B, 0x9F, 0xA3, 0x0A, 0x96, 0xB8, 0xEE, 0x78, + 0x1F, 0x42, 0x2D, 0xF2, 0x2E, 0x90, 0xCE, 0x62, 0x1F, 0xA2, 0x65, 0xFD, 0x66, 0x21, 0x64, 0xC9, + 0xAD, 0x6A, 0xAC, 0xD9, 0x86, 0x1B, 0x16, 0x06, 0x73, 0x22, 0x56, 0x6C, 0x09, 0x1B, 0xCD, 0x8E, + 0xF8, 0xCE, 0xCA, 0xB2, 0xD5, 0x12, 0xF7, 0x4B, 0x74, 0x50, 0xD1, 0x78, 0xA4, 0x83, 0xE8, 0x38, + 0x43, 0xBE, 0xDE, 0x5A, 0x0B, 0xC3, 0x64, 0x53, 0x03, 0xC1, 0xEB, 0xA9, 0xEB, 0xCD, 0x92, 0x01, + 0xE1, 0x17, 0xB9, 0x7C, 0x2C, 0x10, 0x6D, 0xA1, 0x3F, 0x02, 0x19, 0xEA, 0x9E, 0x13, 0xAF, 0x65, + 0xF7, 0xBA, 0x9E, 0xFB, 0x29, 0xED, 0x95, 0x6F, 0xE5, 0xAA, 0x8B, 0x78, 0xFB, 0xE2, 0xB1, 0x9E, + 0x76, 0xC1, 0xF7, 0x02, 0x70, 0x58, 0x05, 0x6B, 0xB6, 0x8D, 0x29, 0x24, 0xD8, 0x1D, 0x7D, 0x64, + 0x7F, 0x70, 0x29, 0x8D, 0x3A, 0xF1, 0x4B, 0x2C, 0x52, 0xB3, 0x4A, 0x9A, 0x0B, 0xF0, 0x55, 0x8F, + 0xC1, 0xC8, 0x9B, 0x9B, 0x30, 0xB6, 0xCF, 0x08, 0xAA, 0x41, 0x60, 0xDE, 0xC1, 0x00, 0xA8, 0xDB, + 0x2D, 0xA7, 0xDE, 0x84, 0xAC, 0x92, 0xFC, 0x36, 0xE7, 0x17, 0x39, 0x09, 0x03, 0xFC, 0xB0, 0x0A, + 0x68, 0x62, 0xD0, 0xB1, 0xC0, 0xF9, 0x4A, 0x0D, 0xB5, 0x4C, 0xB8, 0xA5, 0xB6, 0xF1, 0xE4, 0x07, + 0x08, 0x2A, 0xDA, 0xDB, 0x5A, 0x1F, 0xD0, 0xA7, 0x01, 0xBB, 0x3C, 0x96, 0x6F, 0xCF, 0xCB, 0x0D, + 0xD7, 0xC3, 0x59, 0x30, 0xA3, 0xF1, 0x41, 0x4C, 0xC5, 0x8A, 0x88, 0xF0, 0xDC, 0xC6, 0x71, 0xEB, + 0x00, 0x79, 0xC2, 0x30, 0x27, 0xDC, 0xE7, 0xDE, 0x38, 0x87, 0xDD, 0x55, 0xB7, 0x9A, 0xF3, 0xB4, + 0x24, 0xBF, 0x26, 0x55, 0xBF, 0xCA, 0xE2, 0xCD, 0x01, 0x92, 0x04, 0x84, 0x41, 0xCE, 0x2A, 0xB2, + 0x44, 0xBE, 0x52, 0x5F, 0x07, 0xC2, 0x0F, 0x22, 0x64, 0x70, 0xC8, 0x5E, 0xFF, 0x9F, 0xCE, 0xCB, + 0x34, 0xC7, 0x4F, 0xD9, 0xA3, 0x7B, 0xBE, 0xAD, 0x8B, 0x10, 0x32, 0xAA, 0x39, 0xBE, 0xB1, 0x7C, + 0x16, 0x2E, 0x32, 0x61, 0x97, 0x3A, 0xE8, 0x41, 0x5F, 0x76, 0xE4, 0xAB, 0x55, 0x6B, 0x1D, 0x55, + 0x1A, 0x06, 0x9F, 0x17, 0x68, 0x23, 0x0D, 0x43, 0x27, 0xBC, 0xDC, 0x9F, 0x8D, 0x0F, 0xC4, 0x11, + 0xD3, 0x1F, 0xF3, 0x63, 0x0A, 0x19, 0x7F, 0x6F, 0xC6, 0xDE, 0x9F, 0xB6, 0x1F, 0x64, 0x21, 0x3A, + 0x76, 0xBD, 0x2F, 0x67, 0x1E, 0x2D, 0xFE, 0x24, 0x96, 0x01, 0x32, 0x7E, 0x07, 0xEA, 0xA8, 0x68, + 0x33, 0x00, 0xA0, 0x98, 0x1E, 0xBA, 0x69, 0xEF, 0xF8, 0x17, 0xF1, 0x9A, 0x09, 0x3E, 0x60, 0xEF, + 0x19, 0x95, 0xB2, 0x0F, 0xF0, 0xB9, 0xC4, 0x3B, 0x61, 0x03, 0x33, 0xE6, 0x3E, 0xDB, 0x65, 0x3E, + 0xAF, 0x09, 0x5C, 0xAB, 0xFB, 0x72, 0x6E, 0x8B, 0xAB, 0xA6, 0x6F, 0x8D, 0x6D, 0xDB, 0x13, 0xC2, + 0xF4, 0xF9, 0x25, 0x70, 0x56, 0x7F, 0xD3, 0xBE, 0x0A, 0xE3, 0x9C, 0x7A, 0x52, 0x9B, 0xC4, 0xDE, + 0xF0, 0x39, 0x70, 0xD5, 0x60, 0x65, 0x28, 0x1E, 0x99, 0x3A, 0x60, 0xA8, 0xD4, 0xED, 0xC5, 0xA7, + 0xC6, 0xF4, 0xE9, 0x1E, 0x9B, 0x6C, 0x79, 0xB0, 0x48, 0x34, 0x0E, 0x54, 0x92, 0x8D, 0x2B, 0x38, + 0x84, 0xDF, 0xCF, 0x86, 0x76, 0x6A, 0x74, 0x4B, 0xF2, 0xE8, 0x34, 0x99, 0x40, 0x31, 0x52, 0xC2, + 0xF5, 0xD8, 0x2B, 0x4E, 0x73, 0x08, 0xE1, 0x62, 0xF9, 0x84, 0xDF, 0x79, 0xC5, 0xB5, 0xB6, 0x9B, + 0xA9, 0x53, 0x16, 0xBE, 0xC6, 0x6E, 0xA6, 0x17, 0xB9, 0x55, 0xD4, 0xC7, 0xE2, 0x68, 0x86, 0x77, + 0x66, 0xBD, 0x5B, 0x98, 0xA8, 0x05, 0x84, 0x92, 0xCA, 0x8B, 0xDA, 0x30, 0x20, 0x5E, 0x93, 0x78, + 0xAD, 0xB0, 0x71, 0x4B, 0x43, 0x46, 0x3E, 0x9F, 0xE0, 0x26, 0x52, 0x6F, 0xA4, 0x28, 0xBC, 0xB0, + 0xBB, 0x6F, 0x44, 0x1F, 0xA6, 0x22, 0x9C, 0x51, 0xC3, 0xB2, 0x8C, 0x6A, 0xF5, 0x02, 0x19, 0x9F, + 0xD2, 0x1F, 0x6C, 0xC6, 0xDB, 0x21, 0xE9, 0x0F, 0xDF, 0xA7, 0xFB, 0x3A, 0xE0, 0xE9, 0x75, 0xD4, + 0x6B, 0x2F, 0x21, 0x96, 0xD2, 0x90, 0x83, 0xD4, 0x78, 0xE2, 0x34, 0x0B, 0x75, 0x9F, 0xBE, 0x93, + 0x97, 0xB6, 0xD7, 0xEF, 0xAD, 0xCF, 0xB8, 0xA6, 0xB8, 0x29, 0xBD, 0xBF, 0xC8, 0xA3, 0xE5, 0x2C, + 0x1C, 0x99, 0x40, 0x83, 0x51, 0xD5, 0x63, 0xCA, 0xC8, 0xCA, 0x67, 0x18, 0x61, 0x4D, 0x3F, 0xB7, + 0x95, 0xB6, 0x5B, 0x38, 0x0A, 0xA8, 0x60, 0x40, 0x7C, 0x06, 0xD4, 0xDE, 0xBD, 0xDE, 0x3E, 0x05, + 0x7D, 0xF5, 0xF8, 0x65, 0xE4, 0x5A, 0x42, 0x91, 0xD8, 0xFC, 0xCB, 0xA0, 0xC9, 0x91, 0x16, 0x9D, + 0x41, 0xB4, 0x16, 0x1C, 0xCE, 0x76, 0x75, 0xAC, 0x2F, 0x54, 0xF1, 0x0A, 0xEE, 0xD4, 0x88, 0xED, + 0x8F, 0xA0, 0x18, 0x53, 0x99, 0x46, 0x6E, 0x66, 0x48, 0xC8, 0x5F, 0xB9, 0xE0, 0xBF, 0xEF, 0x3E, + 0xC1, 0x64, 0xF2, 0xC4, 0xBD, 0x1B, 0x3B, 0xE5, 0x2A, 0x3E, 0x40, 0x2C, 0xE4, 0x8D, 0x32, 0xAB, + 0xC1, 0x0A, 0x28, 0xC0, 0xA5, 0xF6, 0xB5, 0xED, 0x5C, 0x68, 0x45, 0x05, 0xC0, 0x2F, 0x76, 0x95, + 0x5C, 0x16, 0x63, 0x51, 0xD0, 0x8E, 0x22, 0x2B, 0x74, 0x02, 0x10, 0xF6, 0x82, 0x00, 0x9A, 0x31, + 0x2C, 0xD0, 0xF2, 0x20, 0x44, 0x4A, 0x78, 0xE8, 0xEB, 0x04, 0x0F, 0x91, 0xDC, 0x21, 0xA5, 0xC1, + 0x9E, 0xCF, 0x78, 0x89, 0x19, 0xCE, 0xC8, 0x08, 0x91, 0x63, 0x25, 0x8C, 0x1D, 0x98, 0xB3, 0x27, + 0xB8, 0xFE, 0x55, 0x78, 0x35, 0x24, 0x96, 0x5C, 0x92, 0x9F, 0xC9, 0xD2, 0xAC, 0xCC, 0xCE, 0x65, + 0x5E, 0x36, 0xA8, 0xE2, 0x9B, 0x96, 0x69, 0x9B, 0x7F, 0xC3, 0xC4, 0x1B, 0x70, 0x76, 0xAA, 0xC0, + 0xFD, 0xBE, 0xAE, 0xDB, 0x85, 0x7F, 0x72, 0x06, 0xCE, 0x3E, 0x30, 0x5D, 0x3E, 0x24, 0x85, 0x14, + 0x8E, 0x32, 0x71, 0xEA, 0x80, 0x7E, 0x3B, 0x06, 0x65, 0xF1, 0xC8, 0xC8, 0x55, 0x2E, 0x2A, 0x9B, + 0x60, 0xF8, 0xC4, 0x1D, 0x75, 0x9F, 0xFC, 0xE4, 0x2B, 0x3F, 0x64, 0xE4, 0xFB, 0xE0, 0x24, 0x03, + 0xED, 0x8A, 0xD4, 0x7D, 0x31, 0x51, 0x1C, 0xCF, 0x61, 0xDD, 0x80, 0x05, 0x33, 0x59, 0xB0, 0xD1, + 0x83, 0x8F, 0xA8, 0xDF, 0xAC, 0x01, 0xB1, 0x2A, 0xE2, 0x5C, 0x6A, 0xA9, 0xB6, 0xCC, 0xB1, 0x03, + 0xF3, 0x7E, 0x14, 0x93, 0xB4, 0x85, 0x06, 0x33, 0x17, 0x15, 0x14, 0x75, 0xC8, 0x2B, 0xF3, 0xFF, + 0xD6, 0xAC, 0x6B, 0x32, 0xF2, 0xEC, 0x53, 0x8E, 0xEA, 0x04, 0xCB, 0xB0, 0x86, 0x1B, 0xD3, 0x75, + 0xF1, 0xF0, 0x3A, 0x56, 0xF6, 0x3F, 0x31, 0xD0, 0xC4, 0x79, 0x3C, 0x5F, 0x21, 0x73, 0xB5, 0xEA, + 0xED, 0x92, 0x0F, 0xDB, 0x32, 0x6E, 0xEC, 0x11, 0xC1, 0xF3, 0xF5, 0x36, 0x2D, 0x97, 0x5A, 0x50, + 0x13, 0x8F, 0xF7, 0x3A, 0x6E, 0x8A, 0x88, 0xB0, 0x0B, 0x11, 0x73, 0x0C, 0x7C, 0x5B, 0x02, 0xD3, + 0xDF, 0xBA, 0x4C, 0x3F, 0x6A, 0x69, 0x82, 0x68, 0x7A, 0xD1, 0xCD, 0xCA, 0xBA, 0xEB, 0xB1, 0xFD, + 0xCF, 0x45, 0x45, 0x94, 0x23, 0x58, 0xF4, 0xBF, 0x36, 0x5A, 0x6A, 0xBF, 0x0E, 0x7A, 0xF6, 0xE3, + 0x38, 0x42, 0x85, 0x55, 0x7F, 0x31, 0x49, 0x7A, 0x18, 0x21, 0x6C, 0xC9, 0xA5, 0x07, 0xAC, 0x1D, + 0x72, 0x47, 0x4E, 0x7F, 0xB7, 0x17, 0xBC, 0x7B, 0x26, 0xC5, 0x40, 0x2B, 0x95, 0xC6, 0xFF, 0xD1, + 0xCD, 0x46, 0xAB, 0xBB, 0xAB, 0xDC, 0x74, 0x60, 0x30, 0x75, 0x45, 0x49, 0xF7, 0x05, 0x52, 0xB8, + 0x32, 0xB2, 0x14, 0x8B, 0xE9, 0x10, 0x04, 0x28, 0xB4, 0x55, 0x50, 0xE4, 0x36, 0x9F, 0xF8, 0x21, + 0x24, 0xAB, 0x52, 0x19, 0x50, 0xA5, 0x7F, 0x68, 0x39, 0xDC, 0xC5, 0x8C, 0xAF, 0x10, 0xF6, 0x57, + 0x9B, 0x9B, 0x95, 0x33, 0x25, 0xC1, 0x68, 0xA5, 0xFA, 0xE7, 0xBA, 0x9D, 0xA0, 0x5E, 0x03, 0x92, + 0x14, 0x3E, 0xE9, 0xDC, 0x4F, 0x2F, 0xFD, 0x73, 0xE7, 0x49, 0x45, 0xEE, 0x80, 0x69, 0xB3, 0x1F, + 0x51, 0xE0, 0xB0, 0x1F, 0x89, 0xE0, 0xED, 0x1E, 0x89, 0x9C, 0xC3, 0xB7, 0x63, 0x05, 0x82, 0xD2, + 0xA7, 0xC7, 0xBC, 0xA5, 0x53, 0x9E, 0x17, 0x59, 0xFC, 0x8B, 0x48, 0x54, 0x32, 0x0E, 0x8E, 0x9F, + 0xC4, 0xBB, 0x43, 0x24, 0x78, 0x3A, 0xD2, 0xDF, 0xB7, 0xE0, 0xD7, 0x20, 0x9D, 0xBF, 0x49, 0xB8, + 0xD2, 0xB5, 0xA7, 0xD0, 0x1A, 0x32, 0x2B, 0x6C, 0xAD, 0xCA, 0x01, 0x16, 0xC7, 0xBD, 0x32, 0x7B, + 0x5A, 0x82, 0xB1, 0x2B, 0x42, 0x81, 0x4F, 0xB3, 0x91, 0x25, 0xB4, 0x91, 0x91, 0xD0, 0xB8, 0xB1, + 0x51, 0x24, 0x62, 0x92, 0x3D, 0x77, 0x3F, 0x25, 0x68, 0x9A, 0x34, 0x41, 0x33, 0xFF, 0xA0, 0xAF, + 0x1A, 0x14, 0x90, 0xE8, 0xC6, 0x36, 0x4F, 0xC5, 0xAF, 0xDC, 0x66, 0x2C, 0x1A, 0x73, 0x40, 0x5E, + 0xE8, 0x94, 0xD9, 0xC2, 0x27, 0x7F, 0xD7, 0x34, 0x06, 0xC4, 0x89, 0x00, 0xBF, 0x1A, 0x6A, 0x24, + 0x1A, 0x31, 0xEC, 0x91, 0xCD, 0xBB, 0x97, 0xBF, 0x50, 0x74, 0x24, 0xED, 0x0D, 0x72, 0xA5, 0xEA, + 0x43, 0x9E, 0x83, 0x33, 0x19, 0x8B, 0x97, 0x66, 0x9E, 0x43, 0xBA, 0xF2, 0x0A, 0x53, 0x37, 0xEF, + 0xE9, 0x60, 0x76, 0x98, 0x66, 0x0A, 0x21, 0x8D, 0xCA, 0x85, 0xB9, 0x9F, 0x91, 0xCC, 0x0D, 0xA3, + 0xCF, 0xB9, 0x65, 0xEA, 0x50, 0xD9, 0x8A, 0x0C, 0xAF, 0xA1, 0xB3, 0x3C, 0xBA, 0xA1, 0x6B, 0xBA, + 0x53, 0x37, 0xCA, 0x57, 0xF2, 0x55, 0x73, 0x12, 0x63, 0x61, 0x98, 0x01, 0x2C, 0x6E, 0x4A, 0x66, + 0xCC, 0x35, 0xA8, 0x86, 0x52, 0x45, 0xC0, 0xD4, 0x88, 0xDA, 0x00, 0xFF, 0x86, 0x84, 0x97, 0x1C, + 0x5B, 0x16, 0x96, 0x12, 0x77, 0x45, 0x81, 0x8F, 0xA2, 0xCC, 0x55, 0x12, 0x8D, 0x8C, 0xBD, 0xA8, + 0x61, 0xBA, 0x8D, 0x0C, 0xD4, 0x58, 0xC9, 0x03, 0xE4, 0x0C, 0xBE, 0x53, 0x4C, 0x9E, 0xA0, 0x58, + 0x6F, 0x16, 0xEB, 0x9F, 0x89, 0xB8, 0x94, 0x35, 0x58, 0xF8, 0x83, 0x94, 0x3D, 0x49, 0x44, 0x88, + 0x82, 0x18, 0xCB, 0xA9, 0x4B, 0x2D, 0x08, 0xAD, 0x8E, 0xCF, 0x08, 0x8F, 0xF3, 0x08, 0x3B, 0xFF, + 0x10, 0x80, 0x34, 0x31, 0xE7, 0xF9, 0xB9, 0x52, 0xD8, 0x78, 0xD0, 0x0F, 0x5E, 0xB7, 0xE2, 0xF9, + 0x37, 0xCB, 0xD4, 0x49, 0x36, 0x67, 0xF2, 0xE0, 0x78, 0xB9, 0x13, 0x89, 0xEC, 0x85, 0xFA, 0x6D, + 0x74, 0x5E, 0xC5, 0x59, 0xB6, 0xA9, 0xEE, 0x1C, 0x0E, 0xD4, 0xA3, 0x1E, 0x7A, 0x09, 0x0D, 0x4F, + 0xB8, 0x2D, 0xBE, 0x0C, 0xF7, 0x69, 0x04, 0x29, 0x44, 0x27, 0x94, 0x72, 0xC0, 0xAB, 0x86, 0x40, + 0x21, 0x5E, 0xC6, 0xBD, 0x24, 0x4A, 0x5E, 0x06, 0x46, 0x53, 0xA1, 0xD7, 0xD7, 0xBC, 0xD9, 0x97, + 0x13, 0xA9, 0x09, 0x15, 0x33, 0xBC, 0x9B, 0x13, 0x50, 0xCE, 0xA7, 0xDC, 0xFA, 0x69, 0x70, 0x22, + 0x14, 0x79, 0xD0, 0xA8, 0x3C, 0xB3, 0x46, 0xC3, 0xDA, 0x6C, 0x0C, 0xEC, 0x2A, 0xB2, 0x9B, 0x21, + 0xB2, 0xAD, 0x8C, 0x0C, 0x85, 0x9A, 0x8D, 0x7C, 0x10, 0xEA, 0x51, 0x1D, 0x2D, 0xDE, 0x7D, 0x8F}; + +const long int default_keys_enc_size = sizeof(default_keys_enc); diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 0b2058293..d988dbb83 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -34,6 +34,7 @@ namespace Loader { using namespace Common::Literals; static constexpr u64 UPDATE_TID_HIGH = 0x0004000e00000000; +static constexpr u64 DLP_CHILD_TID_HIGH = 0x0004000100000000; FileType AppLoader_NCCH::IdentifyType(FileUtil::IOFile* file) { u32 magic; @@ -314,12 +315,16 @@ ResultStatus AppLoader_NCCH::Load(std::shared_ptr& process) { LOG_INFO(Loader, "Program ID: {}", program_id); - u64 update_tid = (ncch_program_id & 0xFFFFFFFFULL) | UPDATE_TID_HIGH; - update_ncch.OpenFile( - Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, update_tid)); - result = update_ncch.Load(); - if (result == ResultStatus::Success) { - overlay_ncch = &update_ncch; + bool is_dlp_child = (ncch_program_id & 0xFFFFFFFF00000000) == DLP_CHILD_TID_HIGH; + + if (!is_dlp_child) { + u64 update_tid = (ncch_program_id & 0xFFFFFFFFULL) | UPDATE_TID_HIGH; + update_ncch.OpenFile( + Service::AM::GetTitleContentPath(Service::FS::MediaType::SDMC, update_tid)); + result = update_ncch.Load(); + if (result == ResultStatus::Success) { + overlay_ncch = &update_ncch; + } } if (auto room_member = Network::GetRoomMember().lock()) { From 4c054ff2e7e71f8adf46af22f38eb4740ad57198 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Sun, 22 Feb 2026 22:41:24 +0100 Subject: [PATCH 18/94] video_core: Fix transferability issue in vulkan shader disk cache (#1770) --- .../renderer_vulkan/vk_shader_disk_cache.cpp | 52 +++++-------------- .../renderer_vulkan/vk_shader_disk_cache.h | 21 +++++--- 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp index 15e45ce81..bc21dc7c1 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp @@ -713,7 +713,10 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, // New config entry, usually always taken unless there is duplicate entries on the cache // for some reason. - auto shader_it = programmable_vertex_cache.find(entry->spirv_entry_id); + // We cannot trust the SPIRV entry ID anymore if we are regenerating. + auto shader_it = regenerate_file + ? programmable_vertex_cache.end() + : programmable_vertex_cache.find(entry->spirv_entry_id); if (shader_it != programmable_vertex_cache.end()) { // The config entry uses a SPIRV entry that was already compiled (this is the usual // path when the cache doesn't need to be re-generated). @@ -722,40 +725,7 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, entry->spirv_entry_id); iter_config->second = &shader_it->second; - - if (regenerate_file) { - // In case we are re-generating the cache, we could only have gotten here if the - // SPIRV was already compiled and cached, so only cache the config. - AppendVSConfig(*regenerate_file, *entry, curr.Id()); - } - - bool new_program = known_vertex_programs.emplace(entry->program_entry_id).second; - if (new_program && regenerate_file) { - // If the vertex program is not known at this point we need to save it as well. - // This can happen to config entries that compile to the same SPIRV but use - // different program code (maybe because garbage data was in the program - // buffer). - auto program_it = pending_programs.find(entry->program_entry_id); - if (program_it == pending_programs.end()) { - // Program code not in disk cache, should never happen. - LOG_ERROR(Render_Vulkan, "Missing program code for config entry"); - programmable_vertex_map.erase(iter_config); - continue; - } - - // This is very rare so no need to use the LRU. - auto program_cache_entry = vs_cache.ReadAt(program_it->second); - const VSProgramEntry* program_entry; - - if (!program_cache_entry.Valid() || - program_cache_entry.Type() != CacheEntryType::VS_PROGRAM || - !(program_entry = program_cache_entry.Payload()) || - program_entry->version != VSProgramEntry::EXPECTED_VERSION) { - MALFORMED_DISK_CACHE; - } - - AppendVSProgram(*regenerate_file, *program_entry, entry->program_entry_id); - } + known_vertex_programs.emplace(entry->program_entry_id).second; } else { // Cached SPIRV not found, need to recompile. @@ -859,8 +829,7 @@ bool ShaderDiskCache::InitVSCache(const std::atomic_bool& stop_loading, // Asign the SPIRV shader to the config iter_config->second = &iter_prog->second; - LOG_DEBUG(Render_Vulkan, " linked with new SPIRV {:016X}.", - entry->spirv_entry_id); + LOG_DEBUG(Render_Vulkan, " linked with SPIRV {:016X}.", entry->spirv_entry_id); } } } @@ -1017,7 +986,7 @@ bool ShaderDiskCache::InitFSCache(const std::atomic_bool& stop_loading, tot_callback_index, "Fragment Shader"); } - LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", offset.first); if (fragment_shaders.find(offset.first) != fragment_shaders.end()) { // SPIRV of config was already compiled, no need to regenerate @@ -1250,7 +1219,7 @@ bool ShaderDiskCache::InitGSCache(const std::atomic_bool& stop_loading, tot_callback_index, "Geometry Shader"); } - LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", curr.Id()); + LOG_DEBUG(Render_Vulkan, "Linking {:016X}.", offset.first); if (fixed_geometry_shaders.find(offset.first) != fixed_geometry_shaders.end()) { // SPIRV of config was already compiled, no need to regenerate @@ -1400,6 +1369,11 @@ bool ShaderDiskCache::InitPLCache(const std::atomic_bool& stop_loading, // if any is missing we cannot build it. std::array shaders; + LOG_DEBUG(Render_Vulkan, " uses VS: {:016X}, FS: {:016X}, GS: {:016X}", + entry->pl_info.shader_ids[ProgramType::VS], + entry->pl_info.shader_ids[ProgramType::FS], + entry->pl_info.shader_ids[ProgramType::GS]); + if (entry->pl_info.shader_ids[ProgramType::VS]) { auto it_vs = programmable_vertex_map.find(entry->pl_info.shader_ids[ProgramType::VS]); diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h index d6b487f18..c2e44215e 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -103,7 +103,8 @@ private: struct VSConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + u64 program_entry_id; u64 spirv_entry_id; Pica::Shader::Generator::PicaVSConfig vs_config; @@ -113,34 +114,38 @@ private: struct VSProgramEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + u32 program_len; u32 swizzle_len; Pica::ProgramCode program_code; Pica::SwizzleData swizzle_code; }; - static_assert(sizeof(VSProgramEntry) == 32780); + static_assert(sizeof(VSProgramEntry) == 32784); struct FSConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + Pica::Shader::FSConfig fs_config; }; - static_assert(sizeof(FSConfigEntry) == 276); + static_assert(sizeof(FSConfigEntry) == 280); struct GSConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + Pica::Shader::Generator::PicaFixedGSConfig gs_config; }; - static_assert(sizeof(GSConfigEntry) == 44); + static_assert(sizeof(GSConfigEntry) == 48); struct PLConfigEntry { static constexpr u8 EXPECTED_VERSION = 0; - u8 version; // Surprise tool that can help us later + u64 version; // Surprise tool that can help us later + StaticPipelineInfo pl_info; }; static_assert(sizeof(PLConfigEntry) == 152); From fcb345e2734cb01de7bc9b052204edda6a1608b1 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 23 Feb 2026 14:26:20 +0100 Subject: [PATCH 19/94] logging: Check filter before log format (#1773) --- src/common/logging/backend.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp index e1b8621bd..837911981 100644 --- a/src/common/logging/backend.cpp +++ b/src/common/logging/backend.cpp @@ -333,15 +333,16 @@ public: return true; } + const Filter& GetFilter() const { + return filter; + } + void SetColorConsoleBackendEnabled(bool enabled) { color_console_backend.SetEnabled(enabled); } void PushEntry(Class log_class, Level log_level, const char* filename, unsigned int line_num, const char* function, std::string message) { - if (!filter.CheckMessage(log_class, log_level)) { - return; - } Entry new_entry = CreateEntry(log_class, log_level, filename, line_num, function, std::move(message), time_origin); if (!regex_filter.empty() && @@ -599,6 +600,9 @@ void FmtLogMessageImpl(Class log_class, Level log_level, const char* filename, } if (logging_initialized) [[likely]] { + if (!Impl::Instance().GetFilter().CheckMessage(log_class, log_level)) { + return; + } Impl::Instance().PushEntry(log_class, log_level, filename, line_num, function, fmt::vformat(format, args)); } else { From 8b72dcb235151389f82db9207ba1295fc509c3d0 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Mon, 23 Feb 2026 15:45:26 +0100 Subject: [PATCH 20/94] video_core: Do not spam file IO when reading vulkan shader disk cache (#1774) --- .../renderer_vulkan/vk_shader_disk_cache.cpp | 50 ++++++++++++++++--- .../renderer_vulkan/vk_shader_disk_cache.h | 5 ++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp index bc21dc7c1..fd8809443 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.cpp @@ -301,11 +301,11 @@ ShaderDiskCache::CacheEntry::CacheEntryHeader ShaderDiskCache::CacheFile::ReadAt CacheEntry::CacheEntryHeader header; - if (file.ReadAtArray(&header, 1, position) == sizeof(CacheEntry::CacheEntryHeader)) { - return header; + if (!ReadFromFileCached(&header, position, sizeof(header))) { + return CacheEntry::CacheEntryHeader(); } - return CacheEntry::CacheEntryHeader(); + return header; } ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) { @@ -323,8 +323,8 @@ ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) u32 payload_size = res.header.entry_size - headers_size; std::vector payload(payload_size); - if (file.ReadAtBytes(payload.data(), payload_size, - position + sizeof(CacheEntry::CacheEntryHeader)) == payload_size) { + if (ReadFromFileCached(payload.data(), (position + sizeof(CacheEntry::CacheEntryHeader)), + payload_size)) { // Decompress data if needed if (res.header.zstd_compressed) { if (Common::Compression::GetDecompressedSize(payload) < @@ -344,6 +344,7 @@ ShaderDiskCache::CacheEntry ShaderDiskCache::CacheFile::ReadAt(size_t position) size_t ShaderDiskCache::CacheFile::GetTotalEntries() { if (!file.IsGood()) { next_entry_id = SIZE_MAX; + file_size = 0; return next_entry_id; } @@ -351,7 +352,7 @@ size_t ShaderDiskCache::CacheFile::GetTotalEntries() { return next_entry_id; } - const size_t file_size = file.GetSize(); + file_size = file.GetSize(); if (file_size == 0) { next_entry_id = 0; return next_entry_id; @@ -363,6 +364,7 @@ size_t ShaderDiskCache::CacheFile::GetTotalEntries() { footer.version == CacheEntry::CacheEntryFooter::ENTRY_VERSION) { next_entry_id = footer.entry_id + 1; } else { + file_size = 0; return SIZE_MAX; } @@ -428,7 +430,9 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { switch (mode) { case CacheOpMode::READ: { - next_entry_id = SIZE_MAX; // Force reading entries agains + next_entry_id = SIZE_MAX; // Force reading entries again + cached_file_data_start = 0; + cached_file_data.clear(); file = FileUtil::IOFile(filepath, "rb"); bool is_open = file.IsGood(); if (is_open) { @@ -443,6 +447,8 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { return false; } file.Close(); + cached_file_data_start = 0; + cached_file_data.clear(); curr_mode = mode; if (next_entry_id == SIZE_MAX) { // Cannot append if getting total items fails @@ -454,6 +460,8 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { } case CacheOpMode::DELETE: { next_entry_id = SIZE_MAX; + cached_file_data_start = 0; + cached_file_data.clear(); file.Close(); curr_mode = mode; return FileUtil::Delete(filepath); @@ -473,6 +481,34 @@ bool ShaderDiskCache::CacheFile::SwitchMode(CacheOpMode mode) { return false; } +bool ShaderDiskCache::CacheFile::ReadFromFileCached(void* dst, size_t position, size_t size) { + if (!dst || position + size > file_size) { + return false; + } + + size_t offset = position - cached_file_data_start; + if (position < cached_file_data_start || offset > cached_file_data.size() || + size > cached_file_data.size() - offset) { + if (size > CacheEntry::MAX_ENTRY_SIZE) { + return false; + } + + size_t to_read = std::min(CacheEntry::MAX_ENTRY_SIZE, file_size - position); + + cached_file_data_start = position; + cached_file_data.resize(to_read); + + if (file.ReadAtBytes(cached_file_data.data(), to_read, position) != to_read) { + return false; + } + + offset = 0; + } + + std::memcpy(dst, cached_file_data.data() + offset, size); + return true; +} + std::string ShaderDiskCache::GetVSFile(u64 title_id, bool is_temp) const { return parent.GetTransferableDir() + DIR_SEP + fmt::format("{:016X}_vs", title_id) + (is_temp ? "_temp" : "") + ".vkch"; diff --git a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h index c2e44215e..e05d43d09 100644 --- a/src/video_core/renderer_vulkan/vk_shader_disk_cache.h +++ b/src/video_core/renderer_vulkan/vk_shader_disk_cache.h @@ -292,9 +292,14 @@ private: bool SwitchMode(CacheOpMode mode); private: + bool ReadFromFileCached(void* dst, size_t absolute_pos, size_t size); + CacheOpMode curr_mode = CacheOpMode::NONE; std::string filepath; FileUtil::IOFile file{}; + size_t file_size; + size_t cached_file_data_start{}; + std::vector cached_file_data; std::atomic next_entry_id = SIZE_MAX; Common::ThreadWorker append_worker{1, "Disk Shader Cache Append Worker"}; }; From 5d583a8a411d3c1c91e70ff64b80b9d7ffe20616 Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 23 Feb 2026 15:55:50 +0000 Subject: [PATCH 21/94] .gitlab-ci.yml: Bump macOS libretro core minimum OS version to 11.0 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e8f4f141b..75a6bfa5b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -82,7 +82,7 @@ libretro-build-osx-x64: - mac-apple-silicon variables: CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64 - MACOSX_DEPLOYMENT_TARGET: "10.15" + MACOSX_DEPLOYMENT_TARGET: "11.0" extends: - .core-defs - .libretro-osx-cmake-x86_64 From 76db4b08f69c7f0c42055bf7454453e4bd7f898f Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 23 Feb 2026 18:25:05 +0000 Subject: [PATCH 22/94] .gitlab-ci.yml: Fixed ARM64 macOS not having minimum OS correctly set --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 75a6bfa5b..64cafa617 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -92,6 +92,8 @@ libretro-build-osx-arm64: extends: - .core-defs - .libretro-osx-cmake-arm64 + variables: + MACOSX_DEPLOYMENT_TARGET: "11.0" ################################### CELLULAR ################################# # Android ARMv8a From 13e0fdeac1d11dcbfe499fa704eb2d4e251c9db5 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:35:02 +0100 Subject: [PATCH 23/94] libretro core: Add some ifdefs (#1765) --- src/audio_core/input_details.h | 2 ++ src/audio_core/sink_details.h | 2 ++ src/common/file_util.h | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/audio_core/input_details.h b/src/audio_core/input_details.h index 709453eac..fcefb8343 100644 --- a/src/audio_core/input_details.h +++ b/src/audio_core/input_details.h @@ -24,7 +24,9 @@ enum class InputType : u32 { Static = 2, Cubeb = 3, OpenAL = 4, +#ifdef HAVE_LIBRETRO LibRetro = 5, +#endif }; struct InputDetails { diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h index a3e7a4edb..299aee459 100644 --- a/src/audio_core/sink_details.h +++ b/src/audio_core/sink_details.h @@ -20,7 +20,9 @@ enum class SinkType : u32 { Cubeb = 2, OpenAL = 3, SDL2 = 4, +#ifdef HAVE_LIBRETRO LibRetro = 5, +#endif }; struct SinkDetails { diff --git a/src/common/file_util.h b/src/common/file_util.h index 58cc4f7aa..ea4af8cb3 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -15,7 +15,9 @@ #include #include #include +#ifdef HAVE_LIBRETRO #include +#endif #include #include #include From fe59958b63d3bed4cf085befcb56385d212e1d0f Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 15:49:42 -0500 Subject: [PATCH 24/94] older tvos hardware does not support layered rendering --- src/video_core/renderer_vulkan/vk_instance.cpp | 14 ++++++++++++++ .../renderer_vulkan/vk_texture_runtime.cpp | 3 +++ 2 files changed, 17 insertions(+) diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp index 3f968e4c9..a2c2cb6be 100644 --- a/src/video_core/renderer_vulkan/vk_instance.cpp +++ b/src/video_core/renderer_vulkan/vk_instance.cpp @@ -618,6 +618,20 @@ bool Instance::CreateDevice() { #undef PROP_GET #undef FEAT_SET + // Check layered rendering support on MoltenVK + // MoltenVK maps Metal's layeredRendering capability to shaderOutputLayer + if (is_moltenvk) { + vk::PhysicalDeviceVulkan12Features vulkan12_features; + vk::PhysicalDeviceFeatures2 features2; + features2.pNext = &vulkan12_features; + physical_device.getFeatures2(&features2); + if (!vulkan12_features.shaderOutputLayer) { + LOG_INFO(Render_Vulkan, + "Disabling layered rendering (shaderOutputLayer not supported by device)"); + layered_rendering_supported = false; + } + } + #ifdef HAVE_LIBRETRO // LibRetro builds: device already created by frontend, just return after feature detection return true; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 86f46787e..8bfa0e30f 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -169,6 +169,9 @@ void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, std::string_view debug_name) { const bool is_cube_map = type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); + if (!is_cube_map) { + flags &= ~vk::ImageCreateFlagBits::eCubeCompatible; + } this->instance = instance; this->width = width; From 8fac24d2a414cf362b3d93ab2e5dd310f8caa2b3 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 16:29:08 -0500 Subject: [PATCH 25/94] libretro: better load failure check --- src/citra_libretro/citra_libretro.cpp | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/citra_libretro/citra_libretro.cpp b/src/citra_libretro/citra_libretro.cpp index 1fb73cb23..6028349ae 100644 --- a/src/citra_libretro/citra_libretro.cpp +++ b/src/citra_libretro/citra_libretro.cpp @@ -239,6 +239,14 @@ static void UpdateSettings() { * libretro callback; Called every game tick. */ void retro_run() { + if (!emu_instance->game_loaded) { + // Game failed to load (e.g. encrypted ROM, bad path). + // Present an empty frame so RetroArch doesn't hang. + LibRetro::PollInput(); + LibRetro::UploadVideoFrame(nullptr, 0, 0, 0); + return; + } + // Check to see if we actually have any config updates to process. if (LibRetro::HasUpdatedConfig()) { LibRetro::ParseCoreOptions(); @@ -527,6 +535,40 @@ bool retro_load_game(const struct retro_game_info* info) { // the graphics context ready and available before calling System::Load. LibRetro::settings.file_path = info->path; + // Early validation: check that the ROM can be loaded before committing to + // the HW renderer setup. Without this, failures (encrypted ROMs, bad files) + // are only detected in context_reset after retro_load_game already returned + // true, leaving the frontend stuck on a black screen. + // GetLoader + LoadKernelMemoryMode only read ROM headers — no renderer needed. + { + auto loader = Loader::GetLoader(LibRetro::settings.file_path); + if (!loader) { + LibRetro::DisplayMessage("Failed to obtain loader for the specified ROM."); + return false; + } + auto [memory_mode, result] = loader->LoadKernelMemoryMode(); + if (result != Loader::ResultStatus::Success) { + switch (result) { + case Loader::ResultStatus::ErrorEncrypted: + LibRetro::DisplayMessage( + "This ROM is encrypted and must be decrypted before use with Azahar."); + break; + case Loader::ResultStatus::ErrorInvalidFormat: + LibRetro::DisplayMessage("The ROM format is not supported."); + break; + case Loader::ResultStatus::ErrorGbaTitle: + LibRetro::DisplayMessage("GBA Virtual Console titles are not supported."); + break; + default: + LibRetro::DisplayMessage("Failed to load ROM metadata."); + break; + } + return false; + } + // Stash the loader so System::Load can reuse it instead of re-opening + Core::System::GetInstance().RegisterAppLoaderEarly(loader); + } + if (!LibRetro::SetPixelFormat(RETRO_PIXEL_FORMAT_XRGB8888)) { LibRetro::DisplayMessage("XRGB8888 is not supported."); return false; From 27c3e0e5c39b7bb43f23130a0fcb59afde28ac30 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 16:29:56 -0500 Subject: [PATCH 26/94] libretro: better safety on vkDevice feature checks --- src/citra_libretro/libretro_vk.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/citra_libretro/libretro_vk.cpp b/src/citra_libretro/libretro_vk.cpp index eed09d5bc..2a9c6d9bf 100644 --- a/src/citra_libretro/libretro_vk.cpp +++ b/src/citra_libretro/libretro_vk.cpp @@ -115,11 +115,24 @@ bool CreateVulkanDevice(struct retro_vulkan_context* context, VkInstance instanc } } - // Request features we need (these will be OR'd with frontend requirements) - // The Instance class will validate these against actual device capabilities - merged_features.geometryShader = VK_TRUE; // Used for certain rendering effects - merged_features.logicOp = VK_TRUE; // Used for blending modes - merged_features.samplerAnisotropy = VK_TRUE; // Used for texture filtering + // Query actual device features so we only request what's supported + PFN_vkGetPhysicalDeviceFeatures vkGetPhysicalDeviceFeatures = + (PFN_vkGetPhysicalDeviceFeatures)get_instance_proc_addr(instance, + "vkGetPhysicalDeviceFeatures"); + VkPhysicalDeviceFeatures device_features{}; + vkGetPhysicalDeviceFeatures(gpu, &device_features); + + // Request features we want, gated by actual device support + if (device_features.geometryShader) + merged_features.geometryShader = VK_TRUE; + if (device_features.logicOp) + merged_features.logicOp = VK_TRUE; + if (device_features.samplerAnisotropy) + merged_features.samplerAnisotropy = VK_TRUE; + if (device_features.fragmentStoresAndAtomics) + merged_features.fragmentStoresAndAtomics = VK_TRUE; + if (device_features.shaderClipDistance) + merged_features.shaderClipDistance = VK_TRUE; // Find queue family with graphics support PFN_vkGetPhysicalDeviceQueueFamilyProperties vkGetPhysicalDeviceQueueFamilyProperties = From 17f4c52e56e3a19b84d0ee2e8b9e620f4b0a3aee Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Mon, 23 Feb 2026 16:31:33 -0500 Subject: [PATCH 27/94] libretro: default system type to New 3DS --- src/citra_libretro/core_settings.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/citra_libretro/core_settings.cpp b/src/citra_libretro/core_settings.cpp index 86e7098f6..b155a9b0f 100644 --- a/src/citra_libretro/core_settings.cpp +++ b/src/citra_libretro/core_settings.cpp @@ -172,11 +172,11 @@ static constexpr retro_core_option_v2_definition option_definitions[] = { nullptr, config::category::system, { - { "Old 3DS", "Original 3DS" }, { "New 3DS", "New 3DS" }, + { "Old 3DS", "Original 3DS" }, { nullptr, nullptr } }, - "Old 3DS" + "New 3DS" }, { config::system::region, @@ -779,7 +779,7 @@ static Service::CFG::SystemLanguage GetLanguageValue(const std::string& name) { static void ParseSystemOptions(void) { Settings::values.is_new_3ds = - LibRetro::FetchVariable(config::system::is_new_3ds, "Old 3DS") == "New 3DS"; + LibRetro::FetchVariable(config::system::is_new_3ds, "New 3DS") == "New 3DS"; Settings::values.region_value = GetRegionValue(LibRetro::FetchVariable("citra_region_value", "Auto")); From d721cbe29bfca9fa38f2a4443234ce617c02d23c Mon Sep 17 00:00:00 2001 From: OpenSauce04 Date: Mon, 23 Feb 2026 22:16:09 +0000 Subject: [PATCH 28/94] cmake: Only add catch2 library if ENABLE_TESTS is enabled --- externals/CMakeLists.txt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index d7cf35f3b..db8de43a7 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -50,15 +50,17 @@ else() endif() # Catch2 -add_library(catch2 INTERFACE) -if(USE_SYSTEM_CATCH2) - find_package(Catch2 3.0.0 REQUIRED) -else() - set(CATCH_INSTALL_DOCS OFF CACHE BOOL "") - set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "") - add_subdirectory(catch2) +if (ENABLE_TESTS) + add_library(catch2 INTERFACE) + if(USE_SYSTEM_CATCH2) + find_package(Catch2 3.0.0 REQUIRED) + else() + set(CATCH_INSTALL_DOCS OFF CACHE BOOL "") + set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "") + add_subdirectory(catch2) + endif() + target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain) endif() -target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain) # Crypto++ if(USE_SYSTEM_CRYPTOPP) From 15bdd27b9c0f62a7c07d57a0311f14d652ca3ba1 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:06:29 +0100 Subject: [PATCH 29/94] citra-meta: Use dedicated GPU by default on AMD (#1783) --- src/citra_meta/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/citra_meta/main.cpp b/src/citra_meta/main.cpp index 92ea7756f..b7a7e5584 100644 --- a/src/citra_meta/main.cpp +++ b/src/citra_meta/main.cpp @@ -20,8 +20,10 @@ #ifdef _WIN32 extern "C" { -// tells Nvidia drivers to use the dedicated GPU by default on laptops with switchable graphics +// tells Nvidia and AMD drivers to use the dedicated GPU by default on laptops with switchable +// graphics __declspec(dllexport) unsigned long NvOptimusEnablement = 0x00000001; +__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } #endif From b3fd0b6c8912214ce863c0ba428fca8861d90e8a Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Wed, 25 Feb 2026 13:08:18 +0100 Subject: [PATCH 30/94] video_core: Apply texture filter to color surfaces (#1784) --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 8bfa0e30f..abe9c400e 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -922,7 +922,8 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .src_rect = upload.texture_rect, .dst_rect = upload.texture_rect * res_scale, }; - if (type != SurfaceType::Texture || !runtime->blit_helper.Filter(*this, blit)) { + if ((type != SurfaceType::Color && type != SurfaceType::Texture) || + !runtime->blit_helper.Filter(*this, blit)) { BlitScale(blit, true); } } From 7d19679cc536a29c21e5ecd0646ea6e02f8cb7fc Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Wed, 25 Feb 2026 19:53:03 +0100 Subject: [PATCH 31/94] video_core: vk_texture_runtime: Refactor and fix resource leak (#1790) --- .../renderer_vulkan/vk_texture_runtime.cpp | 139 +++++++++--------- .../renderer_vulkan/vk_texture_runtime.h | 62 ++++++-- 2 files changed, 116 insertions(+), 85 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index abe9c400e..52b7c6392 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -163,17 +163,15 @@ constexpr u64 DOWNLOAD_BUFFER_SIZE = 16_MiB; } // Anonymous namespace -void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, TextureType type, - vk::Format format, vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, +void Handle::Create(u32 width, u32 height, u32 levels, TextureType type, vk::Format format, + vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, std::string_view debug_name) { - const bool is_cube_map = - type == TextureType::CubeMap && instance->IsLayeredRenderingSupported(); + const bool is_cube_map = type == TextureType::CubeMap && instance.IsLayeredRenderingSupported(); if (!is_cube_map) { flags &= ~vk::ImageCreateFlagBits::eCubeCompatible; } - this->instance = instance; this->width = width; this->height = height; this->levels = levels; @@ -212,7 +210,7 @@ void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, VkImage unsafe_image{}; VkImageCreateInfo unsafe_image_info = static_cast(image_info); - VkResult result = vmaCreateImage(instance->GetAllocator(), &unsafe_image_info, &alloc_info, + VkResult result = vmaCreateImage(instance.GetAllocator(), &unsafe_image_info, &alloc_info, &unsafe_image, &allocation, nullptr); if (result != VK_SUCCESS) [[unlikely]] { LOG_CRITICAL(Render_Vulkan, "Failed allocating image with error {}", result); @@ -233,25 +231,20 @@ void Handle::Create(const Instance* instance, u32 width, u32 height, u32 levels, .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - image_views[ViewType::Sample] = instance->GetDevice().createImageView(view_info); + image_views[ViewType::Sample] = instance.GetDevice().createImageView(view_info); if (levels == 1) { image_views[ViewType::Mip0] = image_views[ViewType::Mip0]; } - if (!debug_name.empty() && instance->HasDebuggingToolAttached()) { - SetObjectName(instance->GetDevice(), image, debug_name); - SetObjectName(instance->GetDevice(), image_views[ViewType::Sample], "{} View({})", + if (!debug_name.empty() && instance.HasDebuggingToolAttached()) { + SetObjectName(instance.GetDevice(), image, debug_name); + SetObjectName(instance.GetDevice(), image_views[ViewType::Sample], "{} View({})", debug_name, vk::to_string(aspect)); } } void Handle::Destroy() { - if (!allocation || !instance) { - return; - } - - const auto device = instance->GetDevice(); - const auto allocator = instance->GetAllocator(); + const auto device = instance.GetDevice(); // Image views if (auto view = image_views[ViewType::Sample]) { @@ -277,7 +270,9 @@ void Handle::Destroy() { framebuffer = VK_NULL_HANDLE; } - vmaDestroyImage(allocator, image, allocation); + if (allocation) { + vmaDestroyImage(instance.GetAllocator(), image, allocation); + } image = VK_NULL_HANDLE; allocation = VK_NULL_HANDLE; @@ -733,8 +728,9 @@ bool TextureRuntime::NeedsConversion(VideoCore::PixelFormat format) const { } Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& params) - : SurfaceBase{params}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, - scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(pixel_format)} { + : SurfaceBase{params}, runtime{runtime_}, instance{runtime_.GetInstance()}, + scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(pixel_format)}, + handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { if (pixel_format == VideoCore::PixelFormat::Invalid || !traits.transfer_support) { return; @@ -765,22 +761,22 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param usage |= vk::ImageUsageFlagBits::eColorAttachment; } - const bool need_format_list = is_mutable && instance->IsImageFormatListSupported(); - handles[Type::Base].Create(instance, width, height, levels, texture_type, format, usage, flags, + const bool need_format_list = is_mutable && instance.IsImageFormatListSupported(); + handles[Type::Base].Create(width, height, levels, texture_type, format, usage, flags, traits.aspect, need_format_list, DebugName(false)); raw_images[num_images++] = handles[Type::Base].image; if (res_scale != 1) { - handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, - texture_type, format, usage, flags, traits.aspect, - need_format_list, DebugName(true)); + handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, + format, usage, flags, traits.aspect, need_format_list, + DebugName(true)); raw_images[num_images++] = handles[Type::Scaled].image; } current = res_scale != 1 ? Type::Scaled : Type::Base; - runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + runtime.renderpass_cache.EndRendering(); + scheduler.Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, num_images, raw_images, barriers); cmdbuf.pipelineBarrier( @@ -791,8 +787,9 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceParams& param Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface, const VideoCore::Material* mat) - : SurfaceBase{surface}, runtime{&runtime_}, instance{&runtime_.GetInstance()}, - scheduler{&runtime_.GetScheduler()}, traits{instance->GetTraits(mat->format)} { + : SurfaceBase{surface}, runtime{runtime_}, instance{runtime_.GetInstance()}, + scheduler{runtime_.GetScheduler()}, traits{instance.GetTraits(mat->format)}, + handles{Handle(instance), Handle(instance), Handle(instance), Handle(instance)} { if (!traits.transfer_support) { return; } @@ -809,26 +806,26 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface } const std::string debug_name = DebugName(false, true); - handles[Type::Base].Create(instance, mat->width, mat->height, levels, texture_type, format, - traits.usage, flags, traits.aspect, false, debug_name); + handles[Type::Base].Create(mat->width, mat->height, levels, texture_type, format, traits.usage, + flags, traits.aspect, false, debug_name); raw_images[num_images++] = handles[Type::Base].image; if (res_scale != 1) { - handles[Type::Scaled].Create(instance, mat->width, mat->height, levels, texture_type, + handles[Type::Scaled].Create(mat->width, mat->height, levels, texture_type, vk::Format::eR8G8B8A8Unorm, traits.usage, flags, traits.aspect, false, debug_name); raw_images[num_images++] = handles[Type::Scaled].image; } if (has_normal) { - handles[Type::Custom].Create(instance, mat->width, mat->height, levels, texture_type, - format, traits.usage, flags, traits.aspect, false, debug_name); + handles[Type::Custom].Create(mat->width, mat->height, levels, texture_type, format, + traits.usage, flags, traits.aspect, false, debug_name); raw_images[num_images++] = handles[Type::Custom].image; } current = res_scale != 1 ? Type::Scaled : Type::Base; - runtime->renderpass_cache.EndRendering(); - scheduler->Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { + runtime.renderpass_cache.EndRendering(); + scheduler.Record([raw_images, num_images, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, num_images, raw_images, barriers); cmdbuf.pipelineBarrier( @@ -842,7 +839,7 @@ Surface::Surface(TextureRuntime& runtime_, const VideoCore::SurfaceBase& surface void Surface::Upload(const VideoCore::BufferTextureCopy& upload, const VideoCore::StagingData& staging) { - runtime->renderpass_cache.EndRendering(); + runtime.renderpass_cache.EndRendering(); const RecordParams params = { .aspect = Aspect(), @@ -851,8 +848,8 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .src_image = Image(Type::Base), }; - scheduler->Record([buffer = runtime->upload_buffer.Handle(), format = traits.native, params, - staging, upload](vk::CommandBuffer cmdbuf) { + scheduler.Record([buffer = runtime.upload_buffer.Handle(), format = traits.native, params, + staging, upload](vk::CommandBuffer cmdbuf) { boost::container::static_vector buffer_image_copies; const auto rect = upload.texture_rect; @@ -911,7 +908,7 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, vk::DependencyFlagBits::eByRegion, {}, {}, write_barrier); }); - runtime->upload_buffer.Commit(staging.size); + runtime.upload_buffer.Commit(staging.size); if (res_scale != 1) { ASSERT_MSG(handles[Type::Scaled], "Scaled allocation missing during upload"); @@ -923,7 +920,7 @@ void Surface::Upload(const VideoCore::BufferTextureCopy& upload, .dst_rect = upload.texture_rect * res_scale, }; if ((type != SurfaceType::Color && type != SurfaceType::Texture) || - !runtime->blit_helper.Filter(*this, blit)) { + !runtime.blit_helper.Filter(*this, blit)) { BlitScale(blit, true); } } @@ -944,12 +941,12 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { .src_image = Image(type), }; - const auto [data, offset, invalidate] = runtime->upload_buffer.Map(custom_size, 0); + const auto [data, offset, invalidate] = runtime.upload_buffer.Map(custom_size, 0); std::memcpy(data, texture->data.data(), custom_size); - runtime->upload_buffer.Commit(custom_size); + runtime.upload_buffer.Commit(custom_size); - scheduler->Record([buffer = runtime->upload_buffer.Handle(), level, params, rect, - offset = offset](vk::CommandBuffer cmdbuf) { + scheduler.Record([buffer = runtime.upload_buffer.Handle(), level, params, rect, + offset = offset](vk::CommandBuffer cmdbuf) { const vk::BufferImageCopy buffer_image_copy = { .bufferOffset = offset, .bufferRowLength = 0, @@ -1005,14 +1002,14 @@ void Surface::UploadCustom(const VideoCore::Material* material, u32 level) { void Surface::Download(const VideoCore::BufferTextureCopy& download, const VideoCore::StagingData& staging) { SCOPE_EXIT({ - scheduler->Finish(); - runtime->download_buffer.Commit(staging.size); + scheduler.Finish(); + runtime.download_buffer.Commit(staging.size); }); - runtime->renderpass_cache.EndRendering(); + runtime.renderpass_cache.EndRendering(); if (pixel_format == PixelFormat::D24S8) { - runtime->blit_helper.DepthToBuffer(*this, runtime->download_buffer.Handle(), download); + runtime.blit_helper.DepthToBuffer(*this, runtime.download_buffer.Handle(), download); return; } @@ -1034,8 +1031,8 @@ void Surface::Download(const VideoCore::BufferTextureCopy& download, .src_image = Image(Type::Base), }; - scheduler->Record( - [buffer = runtime->download_buffer.Handle(), params, download](vk::CommandBuffer cmdbuf) { + scheduler.Record( + [buffer = runtime.download_buffer.Handle(), params, download](vk::CommandBuffer cmdbuf) { const auto rect = download.texture_rect; const vk::BufferImageCopy buffer_image_copy = { .bufferOffset = download.buffer_offset, @@ -1105,13 +1102,13 @@ void Surface::ScaleUp(u32 new_scale) { flags |= vk::ImageCreateFlagBits::eMutableFormat; } - handles[Type::Scaled].Create(instance, GetScaledWidth(), GetScaledHeight(), levels, - texture_type, traits.native, traits.usage, flags, traits.aspect, - false, DebugName(true)); + handles[Type::Scaled].Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, + traits.native, traits.usage, flags, traits.aspect, false, + DebugName(true)); current = Type::Scaled; - runtime->renderpass_cache.EndRendering(); - scheduler->Record( + runtime.renderpass_cache.EndRendering(); + scheduler.Record( [raw_images = std::array{Image()}, aspect = traits.aspect](vk::CommandBuffer cmdbuf) { std::array barriers; MakeInitBarriers(aspect, 1, raw_images, barriers); @@ -1176,12 +1173,12 @@ vk::ImageView Surface::CopyImageView() noexcept { if (texture_type == VideoCore::TextureType::CubeMap) { flags |= vk::ImageCreateFlagBits::eCubeCompatible; } - copy_handle.Create(instance, GetScaledWidth(), GetScaledHeight(), levels, texture_type, - traits.native, traits.usage, flags, traits.aspect, false); + copy_handle.Create(GetScaledWidth(), GetScaledHeight(), levels, texture_type, traits.native, + traits.usage, flags, traits.aspect, false); copy_layout = vk::ImageLayout::eUndefined; } - runtime->renderpass_cache.EndRendering(); + runtime.renderpass_cache.EndRendering(); const RecordParams params = { .aspect = Aspect(), @@ -1191,8 +1188,8 @@ vk::ImageView Surface::CopyImageView() noexcept { .dst_image = copy_handle.image, }; - scheduler->Record([params, copy_layout, levels = this->levels, width = GetScaledWidth(), - height = GetScaledHeight()](vk::CommandBuffer cmdbuf) { + scheduler.Record([params, copy_layout, levels = this->levels, width = GetScaledWidth(), + height = GetScaledHeight()](vk::CommandBuffer cmdbuf) { std::array pre_barriers = { vk::ImageMemoryBarrier{ .srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite, @@ -1304,7 +1301,7 @@ vk::ImageView Surface::ImageView(ViewType view_type, Type type) noexcept { .layerCount = VK_REMAINING_ARRAY_LAYERS, }, }; - handle.image_views[view_type] = instance->GetDevice().createImageView(view_info); + handle.image_views[view_type] = instance.GetDevice().createImageView(view_info); return handle.image_views[view_type]; } @@ -1321,14 +1318,14 @@ vk::Framebuffer Surface::Framebuffer(Type type) noexcept { const auto image_view = ImageView(ViewType::Mip0, type); const vk::FramebufferCreateInfo framebuffer_info = { - .renderPass = runtime->renderpass_cache.GetRenderpass(color_format, depth_format, false), + .renderPass = runtime.renderpass_cache.GetRenderpass(color_format, depth_format, false), .attachmentCount = 1u, .pAttachments = &image_view, .width = handle.width, .height = handle.height, .layers = handle.layers, }; - handle.framebuffer = instance->GetDevice().createFramebuffer(framebuffer_info); + handle.framebuffer = instance.GetDevice().createFramebuffer(framebuffer_info); return handle.framebuffer; } @@ -1342,9 +1339,9 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { const auto src_type = up_scale ? Type::Base : Type::Scaled; const auto dst_type = up_scale ? Type::Scaled : Type::Base; - scheduler->Record([src_image = Image(src_type), aspect = Aspect(), - filter = MakeFilter(pixel_format), dst_image = Image(dst_type), - blit](vk::CommandBuffer render_cmdbuf) { + scheduler.Record([src_image = Image(src_type), aspect = Aspect(), + filter = MakeFilter(pixel_format), dst_image = Image(dst_type), + blit](vk::CommandBuffer render_cmdbuf) { const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), static_cast(blit.src_rect.bottom), 0}, @@ -1439,7 +1436,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferParams& params, Surface* color, Surface* depth) - : VideoCore::FramebufferParams{params}, + : VideoCore::FramebufferParams{params}, instance{runtime.GetInstance()}, res_scale{color ? color->res_scale : (depth ? depth->res_scale : 1u)} { auto& renderpass_cache = runtime.GetRenderpassCache(); if (shadow_rendering && !color) { @@ -1490,10 +1487,14 @@ Framebuffer::Framebuffer(TextureRuntime& runtime, const VideoCore::FramebufferPa .height = height, .layers = 1, }; - framebuffer = runtime.GetInstance().GetDevice().createFramebuffer(framebuffer_info); + framebuffer = instance.GetDevice().createFramebuffer(framebuffer_info); } -Framebuffer::~Framebuffer() = default; +Framebuffer::~Framebuffer() { + if (framebuffer) { + instance.GetDevice().destroyFramebuffer(framebuffer); + } +} Sampler::Sampler(TextureRuntime& runtime, const VideoCore::SamplerParams& params) { using TextureConfig = VideoCore::SamplerParams::TextureConfig; diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index bb5b5ce91..2149060f2 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -44,20 +44,24 @@ enum ViewType { }; struct Handle { - explicit Handle() = default; + explicit Handle(const Instance& _instance) : instance(_instance) {} ~Handle() { Destroy(); } + Handle(const Handle& other) = delete; + Handle(Handle&& other) noexcept - : allocation(std::exchange(other.allocation, VK_NULL_HANDLE)), + : instance(other.instance), allocation(std::exchange(other.allocation, VK_NULL_HANDLE)), image(std::exchange(other.image, VK_NULL_HANDLE)), image_views(std::exchange(other.image_views, {})), framebuffer(std::exchange(other.framebuffer, VK_NULL_HANDLE)), width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), levels(std::exchange(other.levels, 0)), layers(std::exchange(other.layers, 0)) {} + Handle& operator=(const Handle& other) = delete; + Handle& operator=(Handle&& other) noexcept { if (this == &other) return *this; @@ -74,10 +78,9 @@ struct Handle { return *this; } - void Create(const Instance* instance, u32 width, u32 height, u32 levels, - VideoCore::TextureType type, vk::Format format, vk::ImageUsageFlags usage, - vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, bool need_format_list, - std::string_view debug_name = {}); + void Create(u32 width, u32 height, u32 levels, VideoCore::TextureType type, vk::Format format, + vk::ImageUsageFlags usage, vk::ImageCreateFlags flags, vk::ImageAspectFlags aspect, + bool need_format_list, std::string_view debug_name = {}); void Destroy(); @@ -85,7 +88,7 @@ struct Handle { return allocation; } - const Instance* instance{nullptr}; + const Instance& instance; VmaAllocation allocation{VK_NULL_HANDLE}; vk::Image image{VK_NULL_HANDLE}; @@ -269,9 +272,9 @@ private: const VideoCore::StagingData& staging); public: - TextureRuntime* runtime; - const Instance* instance; - Scheduler* scheduler; + TextureRuntime& runtime; + const Instance& instance; + Scheduler& scheduler; FormatTraits traits; std::array handles; Type current{}; @@ -291,8 +294,34 @@ public: Framebuffer(const Framebuffer&) = delete; Framebuffer& operator=(const Framebuffer&) = delete; - Framebuffer(Framebuffer&& o) noexcept = default; - Framebuffer& operator=(Framebuffer&& o) noexcept = default; + Framebuffer(Framebuffer&& other) noexcept + : instance(other.instance), images(std::exchange(other.images, {})), + image_views(std::exchange(other.image_views, {})), + framebuffer(std::exchange(other.framebuffer, VK_NULL_HANDLE)), + render_pass(std::exchange(other.render_pass, VK_NULL_HANDLE)), + framebuffer_views(std::move(other.framebuffer_views)), + aspects(std::exchange(other.aspects, {})), + formats(std::exchange( + other.formats, {VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid})), + width(std::exchange(other.width, 0)), height(std::exchange(other.height, 0)), + res_scale(std::exchange(other.res_scale, 1)) {} + + Framebuffer& operator=(Framebuffer&& other) noexcept { + + images = std::exchange(other.images, {}); + image_views = std::exchange(other.image_views, {}); + framebuffer = std::exchange(other.framebuffer, VK_NULL_HANDLE); + render_pass = std::exchange(other.render_pass, VK_NULL_HANDLE); + framebuffer_views = std::move(other.framebuffer_views); + aspects = std::exchange(other.aspects, {}); + formats = std::exchange(other.formats, + {VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}); + width = std::exchange(other.width, 0); + height = std::exchange(other.height, 0); + res_scale = std::exchange(other.res_scale, 1); + + return *this; + } VideoCore::PixelFormat Format(VideoCore::SurfaceType type) const noexcept { return formats[Index(type)]; @@ -323,16 +352,17 @@ public: } private: + const Instance& instance; std::array images{}; std::array image_views{}; - vk::Framebuffer framebuffer; - vk::RenderPass render_pass; + vk::Framebuffer framebuffer{}; + vk::RenderPass render_pass{}; std::vector framebuffer_views; std::array aspects{}; std::array formats{VideoCore::PixelFormat::Invalid, VideoCore::PixelFormat::Invalid}; - u32 width; - u32 height; + u32 width{}; + u32 height{}; u32 res_scale{1}; }; From 03d62efe130faca6fb69abea2b8717abe84ac982 Mon Sep 17 00:00:00 2001 From: keynote <119899374+keynote@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:34:52 +0100 Subject: [PATCH 32/94] artic_base_client: Fix high cpu usage (#1789) Fixes high CPU usage by adding a small sleep to the Client::Read and Client::Write methods --- src/network/artic_base/artic_base_client.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/network/artic_base/artic_base_client.cpp b/src/network/artic_base/artic_base_client.cpp index b38416cc4..5b3ac3396 100644 --- a/src/network/artic_base/artic_base_client.cpp +++ b/src/network/artic_base/artic_base_client.cpp @@ -606,6 +606,7 @@ bool Client::Read(SocketHolder sockFD, void* buffer, size_t size, if (GET_ERRNO == ERRNO(EWOULDBLOCK) && (timeout == std::chrono::nanoseconds(0) || std::chrono::steady_clock::now() - before < timeout)) { + std::this_thread::sleep_for(100us); continue; } read_bytes = 0; @@ -630,6 +631,7 @@ bool Client::Write(SocketHolder sockFD, const void* buffer, size_t size, if (GET_ERRNO == ERRNO(EWOULDBLOCK) && (timeout == std::chrono::nanoseconds(0) || std::chrono::steady_clock::now() - before < timeout)) { + std::this_thread::sleep_for(100us); continue; } write_bytes = 0; @@ -863,4 +865,4 @@ void Client::OnAllHandlersFinished() { pending_responses.clear(); } -} // namespace Network::ArticBase \ No newline at end of file +} // namespace Network::ArticBase From 325562093487ec1137f51a1025dc0a731366770b Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 26 Feb 2026 04:33:13 -0800 Subject: [PATCH 33/94] Implement integer scaling (#1400) --- .../features/settings/model/BooleanSetting.kt | 1 + .../settings/ui/SettingsFragmentPresenter.kt | 9 + src/android/app/src/main/jni/config.cpp | 1 + src/android/app/src/main/jni/default_ini.h | 4 + .../app/src/main/res/values/strings.xml | 2 + src/citra_qt/configuration/config.cpp | 2 + .../configuration/configure_enhancements.cpp | 6 + .../configuration/configure_enhancements.h | 3 +- .../configuration/configure_enhancements.ui | 12 +- src/common/settings.cpp | 2 + src/common/settings.h | 1 + src/core/frontend/framebuffer_layout.cpp | 197 +++++++----------- 12 files changed, 121 insertions(+), 119 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt index f06324251..f1ed493f1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt @@ -34,6 +34,7 @@ enum class BooleanSetting( LLE_APPLETS("lle_applets", Settings.SECTION_SYSTEM, false), NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, true), LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, true), + USE_INTEGER_SCALING("use_integer_scaling",Settings.SECTION_RENDERER, false), SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, false), DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, true), DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, false), diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 1326401d5..13c1d2d96 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -898,6 +898,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.RESOLUTION_FACTOR.key, IntSetting.RESOLUTION_FACTOR.defaultValue ) + ) + add( + SwitchSetting( + BooleanSetting.USE_INTEGER_SCALING, + R.string.use_integer_scaling, + R.string.use_integer_scaling_description, + BooleanSetting.USE_INTEGER_SCALING.key, + BooleanSetting.USE_INTEGER_SCALING.defaultValue + ) ) add( SwitchSetting( diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 6d310fea6..785f9c21a 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -168,6 +168,7 @@ void Config::ReadValues() { Settings::values.pp_shader_name = sdl2_config->GetString("Renderer", "pp_shader_name", default_shader); ReadSetting("Renderer", Settings::values.filter_mode); + ReadSetting("Renderer", Settings::values.use_integer_scaling); ReadSetting("Renderer", Settings::values.bg_red); ReadSetting("Renderer", Settings::values.bg_green); diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 08eaf3283..fc86079ad 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -148,6 +148,10 @@ use_disk_shader_cache = # factor for the 3DS resolution resolution_factor = +# Use Integer Scaling when the layout allows +# 0: Off (default), 1: On +use_integer_scaling = + # Turns on the frame limiter, which will limit frames output to the target game speed # 0: Off, 1: On (default) use_frame_limit = diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 18959b3a8..fa542ea74 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -250,6 +250,8 @@ Compiles shaders in the background to reduce stuttering during gameplay. When enabled expect temporary graphical glitches Linear Filtering Enables linear filtering, which causes game visuals to appear smoother. + Integer Scaling + Scales the screens with an integer multiplier of the original 3DS screen. For layouts with two different screen sizes, the largest screen is integer-scaled. Texture Filter Enhances the visuals of applications by applying a filter to textures. The supported filters are Anime4K Ultrafast, Bicubic, ScaleForce, xBRZ freescale, and MMPX. Delay Game Render Thread diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index ddb4eebd6..35abe81ef 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -700,6 +700,7 @@ void QtConfig::ReadRendererValues() { ReadGlobalSetting(Settings::values.use_vsync); ReadGlobalSetting(Settings::values.use_display_refresh_rate_detection); ReadGlobalSetting(Settings::values.resolution_factor); + ReadGlobalSetting(Settings::values.use_integer_scaling); ReadGlobalSetting(Settings::values.frame_limit); ReadGlobalSetting(Settings::values.turbo_limit); @@ -1241,6 +1242,7 @@ void QtConfig::SaveRendererValues() { WriteGlobalSetting(Settings::values.use_vsync); WriteGlobalSetting(Settings::values.use_display_refresh_rate_detection); WriteGlobalSetting(Settings::values.resolution_factor); + WriteGlobalSetting(Settings::values.use_integer_scaling); WriteGlobalSetting(Settings::values.frame_limit); WriteGlobalSetting(Settings::values.turbo_limit); diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index aba7a7ac8..62afcbb77 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -63,6 +63,7 @@ void ConfigureEnhancements::SetConfiguration() { static_cast(Settings::values.mono_render_option.GetValue())); updateShaders(Settings::values.render_3d.GetValue()); ui->toggle_linear_filter->setChecked(Settings::values.filter_mode.GetValue()); + ui->use_integer_scaling->setChecked(Settings::values.use_integer_scaling.GetValue()); ui->toggle_dump_textures->setChecked(Settings::values.dump_textures.GetValue()); ui->toggle_custom_textures->setChecked(Settings::values.custom_textures.GetValue()); ui->toggle_preload_textures->setChecked(Settings::values.preload_textures.GetValue()); @@ -127,6 +128,8 @@ void ConfigureEnhancements::ApplyConfiguration() { ConfigurationShared::ApplyPerGameSetting(&Settings::values.filter_mode, ui->toggle_linear_filter, linear_filter); + ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_integer_scaling, + ui->use_integer_scaling, use_integer_scaling); ConfigurationShared::ApplyPerGameSetting(&Settings::values.texture_filter, ui->texture_filter_combobox); ConfigurationShared::ApplyPerGameSetting(&Settings::values.dump_textures, @@ -148,6 +151,7 @@ void ConfigureEnhancements::SetupPerGameUI() { ui->widget_resolution->setEnabled(Settings::values.resolution_factor.UsingGlobal()); ui->widget_texture_filter->setEnabled(Settings::values.texture_filter.UsingGlobal()); ui->toggle_linear_filter->setEnabled(Settings::values.filter_mode.UsingGlobal()); + ui->use_integer_scaling->setEnabled(Settings::values.use_integer_scaling.UsingGlobal()); ui->toggle_dump_textures->setEnabled(Settings::values.dump_textures.UsingGlobal()); ui->toggle_custom_textures->setEnabled(Settings::values.custom_textures.UsingGlobal()); ui->toggle_preload_textures->setEnabled(Settings::values.preload_textures.UsingGlobal()); @@ -166,6 +170,8 @@ void ConfigureEnhancements::SetupPerGameUI() { ConfigurationShared::SetColoredTristate(ui->toggle_linear_filter, Settings::values.filter_mode, linear_filter); + ConfigurationShared::SetColoredTristate( + ui->use_integer_scaling, Settings::values.use_integer_scaling, use_integer_scaling); ConfigurationShared::SetColoredTristate(ui->toggle_dump_textures, Settings::values.dump_textures, dump_textures); ConfigurationShared::SetColoredTristate(ui->toggle_custom_textures, diff --git a/src/citra_qt/configuration/configure_enhancements.h b/src/citra_qt/configuration/configure_enhancements.h index c9f3449b1..70d6a6673 100644 --- a/src/citra_qt/configuration/configure_enhancements.h +++ b/src/citra_qt/configuration/configure_enhancements.h @@ -1,4 +1,4 @@ -// Copyright 2019 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -39,6 +39,7 @@ private: std::unique_ptr ui; ConfigurationShared::CheckState linear_filter; + ConfigurationShared::CheckState use_integer_scaling; ConfigurationShared::CheckState dump_textures; ConfigurationShared::CheckState custom_textures; ConfigurationShared::CheckState preload_textures; diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index ebf8520c4..dc3c3deaf 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -6,7 +6,7 @@ 0 0 - 440 + 639 950 @@ -110,6 +110,16 @@ + + + + Use Integer Scaling + + + <html><head/><body><p>Use Integer Scaling</p><p>Enforces that the larger screen in all layouts is an integer scale of the 240px height of the original 3DS screen.</p></body></html> + + + diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 564090d0b..d0a08b587 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -97,6 +97,7 @@ void LogSettings() { log_setting("Renderer_ShadersAccurateMul", values.shaders_accurate_mul.GetValue()); log_setting("Renderer_UseShaderJit", values.use_shader_jit.GetValue()); log_setting("Renderer_UseResolutionFactor", values.resolution_factor.GetValue()); + log_setting("Renderer_UseIntegerScaling", values.use_integer_scaling.GetValue()); log_setting("Renderer_FrameLimit", values.frame_limit.GetValue()); log_setting("Renderer_VSyncNew", values.use_vsync.GetValue()); log_setting("Renderer_PostProcessingShader", values.pp_shader_name.GetValue()); @@ -206,6 +207,7 @@ void RestoreGlobalState(bool is_powered_on) { values.shaders_accurate_mul.SetGlobal(true); values.use_vsync.SetGlobal(true); values.resolution_factor.SetGlobal(true); + values.use_integer_scaling.SetGlobal(true); values.frame_limit.SetGlobal(true); values.texture_filter.SetGlobal(true); values.texture_sampling.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 709ba0cf8..178db0019 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -524,6 +524,7 @@ struct Values { true, "use_display_refresh_rate_detection"}; Setting use_shader_jit{true, "use_shader_jit"}; SwitchableSetting resolution_factor{1, 0, 10, "resolution_factor"}; + SwitchableSetting use_integer_scaling{false, "use_integer_scaling"}; SwitchableSetting frame_limit{100, 0, 1000, "frame_limit"}; SwitchableSetting turbo_limit{200, 0, 1000, "turbo_limit"}; SwitchableSetting texture_filter{TextureFilter::NoFilter, "texture_filter"}; diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index 392db8867..918c1454f 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -25,13 +25,33 @@ u32 FramebufferLayout::GetScalingRatio() const { } // Finds the largest size subrectangle contained in window area that is confined to the aspect ratio +// aligned to the upper-left corner of the bounding rectangle template static Common::Rectangle MaxRectangle(Common::Rectangle window_area, float window_aspect_ratio) { float scale = std::min(static_cast(window_area.GetWidth()), window_area.GetHeight() / window_aspect_ratio); - return Common::Rectangle{0, 0, static_cast(std::round(scale)), - static_cast(std::round(scale * window_aspect_ratio))}; + return Common::Rectangle{ + window_area.left, window_area.top, window_area.left + static_cast(std::round(scale)), + window_area.top + static_cast(std::round(scale * window_aspect_ratio))}; +} + +// overload of the above that takes an inner rectangle instead of an aspect ratio, and can be +// limited to integer scaling if desired +template +static Common::Rectangle MaxRectangle(Common::Rectangle bounding_window, + Common::Rectangle inner_window, + bool use_integer = false) { + float scale = + std::min(static_cast(bounding_window.GetWidth()) / inner_window.GetWidth(), + static_cast(bounding_window.GetHeight()) / inner_window.GetHeight()); + if (use_integer && scale >= 1.0) { + scale = std::floor(scale); + } + return Common::Rectangle(bounding_window.left, bounding_window.top, + bounding_window.left + static_cast(inner_window.GetWidth() * scale), + bounding_window.top + + static_cast(inner_window.GetHeight() * scale)); } FramebufferLayout DefaultFrameLayout(u32 width, u32 height, bool swapped, bool upright) { @@ -74,41 +94,31 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up FramebufferLayout res{width, height, !swapped, swapped, {}, {}, !upright}; Common::Rectangle screen_window_area{0, 0, width, height}; - Common::Rectangle top_screen; - Common::Rectangle bot_screen; + Common::Rectangle top_screen{0, 0, Core::kScreenTopWidth, Core::kScreenTopHeight}; + Common::Rectangle bot_screen{0, 0, Core::kScreenBottomWidth, Core::kScreenBottomHeight}; - // TODO: This is kind of gross, make it platform agnostic. -OS -#ifdef ANDROID - const float window_aspect_ratio = static_cast(height) / width; + const float window_aspect_ratio = static_cast(height) / static_cast(width); const auto aspect_ratio_setting = Settings::values.aspect_ratio.GetValue(); - float emulation_aspect_ratio = (swapped) ? BOT_SCREEN_ASPECT_RATIO : TOP_SCREEN_ASPECT_RATIO; switch (aspect_ratio_setting) { case Settings::AspectRatio::Default: + // this is the only one where we allow integer scaling to apply + // also the only option on desktop + top_screen = MaxRectangle(screen_window_area, top_screen, + Settings::values.use_integer_scaling.GetValue()); + bot_screen = MaxRectangle(screen_window_area, bot_screen, + Settings::values.use_integer_scaling.GetValue()); break; case Settings::AspectRatio::Stretch: - emulation_aspect_ratio = window_aspect_ratio; + top_screen = MaxRectangle(screen_window_area, window_aspect_ratio); + bot_screen = MaxRectangle(screen_window_area, window_aspect_ratio); break; default: - emulation_aspect_ratio = res.GetAspectRatioValue(aspect_ratio_setting); + float emulation_aspect_ratio = FramebufferLayout::GetAspectRatioValue(aspect_ratio_setting); + top_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); + bot_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); } - top_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); - bot_screen = MaxRectangle(screen_window_area, emulation_aspect_ratio); - - if (window_aspect_ratio < emulation_aspect_ratio) { - top_screen = - top_screen.TranslateX((screen_window_area.GetWidth() - top_screen.GetWidth()) / 2); - bot_screen = - bot_screen.TranslateX((screen_window_area.GetWidth() - bot_screen.GetWidth()) / 2); - } else { - top_screen = top_screen.TranslateY((height - top_screen.GetHeight()) / 2); - bot_screen = bot_screen.TranslateY((height - bot_screen.GetHeight()) / 2); - } -#else - top_screen = MaxRectangle(screen_window_area, TOP_SCREEN_ASPECT_RATIO); - bot_screen = MaxRectangle(screen_window_area, BOT_SCREEN_ASPECT_RATIO); - const bool stretched = (Settings::values.screen_top_stretch.GetValue() && !swapped) || (Settings::values.screen_bottom_stretch.GetValue() && swapped); if (stretched) { @@ -126,7 +136,6 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up bot_screen = bot_screen.TranslateX((width - bot_screen.GetWidth()) / 2) .TranslateY((height - bot_screen.GetHeight()) / 2); } -#endif res.top_screen = top_screen; res.bottom_screen = bot_screen; @@ -150,19 +159,17 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr FramebufferLayout res{width, height, true, true, {}, {}, !upright}; // Split the window into two parts. Give proportional width to the smaller screen // To do that, find the total emulation box and maximize that based on window size - u32 gap = (u32)(Settings::values.screen_gap.GetValue() * scale_factor); + u32 gap = (u32)(Settings::values.screen_gap.GetValue()); - float large_height = - swapped ? Core::kScreenBottomHeight * scale_factor : Core::kScreenTopHeight * scale_factor; - float small_height = - static_cast(swapped ? Core::kScreenTopHeight : Core::kScreenBottomHeight); - float large_width = - swapped ? Core::kScreenBottomWidth * scale_factor : Core::kScreenTopWidth * scale_factor; - float small_width = - static_cast(swapped ? Core::kScreenTopWidth : Core::kScreenBottomWidth); + u32 large_height = swapped ? Core::kScreenBottomHeight : Core::kScreenTopHeight; + u32 small_height = static_cast(swapped ? Core::kScreenTopHeight / scale_factor + : Core::kScreenBottomHeight / scale_factor); + u32 large_width = swapped ? Core::kScreenBottomWidth : Core::kScreenTopWidth; + u32 small_width = static_cast(swapped ? Core::kScreenTopWidth / scale_factor + : Core::kScreenBottomWidth / scale_factor); - float emulation_width; - float emulation_height; + u32 emulation_width; + u32 emulation_height; if (vertical) { // width is just the larger size at this point emulation_width = std::max(large_width, small_width); @@ -172,12 +179,13 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr emulation_height = std::max(large_height, small_height); } - const float window_aspect_ratio = static_cast(height) / static_cast(width); - const float emulation_aspect_ratio = emulation_height / emulation_width; - Common::Rectangle screen_window_area{0, 0, width, height}; - Common::Rectangle total_rect = MaxRectangle(screen_window_area, emulation_aspect_ratio); - // TODO: Wtf does this `scale_amount` value represent? -OS + Common::Rectangle total_rect{0, 0, emulation_width, emulation_height}; + total_rect = MaxRectangle(screen_window_area, total_rect, + Settings::values.use_integer_scaling.GetValue()); + total_rect = total_rect.TranslateX((width - total_rect.GetWidth()) / 2) + .TranslateY((height - total_rect.GetHeight()) / 2); + const float scale_amount = static_cast(total_rect.GetHeight()) / emulation_height; gap = static_cast(static_cast(gap) * scale_amount); @@ -190,61 +198,50 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr static_cast(small_width * scale_amount + total_rect.left), static_cast(small_height * scale_amount + total_rect.top)}; - if (window_aspect_ratio < emulation_aspect_ratio) { - // shift the large screen so it is at the left position of the bounding rectangle - large_screen = large_screen.TranslateX((width - total_rect.GetWidth()) / 2); - } else { - // shift the large screen so it is at the top position of the bounding rectangle - large_screen = large_screen.TranslateY((height - total_rect.GetHeight()) / 2); - } - switch (small_screen_position) { case Settings::SmallScreenPosition::TopRight: // Shift the small screen to the top right corner - small_screen = small_screen.TranslateX(large_screen.right + gap); - small_screen = small_screen.TranslateY(large_screen.top); + small_screen = small_screen.TranslateX(large_screen.GetWidth() + gap); + small_screen = small_screen.TranslateY(large_screen.top - small_screen.top); break; case Settings::SmallScreenPosition::MiddleRight: // Shift the small screen to the center right - small_screen = small_screen.TranslateX(large_screen.right + gap); - small_screen = small_screen.TranslateY( - ((large_screen.GetHeight() - small_screen.GetHeight()) / 2) + large_screen.top); + small_screen = small_screen.TranslateX(large_screen.GetWidth() + gap); + small_screen = + small_screen.TranslateY(((large_screen.GetHeight() - small_screen.GetHeight()) / 2) + + large_screen.top - small_screen.top); break; case Settings::SmallScreenPosition::BottomRight: // Shift the small screen to the bottom right corner - small_screen = small_screen.TranslateX(large_screen.right + gap); - small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.GetHeight()); + small_screen = small_screen.TranslateX(large_screen.GetWidth() + gap); + small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.bottom); break; case Settings::SmallScreenPosition::TopLeft: - // shift the small screen to the upper left then shift the large screen to its right - small_screen = small_screen.TranslateX(large_screen.left); + // shift the large screen to the upper right of the small screen large_screen = large_screen.TranslateX(small_screen.GetWidth() + gap); - small_screen = small_screen.TranslateY(large_screen.top); break; case Settings::SmallScreenPosition::MiddleLeft: // shift the small screen to the middle left and shift the large screen to its right - small_screen = small_screen.TranslateX(large_screen.left); large_screen = large_screen.TranslateX(small_screen.GetWidth() + gap); - small_screen = small_screen.TranslateY( - ((large_screen.GetHeight() - small_screen.GetHeight()) / 2) + large_screen.top); + small_screen = + small_screen.TranslateY(((large_screen.GetHeight() - small_screen.GetHeight()) / 2)); break; case Settings::SmallScreenPosition::BottomLeft: // shift the small screen to the bottom left and shift the large screen to its right - small_screen = small_screen.TranslateX(large_screen.left); large_screen = large_screen.TranslateX(small_screen.GetWidth() + gap); - small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.GetHeight()); + small_screen = small_screen.TranslateY(large_screen.bottom - small_screen.bottom); break; case Settings::SmallScreenPosition::AboveLarge: - // shift the large screen down and the bottom screen above it - small_screen = small_screen.TranslateY(large_screen.top); + // shift the large screen down large_screen = large_screen.TranslateY(small_screen.GetHeight() + gap); // If the "large screen" is actually smaller, center it if (large_screen.GetWidth() < total_rect.GetWidth()) { large_screen = large_screen.TranslateX((total_rect.GetWidth() - large_screen.GetWidth()) / 2); } - small_screen = small_screen.TranslateX(large_screen.left + large_screen.GetWidth() / 2 - - small_screen.GetWidth() / 2); + small_screen = + small_screen.TranslateX((large_screen.left - total_rect.left) + + large_screen.GetWidth() / 2 - small_screen.GetWidth() / 2); break; case Settings::SmallScreenPosition::BelowLarge: // shift the bottom_screen down and then over to the center @@ -253,9 +250,10 @@ FramebufferLayout LargeFrameLayout(u32 width, u32 height, bool swapped, bool upr large_screen = large_screen.TranslateX((total_rect.GetWidth() - large_screen.GetWidth()) / 2); } - small_screen = small_screen.TranslateY(large_screen.bottom + gap); - small_screen = small_screen.TranslateX(large_screen.left + large_screen.GetWidth() / 2 - - small_screen.GetWidth() / 2); + small_screen = small_screen.TranslateY(large_screen.GetHeight() + gap); + small_screen = + small_screen.TranslateX((large_screen.left - total_rect.left) + + large_screen.GetWidth() / 2 - small_screen.GetWidth() / 2); break; default: UNREACHABLE(); @@ -276,54 +274,19 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u if (upright) { std::swap(width, height); } - FramebufferLayout res{width, height, true, true, {}, {}, !upright, false, true, {}}; // Split the window into two parts. Give 2.25x width to the main screen, // and make a bar on the right side with 1x width top screen and 1.25x width bottom screen // To do that, find the total emulation box and maximize that based on window size - const float window_aspect_ratio = static_cast(height) / static_cast(width); - const float scale_factor = 2.25f; - - float main_screen_aspect_ratio = TOP_SCREEN_ASPECT_RATIO; - float hybrid_area_aspect_ratio = 27.f / 65; - float top_screen_aspect_ratio = TOP_SCREEN_ASPECT_RATIO; - float bot_screen_aspect_ratio = BOT_SCREEN_ASPECT_RATIO; - - if (swapped) { - main_screen_aspect_ratio = BOT_SCREEN_ASPECT_RATIO; - hybrid_area_aspect_ratio = - Core::kScreenBottomHeight * scale_factor / - (Core::kScreenBottomWidth * scale_factor + Core::kScreenTopWidth); - } - - Common::Rectangle screen_window_area{0, 0, width, height}; - Common::Rectangle total_rect = MaxRectangle(screen_window_area, hybrid_area_aspect_ratio); - Common::Rectangle large_main_screen = MaxRectangle(total_rect, main_screen_aspect_ratio); - Common::Rectangle side_rect = total_rect.Scale(1.f / scale_factor); - Common::Rectangle small_top_screen = MaxRectangle(side_rect, top_screen_aspect_ratio); - Common::Rectangle small_bottom_screen = MaxRectangle(side_rect, bot_screen_aspect_ratio); - - if (window_aspect_ratio < hybrid_area_aspect_ratio) { - large_main_screen = large_main_screen.TranslateX((width - total_rect.GetWidth()) / 2); - } else { - large_main_screen = large_main_screen.TranslateY((height - total_rect.GetHeight()) / 2); - } - - // Scale the bottom screen so it's width is the same as top screen - small_bottom_screen = small_bottom_screen.Scale(1.25f); - - // Shift the small bottom screen to the bottom right corner - small_bottom_screen = small_bottom_screen.TranslateX(large_main_screen.right) - .TranslateY(large_main_screen.GetHeight() + large_main_screen.top - - small_bottom_screen.GetHeight()); - - // Shift small top screen to upper right corner - small_top_screen = - small_top_screen.TranslateX(large_main_screen.right).TranslateY(large_main_screen.top); - - res.top_screen = small_top_screen; - res.additional_screen = swapped ? small_bottom_screen : large_main_screen; - res.bottom_screen = swapped ? large_main_screen : small_bottom_screen; + const float scale_factor = swapped ? 2.25 : 1.8; + const Settings::SmallScreenPosition pos = swapped ? Settings::SmallScreenPosition::TopRight + : Settings::SmallScreenPosition::BottomRight; + FramebufferLayout res = LargeFrameLayout(width, height, swapped, upright, scale_factor, pos); + const Common::Rectangle main = swapped ? res.bottom_screen : res.top_screen; + const Common::Rectangle small = swapped ? res.top_screen : res.bottom_screen; + res.additional_screen = Common::Rectangle{small.left, swapped ? small.bottom : main.top, + small.right, swapped ? main.bottom : small.top}; + res.additional_screen_enabled = true; if (upright) { return reverseLayout(res); } else { From 5ac0ef8fdee0fd6832787f22f473014c227d57ad Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 26 Feb 2026 05:27:18 -0800 Subject: [PATCH 34/94] hide portrait layout menu on landscape and vice versa (#1473) --- .../org/citra/citra_emu/fragments/EmulationFragment.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index 30b22cc64..fba1a167d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -11,6 +11,7 @@ import android.content.DialogInterface import android.content.Intent import android.content.IntentFilter import android.content.SharedPreferences +import android.content.res.Configuration import android.net.Uri import android.os.BatteryManager import android.os.Build @@ -175,6 +176,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram savedInstanceState: Bundle? ): View { _binding = FragmentEmulationBinding.inflate(inflater) + binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible = + CitraApplication.appContext.resources.configuration.orientation != + Configuration.ORIENTATION_PORTRAIT + binding.inGameMenu.menu.findItem(R.id.menu_portrait_screen_layout).isVisible = + CitraApplication.appContext.resources.configuration.orientation == + Configuration.ORIENTATION_PORTRAIT return binding.root } From 6b2ac400ebd66eb41df9ed719d662ebe7567de09 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Thu, 26 Feb 2026 09:40:42 -0800 Subject: [PATCH 35/94] Android: Hotkey Enable Button (#1464) --- .../citra_emu/activities/EmulationActivity.kt | 26 ++- .../citra_emu/features/hotkeys/Hotkey.kt | 3 +- .../features/hotkeys/HotkeyUtility.kt | 150 ++++++++++++++---- .../features/settings/model/Settings.kt | 3 + .../model/view/InputBindingSetting.kt | 55 +++++-- .../settings/ui/SettingsFragmentPresenter.kt | 2 +- .../app/src/main/res/values/strings.xml | 2 + 7 files changed, 177 insertions(+), 64 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt index ca92b308d..1e372720e 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.kt @@ -267,36 +267,28 @@ class EmulationActivity : AppCompatActivity() { return super.dispatchKeyEvent(event) } - val button = - preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode) - val action: Int = when (event.action) { + when (event.action) { KeyEvent.ACTION_DOWN -> { - hotkeyUtility.handleHotkey(button) - // On some devices, the back gesture / button press is not intercepted by androidx // and fails to open the emulation menu. So we're stuck running deprecated code to // cover for either a fault on androidx's side or in OEM skins (MIUI at least) + if (event.keyCode == KeyEvent.KEYCODE_BACK) { // If the hotkey is pressed, we don't want to open the drawer - if (!hotkeyUtility.HotkeyIsPressed) { + if (!hotkeyUtility.hotkeyIsPressed) { onBackPressed() + return true } } - - // Normal key events. - NativeLibrary.ButtonState.PRESSED + return hotkeyUtility.handleKeyPress(event) } - KeyEvent.ACTION_UP -> { - hotkeyUtility.HotkeyIsPressed = false - NativeLibrary.ButtonState.RELEASED + return hotkeyUtility.handleKeyRelease(event) + } + else -> { + return false; } - else -> return false } - val input = event.device - ?: // Controller was disconnected - return false - return NativeLibrary.onGamePadEvent(input.descriptor, button, action) } private fun onAmiiboSelected(selectedFile: String) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt index 4b22164bb..e2319a7e4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/Hotkey.kt @@ -11,5 +11,6 @@ enum class Hotkey(val button: Int) { PAUSE_OR_RESUME(10004), QUICKSAVE(10005), QUICKLOAD(10006), - TURBO_LIMIT(10007); + TURBO_LIMIT(10007), + ENABLE(10008); } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt index 0a4a1ffa3..d01d5f769 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/hotkeys/HotkeyUtility.kt @@ -5,50 +5,140 @@ package org.citra.citra_emu.features.hotkeys import android.content.Context +import android.view.KeyEvent import android.widget.Toast +import androidx.preference.PreferenceManager +import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.TurboHelper import org.citra.citra_emu.display.ScreenAdjustmentUtil +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.Settings class HotkeyUtility( private val screenAdjustmentUtil: ScreenAdjustmentUtil, - private val context: Context) { + private val context: Context +) { private val hotkeyButtons = Hotkey.entries.map { it.button } - var HotkeyIsPressed = false + private var hotkeyIsEnabled = false + var hotkeyIsPressed = false + private val currentlyPressedButtons = mutableSetOf() + + fun handleKeyPress(keyEvent: KeyEvent): Boolean { + var handled = false + val buttonSet = InputBindingSetting.getButtonSet(keyEvent) + val enableButton = + PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + .getString(Settings.HOTKEY_ENABLE, "") + val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) + val thisKeyIsHotkey = + !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } + hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton + + // Now process all internal buttons associated with this keypress + for (button in buttonSet) { + currentlyPressedButtons.add(button) + //option 1 - this is the enable command, which was already handled + if (button == Hotkey.ENABLE.button) { + handled = true + } + // option 2 - this is a different hotkey command + else if (hotkeyButtons.contains(button)) { + if (hotkeyIsEnabled) { + handled = handleHotkey(button) || handled + } + } + // option 3 - this is a normal key + else { + // if this key press is ALSO associated with a hotkey that will process, skip + // the normal key event. + if (!thisKeyIsHotkey || !hotkeyIsEnabled) { + handled = NativeLibrary.onGamePadEvent( + keyEvent.device.descriptor, + button, + NativeLibrary.ButtonState.PRESSED + ) || handled + } + } + } + return handled + } + + fun handleKeyRelease(keyEvent: KeyEvent): Boolean { + var handled = false + val buttonSet = InputBindingSetting.getButtonSet(keyEvent) + val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button) + val thisKeyIsHotkey = + !thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) } + if (thisKeyIsEnableButton) { + handled = true; hotkeyIsEnabled = false + } + + for (button in buttonSet) { + // this is a hotkey button + if (hotkeyButtons.contains(button)) { + currentlyPressedButtons.remove(button) + if (!currentlyPressedButtons.any { hotkeyButtons.contains(it) }) { + // all hotkeys are no longer pressed + hotkeyIsPressed = false + } + } else { + // if this key ALSO sends a hotkey command that we already/will handle, + // or if we did not register the press of this button, e.g. if this key + // was also a hotkey pressed after enable, but released after enable button release, then + // skip the normal key event + if ((!thisKeyIsHotkey || !hotkeyIsEnabled) && currentlyPressedButtons.contains( + button + ) + ) { + handled = NativeLibrary.onGamePadEvent( + keyEvent.device.descriptor, + button, + NativeLibrary.ButtonState.RELEASED + ) || handled + currentlyPressedButtons.remove(button) + } + } + } + return handled + } fun handleHotkey(bindedButton: Int): Boolean { - if(hotkeyButtons.contains(bindedButton)) { - when (bindedButton) { - Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen() - Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() - Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() - Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() - Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true) - Hotkey.QUICKSAVE.button -> { - NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT) - Toast.makeText(context, - context.getString(R.string.saving), - Toast.LENGTH_SHORT).show() - } - Hotkey.QUICKLOAD.button -> { - val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT) - val stringRes = if(wasLoaded) { - R.string.loading - } else { - R.string.quickload_not_found - } - Toast.makeText(context, - context.getString(stringRes), - Toast.LENGTH_SHORT).show() - } - else -> {} + when (bindedButton) { + Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen() + Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts() + Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame() + Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume() + Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true) + Hotkey.QUICKSAVE.button -> { + NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT) + Toast.makeText( + context, + context.getString(R.string.saving), + Toast.LENGTH_SHORT + ).show() } - HotkeyIsPressed = true - return true + + Hotkey.QUICKLOAD.button -> { + val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT) + val stringRes = if (wasLoaded) { + R.string.loading + } else { + R.string.quickload_not_found + } + Toast.makeText( + context, + context.getString(stringRes), + Toast.LENGTH_SHORT + ).show() + } + + else -> {} } - return false + hotkeyIsPressed = true + return true } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt index 02d10cfe9..96100349a 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt @@ -135,6 +135,7 @@ class Settings { const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal" const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical" const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal" + const val HOTKEY_ENABLE = "hotkey_enable" const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap" const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout" const val HOTKEY_CLOSE_GAME = "hotkey_close_game" @@ -202,6 +203,7 @@ class Settings { R.string.button_zr ) val hotKeys = listOf( + HOTKEY_ENABLE, HOTKEY_SCREEN_SWAP, HOTKEY_CYCLE_LAYOUT, HOTKEY_CLOSE_GAME, @@ -211,6 +213,7 @@ class Settings { HOTKEY_TURBO_LIMIT ) val hotkeyTitles = listOf( + R.string.controller_hotkey_enable_button, R.string.emulation_swap_screens, R.string.emulation_cycle_landscape_layouts, R.string.emulation_close_game, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index 482bc0b08..d78f5c3a3 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -128,6 +128,7 @@ class InputBindingSetting( Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT + Settings.HOTKEY_ENABLE -> Hotkey.ENABLE.button Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button @@ -162,36 +163,40 @@ class InputBindingSetting( fun removeOldMapping() { // Try remove all possible keys we wrote for this setting val oldKey = preferences.getString(reverseKey, "") - (setting as AbstractStringSetting).string = "" if (oldKey != "") { + (setting as AbstractStringSetting).string = "" preferences.edit() .remove(abstractSetting.key) // Used for ui text - .remove(oldKey) // Used for button mapping .remove(oldKey + "_GuestOrientation") // Used for axis orientation .remove(oldKey + "_GuestButton") // Used for axis button .remove(oldKey + "_Inverted") // used for axis inversion - .apply() + .remove(reverseKey) + val buttonCodes = try { + preferences.getStringSet(oldKey, mutableSetOf())!!.toMutableSet() + } catch (e: ClassCastException) { + // if this is an int pref, either old button or an axis, so just remove it + preferences.edit().remove(oldKey).apply() + return; + } + buttonCodes.remove(buttonCode.toString()); + preferences.edit().putStringSet(oldKey,buttonCodes).apply() } } /** * Helper function to write a gamepad button mapping for the setting. */ - private fun writeButtonMapping(key: String) { + private fun writeButtonMapping(keyEvent: KeyEvent) { val editor = preferences.edit() - - // Remove mapping for another setting using this input - val oldButtonCode = preferences.getInt(key, -1) - if (oldButtonCode != -1) { - val oldKey = getButtonKey(oldButtonCode) - editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten - } - + val key = getInputButtonKey(keyEvent) + // Pull in all codes associated with this key + // Migrate from the old int preference if need be + val buttonCodes = InputBindingSetting.getButtonSet(keyEvent) + buttonCodes.add(buttonCode) // Cleanup old mapping for this setting removeOldMapping() - // Write new mapping - editor.putInt(key, buttonCode) + editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()}) // Write next reverse mapping for future cleanup editor.putString(reverseKey, key) @@ -229,7 +234,7 @@ class InputBindingSetting( } val code = translateEventToKeyId(keyEvent) - writeButtonMapping(getInputButtonKey(code)) + writeButtonMapping(keyEvent) val uiString = "${keyEvent.device.name}: Button $code" value = uiString } @@ -289,6 +294,26 @@ class InputBindingSetting( NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT else -> "" } + /** + * Get the mutable set of int button values this key should map to given an event + */ + fun getButtonSet(keyCode: KeyEvent):MutableSet { + val key = getInputButtonKey(keyCode) + val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + var buttonCodes = try { + preferences.getStringSet(key, mutableSetOf()) + } catch (e: ClassCastException) { + val prefInt = preferences.getInt(key, -1); + val migratedSet = if (prefInt != -1) { + mutableSetOf(prefInt.toString()) + } else { + mutableSetOf() + } + migratedSet + } + if (buttonCodes == null) buttonCodes = mutableSetOf() + return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() + } /** * Helper function to get the settings key for an gamepad button. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 13c1d2d96..9a40ba90b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -811,7 +811,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) add(InputBindingSetting(button, Settings.triggerTitles[i])) } - add(HeaderSetting(R.string.controller_hotkeys)) + add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description)) Settings.hotKeys.forEachIndexed { i: Int, key: String -> val button = getInputObject(key) add(InputBindingSetting(button, Settings.hotkeyTitles[i])) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index fa542ea74..de2a78ee9 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -122,6 +122,8 @@ Circle Pad C-Stick Hotkeys + If the "Hotkey Enable" key is mapped, that key must be pressed in addition to the mapped hotkey + Hotkey Enable Triggers Trigger D-Pad From b477ba09c1aa72c5e9004307d7c47da72cb144b8 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Fri, 27 Feb 2026 04:21:53 -0800 Subject: [PATCH 36/94] Ability to select which layouts to cycle with the cycle layout hotkey (#1430) --- .../citra_emu/display/ScreenAdjustmentUtil.kt | 11 +- .../settings/model/AbstractListSetting.kt | 9 + .../features/settings/model/IntListSetting.kt | 52 +++++ .../settings/model/view/MultiChoiceSetting.kt | 46 ++++ .../settings/model/view/SettingsItem.kt | 1 + .../features/settings/ui/SettingsAdapter.kt | 94 +++++++- .../settings/ui/SettingsFragmentPresenter.kt | 14 ++ .../ui/viewholder/MultiChoiceViewHolder.kt | 80 +++++++ .../features/settings/utils/SettingsFile.kt | 8 +- .../app/src/main/res/values/strings.xml | 2 + src/citra_qt/CMakeLists.txt | 3 + src/citra_qt/citra_qt.cpp | 35 ++- src/citra_qt/configuration/config.cpp | 126 +++++----- src/citra_qt/configuration/config.h | 7 + .../configuration/configure_layout.cpp | 8 + .../configuration/configure_layout.ui | 88 ++++--- .../configuration/configure_layout_cycle.cpp | 92 ++++++++ .../configuration/configure_layout_cycle.h | 32 +++ .../configuration/configure_layout_cycle.ui | 216 ++++++++++++++++++ src/common/settings.cpp | 2 + src/common/settings.h | 8 + 21 files changed, 815 insertions(+), 119 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt create mode 100644 src/citra_qt/configuration/configure_layout_cycle.cpp create mode 100644 src/citra_qt/configuration/configure_layout_cycle.h create mode 100644 src/citra_qt/configuration/configure_layout_cycle.ui diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt index 105f49ab8..e63960fa8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenAdjustmentUtil.kt @@ -12,6 +12,7 @@ import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.utils.EmulationMenuSettings @@ -31,8 +32,16 @@ class ScreenAdjustmentUtil( BooleanSetting.SWAP_SCREEN.boolean = isEnabled settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG) } + fun cycleLayouts() { - val landscapeValues = context.resources.getIntArray(R.array.landscapeValues) + + val landscapeLayoutsToCycle = IntListSetting.LAYOUTS_TO_CYCLE.list; + val landscapeValues = + if (landscapeLayoutsToCycle.isNotEmpty()) + landscapeLayoutsToCycle.toIntArray() + else context.resources.getIntArray( + R.array.landscapeValues + ) val portraitValues = context.resources.getIntArray(R.array.portraitValues) if (NativeLibrary.isPortraitMode) { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt new file mode 100644 index 000000000..d89db48af --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractListSetting.kt @@ -0,0 +1,9 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +interface AbstractListSetting : AbstractSetting { + var list: List +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt new file mode 100644 index 000000000..0de51acce --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntListSetting.kt @@ -0,0 +1,52 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model + +enum class IntListSetting( + override val key: String, + override val section: String, + override val defaultValue: List, + val canBeEmpty: Boolean = true +) : AbstractListSetting { + + LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false); + + private var backingList: List = defaultValue + private var lastValidList : List = defaultValue + + override var list: List + get() = backingList + set(value) { + if (!canBeEmpty && value.isEmpty()) { + backingList = lastValidList + } else { + backingList = value + lastValidList = value + } + } + + override val valueAsString: String + get() = list.joinToString() + + + override val isRuntimeEditable: Boolean + get() { + for (setting in NOT_RUNTIME_EDITABLE) { + if (setting == this) { + return false + } + } + return true + } + + companion object { + private val NOT_RUNTIME_EDITABLE: List = emptyList() + + fun from(key: String): IntListSetting? = + values().firstOrNull { it.key == key } + + fun clear() = values().forEach { it.list = it.defaultValue } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt new file mode 100644 index 000000000..d097696e0 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/MultiChoiceSetting.kt @@ -0,0 +1,46 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.model.view +import org.citra.citra_emu.features.settings.model.AbstractSetting +import org.citra.citra_emu.features.settings.model.IntListSetting +class MultiChoiceSetting( + setting: AbstractSetting?, + titleId: Int, + descriptionId: Int, + val choicesId: Int, + val valuesId: Int, + val key: String? = null, + val defaultValue: List? = null, + override var isEnabled: Boolean = true +) : SettingsItem(setting, titleId, descriptionId) { + override val type = TYPE_MULTI_CHOICE + + val selectedValues: List + get() { + if (setting == null) { + return defaultValue!! + } + try { + val setting = setting as IntListSetting + return setting.list + }catch (_: ClassCastException) { + } + return defaultValue!! + } + + /** + * Write a value to the backing list. If that int was previously null, + * initializes a new one and returns it, so it can be added to the Hashmap. + * + * @param selection New value of the int. + * @return the existing setting with the new value applied. + */ + fun setSelectedValue(selection: List): IntListSetting { + val intSetting = setting as IntListSetting + intSetting.list = selection + return intSetting + } + +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index c3f11def5..68aa2226c 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -47,5 +47,6 @@ abstract class SettingsItem( const val TYPE_INPUT_BINDING = 8 const val TYPE_STRING_INPUT = 9 const val TYPE_FLOAT_INPUT = 10 + const val TYPE_MULTI_CHOICE = 11 } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 4bd5d3b5f..054ff8d63 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -41,12 +41,14 @@ import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.AbstractShortSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.model.view.SliderSetting import org.citra.citra_emu.features.settings.model.view.StringInputSetting import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting @@ -55,6 +57,7 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder +import org.citra.citra_emu.features.settings.ui.viewholder.MultiChoiceViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder @@ -72,7 +75,8 @@ import kotlin.math.roundToInt class SettingsAdapter( private val fragmentView: SettingsFragmentView, public val context: Context -) : RecyclerView.Adapter(), DialogInterface.OnClickListener { +) : RecyclerView.Adapter(), DialogInterface.OnClickListener, + DialogInterface.OnMultiChoiceClickListener { private var settings: ArrayList? = null private var clickedItem: SettingsItem? = null private var clickedPosition: Int @@ -104,6 +108,10 @@ class SettingsAdapter( SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) } + SettingsItem.TYPE_MULTI_CHOICE -> { + MultiChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this) + } + SettingsItem.TYPE_SLIDER -> { SliderViewHolder(ListItemSettingBinding.inflate(inflater), this) } @@ -181,21 +189,30 @@ class SettingsAdapter( SettingsItem.TYPE_SLIDER -> { (oldItem as SliderSetting).isEnabled == (newItem as SliderSetting).isEnabled } + SettingsItem.TYPE_SWITCH -> { (oldItem as SwitchSetting).isEnabled == (newItem as SwitchSetting).isEnabled } + SettingsItem.TYPE_SINGLE_CHOICE -> { (oldItem as SingleChoiceSetting).isEnabled == (newItem as SingleChoiceSetting).isEnabled } + SettingsItem.TYPE_MULTI_CHOICE -> { + (oldItem as MultiChoiceSetting).isEnabled == (newItem as MultiChoiceSetting).isEnabled + } + SettingsItem.TYPE_DATETIME_SETTING -> { (oldItem as DateTimeSetting).isEnabled == (newItem as DateTimeSetting).isEnabled } + SettingsItem.TYPE_STRING_SINGLE_CHOICE -> { (oldItem as StringSingleChoiceSetting).isEnabled == (newItem as StringSingleChoiceSetting).isEnabled } + SettingsItem.TYPE_STRING_INPUT -> { (oldItem as StringInputSetting).isEnabled == (newItem as StringInputSetting).isEnabled } + else -> { oldItem == newItem } @@ -214,7 +231,7 @@ class SettingsAdapter( // If statement is required otherwise the app will crash on activity recreate ex. theme settings if (fragmentView.activityView != null) - // Reload the settings list to update the UI + // Reload the settings list to update the UI fragmentView.loadSettingsList() } @@ -232,6 +249,27 @@ class SettingsAdapter( onSingleChoiceClick(item) } + private fun onMultiChoiceClick(item: MultiChoiceSetting) { + clickedItem = item + + val value: BooleanArray = getSelectionForMultiChoiceValue(item); + dialog = MaterialAlertDialogBuilder(context) + .setTitle(item.nameId) + .setMultiChoiceItems(item.choicesId, value, this) + .setOnDismissListener { + if (clickedPosition != -1) { + notifyItemChanged(clickedPosition) + clickedPosition = -1 + } + } + .show() + } + + fun onMultiChoiceClick(item: MultiChoiceSetting, position: Int) { + clickedPosition = position + onMultiChoiceClick(item) + } + private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) { clickedItem = item dialog = context?.let { @@ -360,14 +398,14 @@ class SettingsAdapter( sliderString = sliderProgress.roundToInt().toString() if (textSliderValue?.text.toString() != sliderString) { textSliderValue?.setText(sliderString) - textSliderValue?.setSelection(textSliderValue?.length() ?: 0 ) + textSliderValue?.setSelection(textSliderValue?.length() ?: 0) } } else { val currentText = textSliderValue?.text.toString() val currentTextValue = currentText.toFloat() if (currentTextValue != sliderProgress) { textSliderValue?.setText(sliderString) - textSliderValue?.setSelection(textSliderValue?.length() ?: 0 ) + textSliderValue?.setSelection(textSliderValue?.length() ?: 0) } } } @@ -447,6 +485,7 @@ class SettingsAdapter( } it.setSelectedValue(value) } + is AbstractShortSetting -> { val value = getValueForSingleChoiceSelection(it, which).toShort() if (it.selectedValue.toShort() != value) { @@ -454,6 +493,7 @@ class SettingsAdapter( } it.setSelectedValue(value) } + else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!") } fragmentView?.putSetting(setting) @@ -499,11 +539,12 @@ class SettingsAdapter( val setting = it.setSelectedValue(value) fragmentView?.putSetting(setting) } + else -> { val setting = it.setSelectedValue(sliderProgress) fragmentView?.putSetting(setting) } - } + } fragmentView.loadSettingsList() closeDialog() } @@ -519,7 +560,7 @@ class SettingsAdapter( fragmentView?.putSetting(setting) fragmentView.loadSettingsList() closeDialog() - } + } } } clickedItem = null @@ -527,6 +568,21 @@ class SettingsAdapter( textInputValue = "" } + //onclick for multichoice + override fun onClick(dialog: DialogInterface?, which: Int, isChecked: Boolean) { + val mcsetting = clickedItem as? MultiChoiceSetting + mcsetting?.let { + val value = getValueForMultiChoiceSelection(it, which) + if (it.selectedValues.contains(value) != isChecked) { + val setting = it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted()) + fragmentView?.putSetting(setting) + fragmentView?.onSettingChanged() + } + fragmentView.loadSettingsList() + } + } + + fun onLongClick(setting: AbstractSetting, position: Int): Boolean { MaterialAlertDialogBuilder(context) .setMessage(R.string.reset_setting_confirmation) @@ -631,6 +687,16 @@ class SettingsAdapter( } } + private fun getValueForMultiChoiceSelection(item: MultiChoiceSetting, which: Int): Int { + val valuesId = item.valuesId + return if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId) + valuesArray[which] + } else { + which + } + } + private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { val value = item.selectedValue val valuesId = item.valuesId @@ -647,4 +713,20 @@ class SettingsAdapter( } return -1 } + + private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray { + val value = item.selectedValues; + val valuesId = item.valuesId; + if (valuesId > 0) { + val valuesArray = context.resources.getIntArray(valuesId); + val res = BooleanArray(valuesArray.size){false} + for (index in valuesArray.indices) { + if (value.contains(valuesArray[index])) { + res[index] = true; + } + } + return res; + } + return BooleanArray(1){false}; + } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 9a40ba90b..be70309ac 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -14,6 +14,7 @@ import android.os.Build import android.text.TextUtils import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.serialization.builtins.IntArraySerializer import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R import org.citra.citra_emu.display.ScreenLayout @@ -27,12 +28,14 @@ import org.citra.citra_emu.features.settings.model.AbstractStringSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting import org.citra.citra_emu.features.settings.model.IntSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.features.settings.model.StringSetting import org.citra.citra_emu.features.settings.model.view.DateTimeSetting import org.citra.citra_emu.features.settings.model.view.HeaderSetting import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting import org.citra.citra_emu.features.settings.model.view.RunnableSetting import org.citra.citra_emu.features.settings.model.view.SettingsItem import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting @@ -1157,6 +1160,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) BooleanSetting.UPRIGHT_SCREEN.defaultValue ) ) + add( + MultiChoiceSetting( + IntListSetting.LAYOUTS_TO_CYCLE, + R.string.layouts_to_cycle, + R.string.layouts_to_cycle_description, + R.array.landscapeLayouts, + R.array.landscapeLayoutValues, + IntListSetting.LAYOUTS_TO_CYCLE.key, + IntListSetting.LAYOUTS_TO_CYCLE.defaultValue + ) + ) add( SingleChoiceSetting( IntSetting.PORTRAIT_SCREEN_LAYOUT, diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt new file mode 100644 index 000000000..8493115a4 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/MultiChoiceViewHolder.kt @@ -0,0 +1,80 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.features.settings.ui.viewholder + +import android.view.View +import org.citra.citra_emu.databinding.ListItemSettingBinding +import org.citra.citra_emu.features.settings.model.view.SettingsItem +import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting +import org.citra.citra_emu.features.settings.ui.SettingsAdapter + +class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : + SettingViewHolder(binding.root, adapter) { + private lateinit var setting: SettingsItem + + override fun bind(item: SettingsItem) { + setting = item + binding.textSettingName.setText(item.nameId) + if (item.descriptionId != 0) { + binding.textSettingDescription.visibility = View.VISIBLE + binding.textSettingDescription.setText(item.descriptionId) + } else { + binding.textSettingDescription.visibility = View.GONE + } + binding.textSettingValue.visibility = View.VISIBLE + binding.textSettingValue.text = getTextSetting() + + if (setting.isActive) { + binding.textSettingName.alpha = 1f + binding.textSettingDescription.alpha = 1f + binding.textSettingValue.alpha = 1f + } else { + binding.textSettingName.alpha = 0.5f + binding.textSettingDescription.alpha = 0.5f + binding.textSettingValue.alpha = 0.5f + } + } + + private fun getTextSetting(): String { + when (val item = setting) { + is MultiChoiceSetting -> { + val resMgr = binding.textSettingDescription.context.resources + val values = resMgr.getIntArray(item.valuesId) + var resList:List = emptyList(); + values.forEachIndexed { i: Int, value: Int -> + if ((setting as MultiChoiceSetting).selectedValues.contains(value)) { + resList = resList + resMgr.getStringArray(item.choicesId)[i]; + } + } + return resList.joinToString(); + } + + else -> return "" + } + } + + override fun onClick(clicked: View) { + if (!setting.isEditable || !setting.isEnabled) { + adapter.onClickDisabledSetting(!setting.isEditable) + return + } + + if (setting is MultiChoiceSetting) { + adapter.onMultiChoiceClick( + (setting as MultiChoiceSetting), + bindingAdapterPosition + ) + } + } + + override fun onLongClick(clicked: View): Boolean { + if (setting.isActive) { + return adapter.onLongClick(setting.setting!!, bindingAdapterPosition) + } else { + adapter.onClickDisabledSetting(!setting.isEditable) + } + return false + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt index dec3e4e0a..a9e1d4743 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -12,6 +12,7 @@ import org.citra.citra_emu.R import org.citra.citra_emu.features.settings.model.AbstractSetting import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.FloatSetting +import org.citra.citra_emu.features.settings.model.IntListSetting import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.ScaledFloatSetting import org.citra.citra_emu.features.settings.model.SettingSection @@ -255,6 +256,11 @@ object SettingsFile { return stringSetting } + val intListSetting = IntListSetting.from(key) + if (intListSetting != null) { + intListSetting.list = value.split(", ").map { it.toInt() } + } + return null } diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index de2a78ee9..7e8257df3 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -361,6 +361,8 @@ Reverse Landscape Portrait Reverse Portrait + Layouts to Cycle + Which layouts are cycled by the Cycle Layout hotkey Default 16:9 4:3 diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt index e6faa88ce..fcb7eabe5 100644 --- a/src/citra_qt/CMakeLists.txt +++ b/src/citra_qt/CMakeLists.txt @@ -49,6 +49,9 @@ add_library(citra_qt STATIC EXCLUDE_FROM_ALL configuration/configure_layout.cpp configuration/configure_layout.h configuration/configure_layout.ui + configuration/configure_layout_cycle.cpp + configuration/configure_layout_cycle.h + configuration/configure_layout_cycle.ui configuration/configure_dialog.cpp configuration/configure_dialog.h configuration/configure_general.cpp diff --git a/src/citra_qt/citra_qt.cpp b/src/citra_qt/citra_qt.cpp index d8e207ec9..c28418eac 100644 --- a/src/citra_qt/citra_qt.cpp +++ b/src/citra_qt/citra_qt.cpp @@ -2741,25 +2741,22 @@ void GMainWindow::AdjustSpeedLimit(bool increase) { void GMainWindow::ToggleScreenLayout() { const Settings::LayoutOption new_layout = []() { - switch (Settings::values.layout_option.GetValue()) { - case Settings::LayoutOption::Default: - return Settings::LayoutOption::SingleScreen; - case Settings::LayoutOption::SingleScreen: - return Settings::LayoutOption::LargeScreen; - case Settings::LayoutOption::LargeScreen: - return Settings::LayoutOption::HybridScreen; - case Settings::LayoutOption::HybridScreen: - return Settings::LayoutOption::SideScreen; - case Settings::LayoutOption::SideScreen: - return Settings::LayoutOption::SeparateWindows; - case Settings::LayoutOption::SeparateWindows: - return Settings::LayoutOption::CustomLayout; - case Settings::LayoutOption::CustomLayout: - return Settings::LayoutOption::Default; - default: - LOG_ERROR(Frontend, "Unknown layout option {}", - Settings::values.layout_option.GetValue()); - return Settings::LayoutOption::Default; + const Settings::LayoutOption current_layout = Settings::values.layout_option.GetValue(); + std::vector layouts_to_cycle = + Settings::values.layouts_to_cycle.GetValue(); + const auto current_pos = + distance(layouts_to_cycle.begin(), + std::find(layouts_to_cycle.begin(), layouts_to_cycle.end(), current_layout)); + // if the layouts_to_cycle setting has somehow been + // cleared out, add just default back in + if (layouts_to_cycle.size() == 0) { + layouts_to_cycle.push_back(Settings::LayoutOption::Default); + } + if (current_pos >= layouts_to_cycle.size() - 1) { + // either this layout wasn't found or it was last so move to the beginning + return layouts_to_cycle[0]; + } else { + return layouts_to_cycle[current_pos + 1]; } }(); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index 35abe81ef..e909fd4d9 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include "citra_qt/configuration/config.h" #include "common/file_util.h" #include "common/settings.h" @@ -132,6 +134,35 @@ void QtConfig::ReadBasicSetting(Settings::Setting& setting) { setting.SetValue(qt_config->value(name, default_value).toString().toStdString()); } } +// definition for vectors of enums +template +void QtConfig::ReadBasicSetting(Settings::Setting, ranged>& setting) { + const QString name = QString::fromStdString(setting.GetLabel()); + const std::vector default_value = setting.GetDefault(); + QStringList stringList = qt_config->value(name).toStringList(); + + if (qt_config->value(name + QStringLiteral("/default"), false).toBool() || + stringList.size() < 1) { + setting.SetValue(default_value); + } else { + if (stringList.size() < 1) { + setting.SetValue(default_value); + } else { + std::vector newValue; + for (const QString& str : stringList) { + if constexpr (std::is_enum_v) { + using TypeU = std::underlying_type_t; + newValue.push_back(static_cast(str.toInt())); + } else if constexpr (std::is_integral_v) { + newValue.push_back(str.toInt()); + } else { + newValue.push_back(str.toStdString()); + } + } + setting.SetValue(newValue); + } + } +} template void QtConfig::ReadBasicSetting(Settings::Setting& setting) { @@ -158,27 +189,7 @@ void QtConfig::ReadGlobalSetting(Settings::SwitchableSetting& sett const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool(); setting.SetGlobal(use_global); if (global || !use_global) { - QVariant default_value{}; - if constexpr (std::is_enum_v) { - using TypeU = std::underlying_type_t; - default_value = QVariant::fromValue(static_cast(setting.GetDefault())); - setting.SetValue(static_cast(ReadSetting(name, default_value).value())); - } else { - default_value = QVariant::fromValue(setting.GetDefault()); - setting.SetValue(ReadSetting(name, default_value).value()); - } - } -} - -template <> -void QtConfig::ReadGlobalSetting(Settings::SwitchableSetting& setting) { - QString name = QString::fromStdString(setting.GetLabel()); - const bool use_global = qt_config->value(name + QStringLiteral("/use_global"), true).toBool(); - setting.SetGlobal(use_global); - if (global || !use_global) { - const QString default_value = QString::fromStdString(setting.GetDefault()); - setting.SetValue( - ReadSetting(name, QVariant::fromValue(default_value)).toString().toStdString()); + ReadBasicSetting(setting); } } @@ -187,16 +198,41 @@ template <> void QtConfig::WriteBasicSetting(const Settings::Setting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); const std::string& value = setting.GetValue(); - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); qt_config->setValue(name, QString::fromStdString(value)); } +template +void QtConfig::WriteBasicSetting(const Settings::Setting, ranged>& setting) { + const QString name = QString::fromStdString(setting.GetLabel()); + const std::vector& value = setting.GetValue(); + + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + + QStringList stringList; + if constexpr (std::is_enum_v) { + // For enums, convert to underlying integer type strings + using TypeU = std::underlying_type_t; + for (const Type& item : value) { + stringList.append(QString::number(static_cast(item))); + } + } else { + // For non-enum types (assuming numeric) + for (const Type& item : value) { + stringList.append(QString::number(item)); + } + } + qt_config->setValue(name, stringList); +} // Explicit u16 definition: Qt would store it as QMetaType otherwise, which is not human-readable template <> void QtConfig::WriteBasicSetting(const Settings::Setting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); const u16& value = setting.GetValue(); - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); qt_config->setValue(name, static_cast(value)); } @@ -204,7 +240,8 @@ template void QtConfig::WriteBasicSetting(const Settings::Setting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); const Type value = setting.GetValue(); - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); if constexpr (std::is_enum_v) { qt_config->setValue(name, static_cast>(value)); } else { @@ -215,44 +252,11 @@ void QtConfig::WriteBasicSetting(const Settings::Setting& setting) template void QtConfig::WriteGlobalSetting(const Settings::SwitchableSetting& setting) { const QString name = QString::fromStdString(setting.GetLabel()); - const Type& value = setting.GetValue(global); if (!global) { qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal()); } if (global || !setting.UsingGlobal()) { - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); - if constexpr (std::is_enum_v) { - qt_config->setValue(name, static_cast>(value)); - } else { - qt_config->setValue(name, QVariant::fromValue(value)); - } - } -} - -template <> -void QtConfig::WriteGlobalSetting(const Settings::SwitchableSetting& setting) { - const QString name = QString::fromStdString(setting.GetLabel()); - const std::string& value = setting.GetValue(global); - if (!global) { - qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal()); - } - if (global || !setting.UsingGlobal()) { - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); - qt_config->setValue(name, QString::fromStdString(value)); - } -} - -// Explicit u16 definition: Qt would store it as QMetaType otherwise, which is not human-readable -template <> -void QtConfig::WriteGlobalSetting(const Settings::SwitchableSetting& setting) { - const QString name = QString::fromStdString(setting.GetLabel()); - const u16& value = setting.GetValue(global); - if (!global) { - qt_config->setValue(name + QStringLiteral("/use_global"), setting.UsingGlobal()); - } - if (global || !setting.UsingGlobal()) { - qt_config->setValue(name + QStringLiteral("/default"), value == setting.GetDefault()); - qt_config->setValue(name, static_cast(value)); + WriteBasicSetting(setting); } } @@ -528,7 +532,7 @@ void QtConfig::ReadLayoutValues() { ReadGlobalSetting(Settings::values.large_screen_proportion); ReadGlobalSetting(Settings::values.screen_gap); ReadGlobalSetting(Settings::values.small_screen_position); - + ReadGlobalSetting(Settings::values.layouts_to_cycle); if (global) { ReadBasicSetting(Settings::values.mono_render_option); ReadBasicSetting(Settings::values.custom_top_x); @@ -1109,6 +1113,7 @@ void QtConfig::SaveLayoutValues() { WriteGlobalSetting(Settings::values.large_screen_proportion); WriteGlobalSetting(Settings::values.screen_gap); WriteGlobalSetting(Settings::values.small_screen_position); + WriteGlobalSetting(Settings::values.layouts_to_cycle); if (global) { WriteBasicSetting(Settings::values.mono_render_option); WriteBasicSetting(Settings::values.custom_top_x); @@ -1445,7 +1450,8 @@ void QtConfig::WriteSetting(const QString& name, const QVariant& value) { void QtConfig::WriteSetting(const QString& name, const QVariant& value, const QVariant& default_value) { - qt_config->setValue(name + QStringLiteral("/default"), value == default_value); + if (global) + qt_config->setValue(name + QStringLiteral("/default"), value == default_value); qt_config->setValue(name, value); } diff --git a/src/citra_qt/configuration/config.h b/src/citra_qt/configuration/config.h index 2c9039dde..3fba498ed 100644 --- a/src/citra_qt/configuration/config.h +++ b/src/citra_qt/configuration/config.h @@ -120,6 +120,10 @@ private: template void ReadBasicSetting(Settings::Setting& setting); + // Add overload for vectors + template + void ReadBasicSetting(Settings::Setting, ranged>& setting); + /** Sets a value from the setting in the qt_config using the setting's label and default value. * * @param The setting @@ -127,6 +131,9 @@ private: template void WriteBasicSetting(const Settings::Setting& setting); + template + void WriteBasicSetting(const Settings::Setting, ranged>& setting); + ConfigType type; std::unique_ptr qt_config; std::string qt_config_loc; diff --git a/src/citra_qt/configuration/configure_layout.cpp b/src/citra_qt/configuration/configure_layout.cpp index 58a0e66da..1cd06d937 100644 --- a/src/citra_qt/configuration/configure_layout.cpp +++ b/src/citra_qt/configuration/configure_layout.cpp @@ -6,6 +6,7 @@ #include #include "citra_qt/configuration/configuration_shared.h" #include "citra_qt/configuration/configure_layout.h" +#include "citra_qt/configuration/configure_layout_cycle.h" #include "common/settings.h" #include "ui_configure_layout.h" #ifdef ENABLE_OPENGL @@ -111,6 +112,13 @@ ConfigureLayout::ConfigureLayout(QWidget* parent) ui->bg_button->setIcon(color_icon); ui->bg_button->setEnabled(true); }); + + connect(ui->customize_layouts_to_cycle, &QPushButton::clicked, this, [this] { + ui->customize_layouts_to_cycle->setEnabled(false); + QDialog* layout_cycle_dialog = new ConfigureLayoutCycle(this); + layout_cycle_dialog->exec(); + ui->customize_layouts_to_cycle->setEnabled(true); + }); } ConfigureLayout::~ConfigureLayout() = default; diff --git a/src/citra_qt/configuration/configure_layout.ui b/src/citra_qt/configuration/configure_layout.ui index 1125ab2fb..fc4d149dc 100644 --- a/src/citra_qt/configuration/configure_layout.ui +++ b/src/citra_qt/configuration/configure_layout.ui @@ -6,8 +6,8 @@ 0 0 - 705 - 656 + 659 + 662 @@ -51,8 +51,8 @@ 0 0 - 688 - 799 + 646 + 824 @@ -130,18 +130,39 @@ - - - Swap screens - - - - - - - Rotate screens upright - - + + + + + + + Rotate screens upright + + + + + + + Swap screens + + + + + + + + + + 0 + 0 + + + + Customize layout cycling + + + + @@ -356,7 +377,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -376,7 +397,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -396,7 +417,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -416,7 +437,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -440,6 +461,12 @@ Bottom Screen + + false + + + false + @@ -451,7 +478,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -471,7 +498,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -491,7 +518,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -511,7 +538,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -538,7 +565,7 @@ - QAbstractSpinBox::ButtonSymbols::PlusMinus + QAbstractSpinBox::UpDownArrows 10 @@ -583,7 +610,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -610,7 +637,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -659,7 +686,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -672,7 +699,7 @@ - QAbstractSpinBox::ButtonSymbols::NoButtons + QAbstractSpinBox::UpDownArrows px @@ -717,9 +744,6 @@ - - Qt::Orientation::Vertical - 20 diff --git a/src/citra_qt/configuration/configure_layout_cycle.cpp b/src/citra_qt/configuration/configure_layout_cycle.cpp new file mode 100644 index 000000000..11f1f116f --- /dev/null +++ b/src/citra_qt/configuration/configure_layout_cycle.cpp @@ -0,0 +1,92 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +#include +#include +#include +#include "citra_qt/configuration/configure_layout_cycle.h" +#include "ui_configure_layout_cycle.h" + +ConfigureLayoutCycle::ConfigureLayoutCycle(QWidget* parent) + : QDialog(parent), ui(std::make_unique()) { + ui->setupUi(this); + SetConfiguration(); + ConnectEvents(); +} + +// You MUST define the destructor in the .cpp file +ConfigureLayoutCycle::~ConfigureLayoutCycle() = default; + +void ConfigureLayoutCycle::ConnectEvents() { + disconnect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, + &ConfigureLayoutCycle::ApplyConfiguration); + connect(ui->globalCheck, &QCheckBox::stateChanged, this, &ConfigureLayoutCycle::UpdateGlobal); +} + +void ConfigureLayoutCycle::SetConfiguration() { + if (Settings::IsConfiguringGlobal()) { + ui->globalCheck->setChecked(true); + ui->globalCheck->setVisible(false); + } else { + ui->globalCheck->setChecked(Settings::values.layouts_to_cycle.UsingGlobal()); + ui->checkGroup->setDisabled(Settings::values.layouts_to_cycle.UsingGlobal()); + } + for (auto option : Settings::values.layouts_to_cycle.GetValue()) { + switch (option) { + case Settings::LayoutOption::Default: + ui->defaultCheck->setChecked(true); + break; + case Settings::LayoutOption::SingleScreen: + ui->singleCheck->setChecked(true); + break; + case Settings::LayoutOption::LargeScreen: + ui->largeCheck->setChecked(true); + break; + case Settings::LayoutOption::SideScreen: + ui->sidebysideCheck->setChecked(true); + break; + case Settings::LayoutOption::SeparateWindows: + ui->separateCheck->setChecked(true); + break; + case Settings::LayoutOption::HybridScreen: + ui->hybridCheck->setChecked(true); + break; + case Settings::LayoutOption::CustomLayout: + ui->customCheck->setChecked(true); + break; + } + } +} + +void ConfigureLayoutCycle::ApplyConfiguration() { + std::vector newSetting{}; + if (ui->defaultCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::Default); + if (ui->singleCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::SingleScreen); + if (ui->sidebysideCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::SideScreen); + if (ui->largeCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::LargeScreen); + if (ui->separateCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::SeparateWindows); + if (ui->hybridCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::HybridScreen); + if (ui->customCheck->isChecked()) + newSetting.push_back(Settings::LayoutOption::CustomLayout); + if (newSetting.empty()) { + QMessageBox::warning(this, tr("No Layout Selected"), + tr("Please select at least one layout option to cycle through.")); + return; + } else { + Settings::values.layouts_to_cycle = newSetting; + accept(); + } +} + +void ConfigureLayoutCycle::UpdateGlobal() { + Settings::values.layouts_to_cycle.SetGlobal(ui->globalCheck->isChecked()); + ui->checkGroup->setDisabled(ui->globalCheck->isChecked()); + ui->checkGroup->repaint(); // Force visual update +} diff --git a/src/citra_qt/configuration/configure_layout_cycle.h b/src/citra_qt/configuration/configure_layout_cycle.h new file mode 100644 index 000000000..bde8f3cb9 --- /dev/null +++ b/src/citra_qt/configuration/configure_layout_cycle.h @@ -0,0 +1,32 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. +#pragma once + +#include +#include +#include "common/settings.h" + +namespace Ui { +class ConfigureLayoutCycle; +} + +class ConfigureLayoutCycle : public QDialog { + Q_OBJECT + +public: + explicit ConfigureLayoutCycle(QWidget* parent = nullptr); + ~ConfigureLayoutCycle() override; + +public slots: + void ApplyConfiguration(); + +private slots: + +private: + void SetConfiguration(); + void ConnectEvents(); + void UpdateGlobal(); + + std::unique_ptr ui; +}; \ No newline at end of file diff --git a/src/citra_qt/configuration/configure_layout_cycle.ui b/src/citra_qt/configuration/configure_layout_cycle.ui new file mode 100644 index 000000000..e1d6aef4f --- /dev/null +++ b/src/citra_qt/configuration/configure_layout_cycle.ui @@ -0,0 +1,216 @@ + + + ConfigureLayoutCycle + + + Qt::ApplicationModal + + + + 0 + 0 + 395 + 334 + + + + + 0 + 0 + + + + Configure Layout Cycling + + + + + 10 + 10 + 381 + 323 + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + 14 + + + + Screen Layout Cycling Customization + + + + + + + + 0 + 0 + + + + Select which screen layout options should be cycled with the "Toggle Screen Layout" hotkey + + + true + + + + + + + Use Global Value + + + true + + + + + + + true + + + + + + Qt::Horizontal + + + + + + + Default + + + false + + + + + + + Single Screen + + + false + + + + + + + Large Screen + + + false + + + + + + + Side by Side + + + false + + + + + + + Separate Windows + + + false + + + + + + + Hybrid + + + false + + + + + + + Custom + + + false + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + ConfigureLayoutCycle + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ConfigureLayoutCycle + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/common/settings.cpp b/src/common/settings.cpp index d0a08b587..eb8e9be3b 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -124,6 +124,7 @@ void LogSettings() { log_setting("Layout_ScreenGap", values.screen_gap.GetValue()); log_setting("Layout_LargeScreenProportion", values.large_screen_proportion.GetValue()); log_setting("Layout_SmallScreenPosition", values.small_screen_position.GetValue()); + // log_setting("Layout_LayoutsToCycle",values.layouts_to_cycle.GetValue()); log_setting("Utility_DumpTextures", values.dump_textures.GetValue()); log_setting("Utility_CustomTextures", values.custom_textures.GetValue()); log_setting("Utility_PreloadTextures", values.preload_textures.GetValue()); @@ -215,6 +216,7 @@ void RestoreGlobalState(bool is_powered_on) { values.layout_option.SetGlobal(true); values.portrait_layout_option.SetGlobal(true); values.secondary_display_layout.SetGlobal(true); + values.layouts_to_cycle.SetGlobal(true); values.swap_screen.SetGlobal(true); values.upright_screen.SetGlobal(true); values.large_screen_proportion.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 178db0019..0f0bdd23f 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -538,6 +538,14 @@ struct Values { SwitchableSetting upright_screen{false, "upright_screen"}; SwitchableSetting secondary_display_layout{SecondaryDisplayLayout::None, "secondary_display_layout"}; + SwitchableSetting> layouts_to_cycle{ + {LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen, + LayoutOption::SideScreen, +#ifndef ANDROID + LayoutOption::SeparateWindows, +#endif + LayoutOption::HybridScreen, LayoutOption::CustomLayout}, + "layouts_to_cycle"}; SwitchableSetting large_screen_proportion{4.f, 1.f, 16.f, "large_screen_proportion"}; SwitchableSetting screen_gap{0, "screen_gap"}; From 526d9d4cea8da1e1d9e704d2c0bfcc74acb70431 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 27 Feb 2026 12:57:41 -0600 Subject: [PATCH 37/94] android: Add auto-map controller button with long-press to clear all bindings (#1769) --- .../model/view/InputBindingSetting.kt | 256 +++++++++++++++++- .../settings/model/view/RunnableSetting.kt | 9 +- .../settings/model/view/SettingsItem.kt | 2 +- .../features/settings/ui/SettingsAdapter.kt | 47 +++- .../settings/ui/SettingsFragmentPresenter.kt | 10 + .../ui/viewholder/RunnableViewHolder.kt | 7 +- .../fragments/AutoMapDialogFragment.kt | 152 +++++++++++ .../drawable-xxxhdpi/automap_face_buttons.png | Bin 0 -> 33023 bytes .../src/main/res/layout/dialog_auto_map.xml | 55 ++++ .../app/src/main/res/values/strings.xml | 6 + 10 files changed, 509 insertions(+), 35 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt create mode 100644 src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png create mode 100644 src/android/app/src/main/res/layout/dialog_auto_map.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt index d78f5c3a3..6ec851db1 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt @@ -9,6 +9,7 @@ import android.content.SharedPreferences import android.view.InputDevice import android.view.InputDevice.MotionRange import android.view.KeyEvent +import android.view.MotionEvent import android.widget.Toast import androidx.preference.PreferenceManager import org.citra.citra_emu.CitraApplication @@ -235,8 +236,7 @@ class InputBindingSetting( val code = translateEventToKeyId(keyEvent) writeButtonMapping(keyEvent) - val uiString = "${keyEvent.device.name}: Button $code" - value = uiString + value = "${keyEvent.device.name}: ${getButtonName(code)}" } /** @@ -263,8 +263,7 @@ class InputBindingSetting( // use UP (-) to map vertical, but use RIGHT (+) to map horizontal val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+' writeAxisMapping(motionRange.axis, button, inverted) - val uiString = "${device.name}: Axis ${motionRange.axis}" + axisDir - value = uiString + value = "Axis ${motionRange.axis}$axisDir" } override val type = TYPE_INPUT_BINDING @@ -272,6 +271,241 @@ class InputBindingSetting( companion object { private const val INPUT_MAPPING_PREFIX = "InputMapping" + private fun toTitleCase(raw: String): String = + raw.replace("_", " ").lowercase() + .split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } + + private const val BUTTON_NAME_L3 = "Button L3" + private const val BUTTON_NAME_R3 = "Button R3" + + private val buttonNameOverrides = mapOf( + KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3, + KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3, + LINUX_BTN_DPAD_UP to "Dpad Up", + LINUX_BTN_DPAD_DOWN to "Dpad Down", + LINUX_BTN_DPAD_LEFT to "Dpad Left", + LINUX_BTN_DPAD_RIGHT to "Dpad Right" + ) + + fun getButtonName(keyCode: Int): String = + buttonNameOverrides[keyCode] + ?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_")) + + private data class DefaultButtonMapping( + val settingKey: String, + val hostKeyCode: Int, + val guestButtonCode: Int + ) + // Auto-map always sets inverted = false. Users needing inverted axes should remap manually. + private data class DefaultAxisMapping( + val settingKey: String, + val hostAxis: Int, + val guestButton: Int, + val orientation: Int, + val inverted: Boolean + ) + + private val xboxFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y) + ) + + private val nintendoFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) + ) + + private val commonButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L), + DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R), + DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL), + DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR), + DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT), + DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START) + ) + + private val dpadButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), + DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), + DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), + DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) + ) + + private val stickAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false) + ) + + private val dpadAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false), + DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false) + ) + + // Nintendo Switch Joy-Con specific mappings. + // Joy-Cons connected via Bluetooth on Android have several quirks: + // - They register as two separate InputDevices (left and right) + // - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A) + // but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y) + // - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes + // - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ + private const val NINTENDO_VENDOR_ID = 0x057e + + // Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as + // KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't + // translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to + // the scan code in that case. + private const val LINUX_BTN_DPAD_UP = 0x220 // 544 + private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545 + private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546 + private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547 + + // Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not. + // This is different from both the standard Xbox table (full swap) and the + // Nintendo table (no swap). + private val joyconFaceButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A), + DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B), + DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X), + DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y) + ) + + // Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN + private val joyconDpadButtonMappings = listOf( + DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP), + DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN), + DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT), + DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT) + ) + + // Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY + // (not Z/RZ like most controllers). The horizontal axis is inverted relative to + // the standard orientation - verified empirically on paired Joy-Cons via Bluetooth. + private val joyconStickAxisMappings = listOf( + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false), + DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true), + DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false) + ) + + /** + * Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a + * Pro Controller or other Nintendo device) by checking vendor ID + device + * capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the + * right stick, while the Pro Controller has standard HAT axes and Z/RZ. + */ + fun isJoyCon(device: InputDevice?): Boolean { + if (device == null) return false + if (device.vendorId != NINTENDO_VENDOR_ID) return false + + // Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick). + // Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ. + var hasHatAxes = false + var hasStandardRightStick = false + for (range in device.motionRanges) { + when (range.axis) { + MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true + MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true + } + } + return !hasHatAxes && !hasStandardRightStick + } + + private val allBindingKeys: Set by lazy { + (Settings.buttonKeys + Settings.triggerKeys + + Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys + + Settings.dPadButtonKeys).toSet() + } + + fun clearAllBindings() { + val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val editor = prefs.edit() + val allKeys = prefs.all.keys.toList() + for (key in allKeys) { + if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) { + editor.remove(key) + } + } + editor.apply() + } + + private fun applyBindings( + buttonMappings: List, + axisMappings: List + ) { + val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext) + val editor = prefs.edit() + buttonMappings.forEach { applyDefaultButtonMapping(editor, it) } + axisMappings.forEach { applyDefaultAxisMapping(editor, it) } + editor.apply() + } + + /** + * Applies Joy-Con specific bindings: scan code D-pad, partial face button + * swap, and AXIS_RX/RY right stick. + */ + fun applyJoyConBindings() { + applyBindings( + joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings, + joyconStickAxisMappings + ) + } + + /** + * Applies auto-mapped bindings based on detected controller layout and d-pad type. + * + * @param isNintendoLayout true if the controller uses Nintendo face button layout + * (A=east, B=south), false for Xbox layout (A=south, B=east) + * @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y), + * false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT) + */ + fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) { + val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings + val buttonMappings = if (useAxisDpad) { + faceButtons + commonButtonMappings + } else { + faceButtons + commonButtonMappings + dpadButtonMappings + } + val axisMappings = if (useAxisDpad) { + stickAxisMappings + dpadAxisMappings + } else { + stickAxisMappings + } + applyBindings(buttonMappings, axisMappings) + } + + private fun applyDefaultButtonMapping( + editor: SharedPreferences.Editor, + mapping: DefaultButtonMapping + ) { + val prefKey = getInputButtonKey(mapping.hostKeyCode) + editor.putInt(prefKey, mapping.guestButtonCode) + editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode)) + editor.putString( + "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}", + prefKey + ) + } + + private fun applyDefaultAxisMapping( + editor: SharedPreferences.Editor, + mapping: DefaultAxisMapping + ) { + val axisKey = getInputAxisKey(mapping.hostAxis) + editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation) + editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton) + editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted) + val dir = if (mapping.orientation == 0) '+' else '-' + editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir") + val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}" + editor.putString(reverseKey, axisKey) + } + /** * Returns the settings key for the specified Citra button code. */ @@ -315,18 +549,10 @@ class InputBindingSetting( return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet() } - /** - * Helper function to get the settings key for an gamepad button. - * - */ - @Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys") - fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}" + private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}" - /** - * Helper function to get the settings key for an gamepad button. - * - */ - fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}" + /** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */ + fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event)) /** * Helper function to get the settings key for an gamepad axis. diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt index 99039556b..54e8fd09b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt @@ -1,10 +1,11 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. package org.citra.citra_emu.features.settings.model.view import androidx.annotation.DrawableRes +import org.citra.citra_emu.activities.EmulationActivity class RunnableSetting( titleId: Int, @@ -12,7 +13,11 @@ class RunnableSetting( val isRuntimeRunnable: Boolean, @DrawableRes val iconId: Int = 0, val runnable: () -> Unit, - val value: (() -> String)? = null + val value: (() -> String)? = null, + val onLongClick: (() -> Boolean)? = null ) : SettingsItem(null, titleId, descriptionId) { override val type = TYPE_RUNNABLE + + override val isEditable: Boolean + get() = if (EmulationActivity.isRunning()) isRuntimeRunnable else true } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt index 68aa2226c..066912dd9 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt @@ -22,7 +22,7 @@ abstract class SettingsItem( ) { abstract val type: Int - val isEditable: Boolean + open val isEditable: Boolean get() { if (!EmulationActivity.isRunning()) return true return setting?.isRuntimeEditable ?: false diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt index 054ff8d63..43a1dcbbd 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt @@ -65,6 +65,7 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder +import org.citra.citra_emu.fragments.AutoMapDialogFragment import org.citra.citra_emu.fragments.MessageDialogFragment import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment import org.citra.citra_emu.utils.SystemSaveGame @@ -642,26 +643,42 @@ class SettingsAdapter( ).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG) } + fun onClickAutoMap() { + val activity = fragmentView.activityView as FragmentActivity + AutoMapDialogFragment.newInstance { + fragmentView.loadSettingsList() + fragmentView.onSettingChanged() + }.show(activity.supportFragmentManager, AutoMapDialogFragment.TAG) + } + + fun onLongClickAutoMap(): Boolean { + showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) { + InputBindingSetting.clearAllBindings() + fragmentView.loadSettingsList() + fragmentView.onSettingChanged() + } + return true + } + fun onClickRegenerateConsoleId() { - MaterialAlertDialogBuilder(context) - .setTitle(R.string.regenerate_console_id) - .setMessage(R.string.regenerate_console_id_description) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - SystemSaveGame.regenerateConsoleId() - notifyDataSetChanged() - } - .setNegativeButton(android.R.string.cancel, null) - .show() + showConfirmationDialog(R.string.regenerate_console_id, R.string.regenerate_console_id_description) { + SystemSaveGame.regenerateConsoleId() + notifyDataSetChanged() + } } fun onClickRegenerateMAC() { + showConfirmationDialog(R.string.regenerate_mac_address, R.string.regenerate_mac_address_description) { + SystemSaveGame.regenerateMac() + notifyDataSetChanged() + } + } + + private fun showConfirmationDialog(titleId: Int, messageId: Int, onConfirm: () -> Unit) { MaterialAlertDialogBuilder(context) - .setTitle(R.string.regenerate_mac_address) - .setMessage(R.string.regenerate_mac_address_description) - .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - SystemSaveGame.regenerateMac() - notifyDataSetChanged() - } + .setTitle(titleId) + .setMessage(messageId) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> onConfirm() } .setNegativeButton(android.R.string.cancel, null) .show() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index be70309ac..1b7812342 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -779,6 +779,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) private fun addControlsSettings(sl: ArrayList) { settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls)) sl.apply { + add( + RunnableSetting( + R.string.controller_auto_map, + R.string.controller_auto_map_description, + true, + R.drawable.ic_controller, + { settingsAdapter.onClickAutoMap() }, + onLongClick = { settingsAdapter.onLongClickAutoMap() } + ) + ) add(HeaderSetting(R.string.generic_buttons)) Settings.buttonKeys.forEachIndexed { i: Int, key: String -> val button = getInputObject(key) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt index e3119e60d..d75368598 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt @@ -67,7 +67,10 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA } override fun onLongClick(clicked: View): Boolean { - // no-op - return true + if (!setting.isEditable) { + adapter.onClickDisabledSetting(true) + return true + } + return setting.onLongClick?.invoke() ?: true } } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt new file mode 100644 index 000000000..569a0caca --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/AutoMapDialogFragment.kt @@ -0,0 +1,152 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +package org.citra.citra_emu.fragments + +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.citra.citra_emu.R +import org.citra.citra_emu.databinding.DialogAutoMapBinding +import org.citra.citra_emu.features.settings.model.view.InputBindingSetting +import org.citra.citra_emu.utils.Log + +/** + * Captures a single button press to detect controller layout (Xbox vs Nintendo) + * and d-pad type (axis vs button), then applies the appropriate bindings. + */ +class AutoMapDialogFragment : BottomSheetDialogFragment() { + private var _binding: DialogAutoMapBinding? = null + private val binding get() = _binding!! + + private var onComplete: (() -> Unit)? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogAutoMapBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + BottomSheetBehavior.from(view.parent as View).state = + BottomSheetBehavior.STATE_EXPANDED + + isCancelable = false + view.requestFocus() + view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() } + + binding.textTitle.setText(R.string.controller_auto_map) + binding.textMessage.setText(R.string.auto_map_prompt) + + binding.imageFaceButtons.setImageResource(R.drawable.automap_face_buttons) + + dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) } + + binding.buttonCancel.setOnClickListener { + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun onKeyEvent(event: KeyEvent): Boolean { + if (event.action != KeyEvent.ACTION_UP) return false + + val keyCode = event.keyCode + val device = event.device + + // Check if this is a Nintendo Switch Joy-Con (not Pro Controller). + // Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes, + // partial A/B swap but no X/Y swap from Android's evdev layer. + val isJoyCon = InputBindingSetting.isJoyCon(device) + + if (isJoyCon) { + Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings") + InputBindingSetting.clearAllBindings() + InputBindingSetting.applyJoyConBindings() + onComplete?.invoke() + dismiss() + return true + } + + // For non-Joy-Con controllers, determine layout from which keycode arrives + // for the east/right position. + // The user is pressing the button in the "A" (east/right) position on the 3DS diamond. + // Xbox layout: east position sends KEYCODE_BUTTON_B (97) + // Nintendo layout: east position sends KEYCODE_BUTTON_A (96) + val isNintendoLayout = when (keyCode) { + KeyEvent.KEYCODE_BUTTON_A -> true + KeyEvent.KEYCODE_BUTTON_B -> false + else -> { + // Unrecognized button - ignore and wait for a valid press + Log.warning("[AutoMap] Ignoring unrecognized keycode $keyCode, waiting for A or B") + return true + } + } + + val layoutName = if (isNintendoLayout) "Nintendo" else "Xbox" + Log.info("[AutoMap] Detected $layoutName layout (keyCode=$keyCode)") + + val useAxisDpad = detectDpadType(device) + + val dpadName = if (useAxisDpad) "axis" else "button" + Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})") + + InputBindingSetting.clearAllBindings() + InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad) + + onComplete?.invoke() + dismiss() + return true + } + + companion object { + const val TAG = "AutoMapDialogFragment" + + fun newInstance( + onComplete: () -> Unit + ): AutoMapDialogFragment { + val dialog = AutoMapDialogFragment() + dialog.onComplete = onComplete + return dialog + } + + /** + * Returns true for axis d-pad (HAT_X/HAT_Y), false for button d-pad (DPAD_UP/DOWN/LEFT/RIGHT). + * Prefers axis when both are present. Defaults to axis if detection fails. + */ + private fun detectDpadType(device: InputDevice?): Boolean { + if (device == null) return true + + val hasAxisDpad = device.motionRanges.any { + it.axis == MotionEvent.AXIS_HAT_X || it.axis == MotionEvent.AXIS_HAT_Y + } + + if (hasAxisDpad) return true + + val dpadKeyCodes = intArrayOf( + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.KEYCODE_DPAD_DOWN, + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.KEYCODE_DPAD_RIGHT + ) + val hasButtonDpad = device.hasKeys(*dpadKeyCodes).any { it } + + return !hasButtonDpad + } + } +} diff --git a/src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png b/src/android/app/src/main/res/drawable-xxxhdpi/automap_face_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..60f7c2badd1f74dfac20360111ccb8616d0fd3ec GIT binary patch literal 33023 zcmXtg2RzmP_y60TaW6t;bT1j1A!KB|MTC%|%p_!IlTF?T$yO+{BztF5DkGy@Bb(5* z_x69?&-eH5@#xWm-1qx6&Uv15p67W)+}2h*Lw%kaf}k@u)nPpdBKk}COGysClNT@a z6Z}Kus;8z574@(!f-gv2m2cjm1b?1UK0+YqB6Jg0y5sd`Y0UfCo!`d_C%cD#9Fpw0 z#cvvtk>ax6b1vsqt_YlkOR1TYiNgv$1>P^9EFzPtutG;o_Sg9pa3){w|Hl%L_%A=n z-}wHgvuL67uTn`FUUhYi^&IZ1kw;N~Pv;i*OxoRCpTsCX{+sALUqAoGmznfy%Ifvw zt%37xqO=AE?ww^jB!Ob^9n`ua|J*Rbs@xu!pr-;8LB!C-MSSsk(_Jdc54WlkK zS~g5&q@+Muclq8;$85BzG@ft}zo| zN5Z6f?S_U?I5Gp7Ly%4H_I2(6Mt{e9%rxj~lIHvTRwQVLUiahUfB*hTW^P`&yHz79 zo~es3~lur-V(ccO(= zKr(DZT{J2AVd;1JA@^eAYy;(4Wme;iW2TPn6heOKh_2lbGu_%{NG|Cx^61XJdz>qE zE1rlAHAH)P^6;yX5=;u6LH2AeC;Bu?bSL#?tG)K0e^_|iIixj(0?uiFSUQDx$wP0b zO*+%OYN~wIVIFLeJDc@-a}bFXCJXP$_jQAvN*k!zMSxmd1=W1Tp03^yJC?&kmziQ@~zkcOnwGSFV)5Vv73G zLl*csFj-+PeXC)f9d^?)F&T^ZpSKXi)rgkA+W5|!NlQse?8!BKu9@qGw~(LVSDGM2 zb8S^s)$$2%*3PrSU4@UpJy@dFwISBum3G_$r0^CI)b-)x$HVdoBi2qb@DmG_6%}`+ zV_b=`!sLO-gtuXFg*yNIFC0z*4^~|k9ll|x>$^MV>E7Y;m!$dfWR~%_C!wgOm@f}E z5yaENQZ?GQtI+1l!&0$@%inkp)5Es4e3V)9P7hC+RBJ2p@*d|;)UtMxgB@j6fGuW2 zla5%K@2i4^s~+s1QiXduJ{S4^{M-Fn;JD|Dp`>$c>e=+Xyu3JMPXeMPyPj5Rh(Y_k zK;{Pr2mKd3S-&1I?xq;&+S=R85utHJ%_y**E=u&SQoHr^`d+DjxJ99Y3wTB}IAl`v zr{_f(VghF#Ts`KA-)fF~@?_fd=`~LiS77PvzF95a@oRqOpNe4YS>S=uz4xz+p zXI}jHp^<_5O3e(G*Am$=6V5e#3b9xXhCvt8G;))gj(0|%NmB_J8mAa3MSgi`1xwIC z&-c^A=2&34GVtc2EpqV}XW77oOx7Q+e3pRgTzf(-d zjP+i6Sm`b2*pRa?3pat}LUask6mnvUl&^O+f|* zndE`@o61Xqw{)}ydRRrx6+ccWvEr%dt;&Pn7X14e7$t^p2(ceMDV@NuZU)ncciIu~ zuGDbJ7%MXXZul)4>U#0zVLXD2$|vEx_+HOdf>*ObawJeaPvZ7DY?UT|>_=dX-&O@* zET0K@`pElZY3UVONQxMooDTAy4O-cfrLY{EYVF!!K)*vhPLbqEkM3e{){gdvY3L!u9WJi|L((G#{t5M_qU?kBv!6=E=fXS*B zEO(SqxJz(V;_B6I(m>0jo|ec9*bJuFj;2*ly048dz*YogJ(k`pw`&q99;Anzm4BtD zJ=*B!n+pzL6u5el6rx`0a3~0|E_yQG6{7>VR!?{;`$UtaHy20jstzndq6oHCn8S@) z-?Nc82m+rZndA`fWV|5d_$w#D6`Wq@6)ZJz^P>N}xjhRos`?Vkju=01o;1*czx8?~ z0-=KtG5_}{Vua6WA^;{;rhKJG{Afs0{UoxNExtE3ZmK+|jf>2hT|U&IB>ME}6L=3DCbT-}DwcS(%3`R* zLT{i#kax*!k=`)iMaK*a$FsGTB!C3A`p0z5h7TT$UWeAsG`T*~FfbS?u?X>KJ0nGE z@cZz=cNaG4)1%F9d~wrt+fk44F>NgN-@-!kW{+%fVQHzZ*D{eNzs0wgn3Z^)>%*yY z$Z-r3h&1XxcyNIzki2QLq(8|snpi*x&UujJ&dxOHKUC!~6H3Kc==5yn0v!~XXPe<4 zri}49TJMOq&h)#RZ~7v{nh_n8XX|{pTz#R`@YX`z%A#+-JNpwmySaeV-2hM0ke2#` z46)lhx%JJfs2464j~hoC{SK!f=jRjGd^8OV46E#o<|kW1OfaMv?A8_y)GJC`2ORE9 zhl?U*oK)B?1A~LZiPNI`wN{$CJX?8xkwvp#eSJy;`8rZHm)y8pxF&8i}i*4}{_|D`8s&&3~iiFztAL`zG1PFQc?m6hr>UW7cG9- zt1Y9^N^%@5EN0JKhVYiCIQ!A(`r6mNq{?}hUCI?)Fu8*+y_|aMX>sa4@Z?P`6%`dq zbiN`EvqR?48cMZOXjHDZ7-61alXgR-Ve8e-bd*RzWY(VPo3^PE z&Ww4@L{kk&&{ShjDbRfhpB_|`AJesJu&_M-8?{v$GZ0}@GGTNO;xOv9KbM^UoSsuy zyvFH8i#aMu=mB$yVAK&?KeKZ0^D`HI@n$~LyJv8=L2wR>Rp*J92BGkgkx`2ldPo3! zJ`aCz4Onn)6jhogI&^oZZ9KfYS&2E%g2<4-=`V z#hn=BZFV+^|G_}!bMaIYI>?Q%h#ncdAaBEXE#JkgU_`tI*LBni&NeG6>tRm9)0By8 z2aM>cci|lW#(nUgsBrGz@yCD7Mb%fEP8&D+HAOz8<;^|I^5A27)pRr`LXu}o>}4G5 zFe(XS(ca_U^F|RcanvHo-4VVvQMxr*QUNXFlvI(N1O>JBK=A0-Rz}i}f24gEdf)X| zIp}_j8!SSwCATs^9sMW@i>8kg2xSF_&VW$?T=<;Afwiwa_1XyVUwGEsq%)3k;l+f zjgy39Dcc;!_sj=tP;K&IhLt6mUPoMnz^+5(3 zvo5PvwLEeb&(gG~`f&DJToLXZ4xxN^o6-@)!oVy(qs6OS*wnn60{4m}#A?;-M_?QZ~v;2;F;%M|VDg;rF0T7maOZO3#*uWJsPoCue#`E-Z?Rdx-GYLCf#UkC z#=YsNGz-=Y4zAqLk&*k}zYXqP(*JPh`_X9bhk^przn+wL?`te;eSP-p*RKi@i0N2# zCwaIkn#I)XoqMkmMQst@CnJ}gl5`=%cZJF(E$O@C!!dc@%F4$dwDILe^es7-Z{QihRxOVo{uQ)Z1#e`nJIT4gHEly%tvB|Zh zzXhD_i-Lj);a2u6xj4OPfQq&t$^HIFY;oIPoIFIyxcqT+srTmNzT{{%Ce?RmE=ckf z*Kb5VVSS$8P-241Ew${B8OYNr7wo(|SEYO@OV>Z#JU2UG!d|c=CxPU+XOaab?QmIJ zD*sufbAA8OXqn(o&f7T&&SUO4G85eL@2^}iDG0O8AB*ZPzxhtV1w4gdEqau?uQ>BO zctiKnCe9fyP6C^{LFrOAaqfsGH}th*9Y3gja&g!wWNo+U)WoF0r;-DlgN#rx(kx0_ zYtxsdh>cT>?YO2VmYM@DPVB1NLhtmo^epZ5A~v zb$EuD8I+f7)cKH>ivVN4?%|qFa=lDE$a>x1%p7dvD*<5Z4oGy{-H@J3&|jhvu4#5- z%b)e_wEbiG_*xq{OH!;UdA%+PEvLo$}AqPNATNv#u~{o9IH1?5f0%&d!$3em?$7 zz^J;?#jRs((|C3hnj#UK2}8lVpkBIrM0l*YmVTVx#_^oIvk8kx1_p_E_pk03QE%rulP?4uxTMv`KrP1@iJZMvQmi} zx%JF9XvssIaVDN*ZYIfyx-zAhR(z7M$$9YIjOQin9YOKhGT$>~Jsqhbji(|@v-5LNtygPdWj%GOjUBwwQ5-(gX6SwZ6y(%e* z^-2j73cc7_WmagyYBI|M2)HGFHwvk6!b}d^moY7hyH1lmhzKP zFXoiLvu_VwKq%+t<|@F^wYNtwcGk(*j0L>KU};H-327OH{s*Rlf=GFx24(mu8wJm; z6!T2io@*Y!LgxqaSZOz8Apw8jGJs04XcA|l=D!rfc1_RJdoJ+1T-RD<*HcjKb;@8jJbZ{r0)I-OTYi7M=Sj;d+PuksM;`NEJ_EuY z-p=eWZMLCAyd%EJP#DxeMudoVjv5#<3T^!}BVB`=!PMH;GYT>9O1aYWr`A>9)OUI* zDikFr5^hDP2x$I@ccQSw=qWx*^7Hda?^LYI97xy6+I4sK^*0Q!atTq{i zzN!^+Ni&Jtpwct@eauXApBm@#tyHluuk<$Na~h{OIXYzs4qn5sHD09s)Xq*IEpPZ^ zW_CeAiR}Q-7YVI}Sl{0-N|y+f4u=YCV`ta4zd0X%Q1;2z)m2bPxhGw=R3yyEz~BgPc3J>ZNbTPe(E{R@-k?OX^wyvxid8tN*#Y^oDL(cg zFzm$xd@;Slczx=`?u)rA{vljV#OKN!F7W7NusJ$7obA;S4~n2PF!(|+$>U|GfR6%^t*650l;K5B^8Z1@3`e;q zOq!P0r$R5E6L7GYuZ-K~2=IVOV`jYk&$NkxIG`FNHSsLo^HC?8StmFc(Q@NS)8}fD zS-k+Po`bvtSS-<&Sg25L4p)qHYpGzTU0?HUK2psjK`Peg7;nQ-S$KBL$Z(JV8Xau` z<2m%HfgkkqpXVlIJTD$llkOk$oEW=i^@FL?-Q?$&{cfp=JM&$sjQ><-UvcGbU6*Jv z6d`<64Nd}<6lB4=F%`}^`1q3#tEgcMLf}~if~BBb@+Z!1oWlkuykZtK zL=_ema*N2O)D=aMkkh`660Z9uro}j*`EG4*Cf2h@e%H9ug-u758U3!d*63)KSAnXw zsPl91wj$;Fi~qmG9`~{Nqn?+=2uQ@<0b3=lav0^QUlNExLJ7?x_qj=pB<>y%51=1Ji8In(-!S1d zAnVOrmZtMQx*ghL{*qbfJ`>2&j$$h*7KdHZK@#~TmH;iVy%L;}dVC4k)!BQ-=o66c z$T@9Ur16Y_DkUN^GSacB$Azta_{l|~l zqmG}2EJmwvrs4~AF~HzmCzq=yn4i7aVZmgnz;bELWifI}A-8A$tjLN;hu^-ig9312? z8Iig8I#Gv_ieDDt?0?CW`>}mEI2$3OY8{ zU0Is{LfSQS1nkH^Ctr?68{@+4iW?+Z>B7-+Omij=+{Zme9jJ;K+v|LbRHK+LDZfK` zo7C}y23A!`)-UG0CT=k=$?=UuQYN#yo)T)4+}LBf$#9c~Ijhd3bA$?adOMIB=WaXQ z9x>?IxXI2tfLW4%)C^e1l&f}0spDSlo!5T8w>>Wh2$_SCN;^$I_1D$YkN!(c`+b^7 z_LKmzF&RSNPC_qdPM}S$6ybx5@Di?nZ;2;rYCAe`D|y{IwKt9 zz|RM?WxF}VAJblo87OYJgKd4M65fBbzZsuo0I}KHCB)NXv%GNiPzz_n~U(u?(UUuO zR%ZY*fb1^33>VC;HDL4o)unVRD=Q;`X9y*ruXF+q^u<$eJ>*dt$+L|#Dt{a%Dm)FZNdLTs`O6I5ioN+^*e= zab0pkC@I+8UJ|@O))=>KY++!KtMytkLXE>47N^Z#5D-TX7ZjZIgVeSHBrVq}0fWd5 z*@N0!5`&drYbFVr&kQi#txKhVdf)42&t|DcF__YLn;9jA0aU3T0~K5yaB`0RL3eg= zee8fTl7gvdZBGT_)FlqtgAOFhz|FyJ!LX2!kQe?_HM!WLu1pLG&eYcd z%SLBeEftU8ehWZXeb` zHDWvI(jbOzS?J3i10|S1YDdr{mysw+N?u&&;fe3>`4#8FLbf^a4_lUViJPu94BAZ7L&H53riuAoI6MdwfIX z`!N+-X+Xkj;a#{4aZunc{|Iv1z(BS7&r{8k^74V|qhV}r{R9Yd5+FhFAEtz;npC!u z?_5?*HubR`23eQEh$XF znJQu6D`W4}K{hkK9JLRK_)S$`-$Q0-@7pSyU*#yuWAb=+;+~jvS{ybGd7}{E|6?_a zw)gj8r6A2u*;P3^5IWDDvHe&ODN?@{d;-F~-50$#5exA%q?6W1G!+)!LbvBRkIppmsvYQ{zaP0WnB zfx+4IfRp{ZJ|Yf3@vk4;zu&&KIM9}4F!V7Bv9XglFZ4aW*U%sE1J6Q`n~)ciT#tDV zFHBlj)JZ7h0MV_i%k-08NJJ!gk;=?_*N+QZQ$#}`aywp}n|WMDlA32*V%5n!2k1Ev z+eCh097qE@j_F!7E`U`Ez|T++wd1$jluG~ctlzj|SyO4pMyk#bEb;E0J1=qa6|QH{ zx`0!z_j1uQF!Ni(ueDG-K)gODl)cqnt({AH5DbVP39b0ZtgSswN({xB z^Ron~PSwcWs4PDsgGoW@cq^J+cJQq7%j8vkU8WECI(sLl@JpJgcdW_(Uki}*NFr@g z3e)a^^%+`adodS)Z;s?Z^LKZ5H(|2$F(DTE3V^-}0z2^jPJnSj{`&0o+Hc0rvWtlt z-q$bDyu}|T>L~+wWRBk!!QMBxch9qO;u@>Kd-CA3#s9VWYW~+n)6k=Z8Xr7(&}o;P zx=QF2h1J#7#iG_@l+AEm)bR;Vud!Gqi2&TJe`hh-;(4fZ*-_w{IO<}MsP*LoSY)(Sg%Y^W`EZY%%XiW!P#jEvjV1X1M;~H8a{EwJ< zbKqS}JN0BlgdhcK-L^?wbjJ$x>Bb{&WH*rove4KLpzj*K@016dJze%>{$GPgbF|ti zQyYFjXdW?Q{U46*=ry^DR`G%lnvMjV9?L@JsT0=>*D=@#r25P`v79f2CKuI2ho!d= zbCkR$JRyw83mTb@I#iuTxx2f^`<-bsl&b0I+DMy`PUgby+8btCByD!v2=xS1ZU9v3=wkCNE3PEcB>D4Zo+1E-$FUa zt_lguQYvxqJ%>L20^rQNp~!Yb9ME-LG%GQ`_00`MNxAAi4hto;8@IQ&n^=^Om>^@&5IMR5 zOF~C{=cBLBgToR7G~?MfhJriXQ2ml8T~ZT*grY<$tE!4NWoax^r>7L}zPx|`{$*oa z!v%;3f>u9tdxXPVp{R$#^cLmH0DI0Be*OBjB#Tdnu@5xpE%q^E`s0&zD`eoMAPcuB zlmhm0w$QCj_AJ_r3bL49B1yaUiVupoOPP358i=)O&0z>J_{ zBn!H?hGxzpZP|cwO47)^9Ewsl_}zj?qtFJh1Y&a;{hLHlk@6j595ob-z&-}_#7EYQ zesBVGG2+`<uAC@Ha5fhi4&0mCK1wUh4fY7ai)^{ZWVe^~;S9dz}m-A@qpR z;#!*}|HHsnKLPh;N82Wvlc21dP>gwqAWYcv#E?Gf3MmANJCtrvQI*}j^r7Ez@r$Pn zZddJuA!#IqJBtZ>od|l3?u*mAulEBm9k&K5WMpL0g62cF5E@shD#x+=pYR8j$_O9& z3|XKO^i~Pvppe_A#HF7&7zUj~Y#RIsn?(jiLu*&CU)@cbD$B~A`UW&UF12xVysoEF z%Ld&bYTDN%W9Zw|`|$u|wtE>978|gj=jcaZjyL>rIag%@7)npe1vq8`VNuVgU z0ItB#&)&0aI>gIDy;RUo%tIRVr+WsK?#+Qjr|P@yngme@l`>+7EKrVz=G5e>y*-IV zzHZ7}K@bYK3&m)aUtmFjCGwsc`bwgRf;_hUiH?qYBNuVWkU2wNII?#cI!Z^blLpeK zy?(9f?^eQnp~&r>L6y@42k7RK1P1S5K}A(3&Ll44Z2_+TXvCFj4#T!`sN)PWB@e-$At+XD;fMldz$Im4VRie8Ds| z6qFD%bi@cHVW;gtgcQnFi$&pg zs?XK}aJ3(p?7Tc4V#rL#>83B_*6qo^<3Wqk)!gehH-~^qp zhLj8@BZh*HADeRde*7ewgs_+AfL`HZ5b`?0M%C4IQyjXTP`(zm!dcVM&>)$5D}w@J zftp1Z()OB9xNgFyH`>*56Jmeka2-c<2{b?_;075IL%IN&x-io`P!0e2=#%kP!CDku zI9~Z(FE=*r1)>r(S}Nj7G^x)&XQ_-(%&dE5PXrz4_c8tTk~0;~eM1YCqpwh35^Wz) zqlC1ek~_Ky;ZL4CiS#!t0XLRa@J|I=)0)I*-@XXd#~|~Gt@lsVx%KFvc(e;01P2xm zWL$(Q82XCYI*i6fM(XTr?D*vH{V%4(y9Q^0M=Yhan9XGq z2!$H5Mc?4ul+}?0OzSPhcHpy@-5jF7m#Gmq`|ABT1kuNT#*?*}It%^8-kz2QBTNza z@wi71G$V8!9G1mk!5`ghh`Rk~)u;OgzpWr=v9-|GZ@992K|PmDAY;0L;v&mg;{2RP z7pFRmhs^K0y1G)#?)%EE(zwcTD8wU0n5+=5nhwszsb%_5R8-V)hM6GqYZxAYCPf>y zUYRWFrYc;VJ_TB_lOAvjOc8L8VtXc0b5Jwm=Vx&lpG(M~q%Sk;SN-P~wa1z3fEbD> z$tqn7j;amnx*&}0DR($r1fspO4nOm9b8}l=-mEWr(_&5(Q_iwOB_&l=9+eYHWgh_C z2NvP2Nt&?>GU~x|06He6d*%QNoIaOZlApH6v05c_TZYsM-Bb%v|Qyo9E2fI<`}y^QmJyf{w|0JyNtYso({{Y3nH;z`e@A^=wch zJ!H_V5_fLcL34j6N8$ae-TXIl%OII}SP)01Ae*E+){SLJ+i>I60FLw_QZKZagpM)my6};-OkTQ}8 zZ#kE;%u2|EROOXva0gD!LJ`{2l;g;xG_f8#6c(W-++GY)L=mV0k}#h+c5Gd4QPE1Q zKgIVj!g{fFij-H*)$Lf$UlK7(z-E0?MnLB?_Sf&*5N}&=6-2o&-+Zu0> zzYIL4X7Nj1xUU z7B~-*)6MH@TbiQhqlJkBzc(`yM^S@-c3mj6rvAeR{`uW>SJRXTbvs8#Qx(`+aebFh z<19lTD3(kNA2#IwM8`pp<>Aiq)gpXPaM8_Zu6NE+6%h!hI=|UH19u~AoQ*%=8$N+v z%-JKfZ$pkMHtsj##p5h{x9}x$GArE!+--31O{250*%F{k>FC*{*Oaga!n+Ak7tlAM zy?A_`KKYsRcV5ZQk6o3J2$j&1ZFYI1tBe$5rqPfnPeV{d;AuU}@sn9kCmJ^R7OJlt zu3(48``nM)3JMCExU#nR&Bj?JY;J$6TBd*|y(TlB>T1R^I{=AoBzUvJ+&_2MyD8P6 z>uao=fG091UgPc33sOe%J(h;9*ljKJxpLXUfX(I*2R0tQi8`tA ztUZ*A+1k8`eTwQM50u0ns8jYWNcXq{PEzonYwck(awEHw$QMtWpmkvGt;r;b)&-%d@IfH~9@9R!Yd4 z3#6*V&|4MUP7I|<^jDT~{l)F_&;GUWBg{i;NCv%r4LhO%$DpP`5N#Sz6XMD4vtcZ5 zSWZGm-^jd_bvo@)Sb;*LbT2?}a3EE=QDr8AXSwIIcW@7OV>au`r9o$Un(V59wBEZk zFthYlPIz&EDm+umE}*qYW$_><;a+t`Mg5y@+>d``H{kn%%)Yc}9f)Ix-<=XF_Vn<` z=$5y>YhD4wb{8ONvKn>)cXc?r$|19FaBxSr%9PuVkp-wfltDs31W7aRJ$!7EJ)#!Y zHgy$lfBn5g84Y5{t_ltoaqM6vQUr?7PEa-{RCYfq(@VJkKDS6El)t%TSEYGyU%K+c z2iv~EPY-IP%b&4;Dyd0bBx0KD!Z`@aS$*_utgP@e^j7dfao?({EAaHRJX=px-+ps> z@$kJk)HHFMD9=87Xz2>j4zKrCdIOE(4anJ%o5ARe(NJXd1kEM^8F;f?{^&m*yA7GU zfD}?1$o#5!L3YHOUp+jVAvT)5#of#Z0EYhHxb|@Kdw)KB%nT79@7EXFT#B%H0+hm< zpzgCz2B*P}^&v&?D^mXr(}I%$vh0oyRvZt090O4B5&@!X2)&My+{7XXjb*9f-IW2+ zK^hr!_k31Yy?XH>4&`#w$jGRoDWlqq8gwgP3%Hw{3PH3Kd-YL2Q&>QDr(*mjbt0?J z#z^A4pLB!CWlQ-H2iHuH!91%0UQnPw_@Jwi(J(!Fz2?MA`0lI5C}d(2NTnR$EqQ_z z7?h)aCIc0myB&<%wW1vlO9xQvyv^bzMiYO|$9DYuPpSo=k(apnliX0pK_F!QSmJy- zsbAqIzdCBb8BnC;?aN^~dXwiZXrVylx`)Z`EC_~IffhM22CU$+)<6bHU9RR1yosFa$1NC@oJ53MAQw=m&LvxS2F%i|F*? zTiL)`67;kuBTNew%+I5RY@R$xoAl(kdp@gg!4@i^gkZDS-;Zb~H^2X!h`y1{q9kHx zU(E4qXo!_3%g&1tJvf2VB?`<0l`l}A(FpippuKaD4WadMN&evJ+*_rP0^iNOaRXP3 zFmG0f&!*v5pJ3#0q{@##Be+DtSn;4$fpS6h*hbn)2VYeuj?DF5Eq(ogVWg%&O602{ z50J9GFD^c9Ag~UAy2wZOIqltL>lEY8G6EHE?Bk8M<69{myVF-*Slyy@i2+T!zO;7L z)H#T#x#E@-$M^T5Qdh5P!R-TLkzWi~)5nK~_S~CPN~ReiU^5}DK-+NN z%(QXHHP%SO`!WRkut$}Se_&Bwi!m=WBP9+RK)Hm1mD^&nP>>gg!_F_u@mg2wc8TtL zh|TTd_$z~&a78o{^Rc)%7VsTsi$KPkrg(9U0a}{zOj3UN9TW8I^LYGn5uT&k?{Mdn zyx+TjbHfY~?et+NGW`TJ+GFJp6`asVrUKvA)s6e|=$Hl#`jEj6qZyp#cxydV(~SSV zM?u_t3#w@W%3bGk&t1@1x*2`jhCYdIeczN@JeW9jXO|VD$}B!G2CxsAyYGg{pKya> z;Tm)#fF=;5FZX{vn^M_(GSQ63h;*5qezDR~m~te7p31@az4k^Vduw0U9{Kns$R`+Ant# zmfVEaP7w$Nh}x19(2b_XZ20PnV@Ut_A1=4VUO!M@2XIm(@HK`Vf3g;?aO)=j@d>k- z0axgxUxiUIC{w5)>O9veUO0z{gnSaHKiv5*)fIilnZ?XZev9M!0?745Vtf4~6nP+x z&q0>sM;(2A{dCmy59LuGMzk&^(LFOi_kN%Q+tom!-Ap>dXjsT|6B3GFkI-Ojz!6MG z@MiQj#89Hp+zeDH^1dFCNJ;MwnYLMof}_2CA;Fde0h_3cX}5=NANA-_0`V-re%?Z2 zVq&Z2=%O^8v0I;NU4od%Xy>z690oOl=c5NbciqRBfg(K?p?>Pkgq~`l;1tZ)$#lG* zG`AnD%M=Erq3m-~P5aSt{uxVQOJp!N&fNkiXR-1`Ca{^Y!={lxMqloA4^Fe!FAVpnAWl%k;%g zQf8^7?Tzg~9-yk3mtKQ@2IZ9=0GK7HzTKmq+kD2jTt*!^-H$~@J@3;g$wQ?5kGzE9 zwQo~#!JuSqY4B??rEvwNcs0t!@b1I8m-sq@sxue(Ld3Z(|I$yNwEj4^ox$*1jc=u$ zRec9ziDKnn&4bk|R5LFOp-WuS6o1%+>na(mlxk1n5t zRtP7sl;`-mbKPay+zg^%0qp3_yoN4U6}u--c$Sw%b`4XczyuL69{Rcu3t_Vh*|8qQ zptbmjr!&&cBtJjX|9-%7C2r$yW5SHI^ZFN-@X^sxc^1PC7n_z~vhUe3GC#VX)*Dk1 zHCMvFmhE-FLW*kb!@XCho_jm*O@#}9+R(sP-xvgIZf(%ZiKZ#%jYK^pU&{TuQYuP8 z97Ks`Px)0uyB~W;uhlP*o)(=m}7?o#3%QFLovKJG?^jm3LnKDJn$@B^CD;LMqs8iFYanNm3QV0)3 z$K@ncJe~WYD801Yc3Om4y5LxYv0}YZDs&>ksLa$&p_as6gpc9Ij^7x%a9+}$mBVkV zuV6;H!H1at5}R`~(L>O{3AVP`eSx^~&ke~Rk8#kHCp`5i6=LSa%Kdwv{U;M>gl{g? z?XN7h5_)aC8D7e#pcnkiNbCkYSto4lvzTiIx_Yghesd;aM`?CNH`zUgEF3Wo+|%fr z`~gdzlt~kJ3>wOYMoR6`$^>NvExa@kz6T71pp!q-`Om{rfZQaTRvAh4J@{PA^cqx0 z@sN2_Vg5#tZJO6g?fiWplokv5vi82+8rH zumkjT|KY=R!Mg8@0j zpv-o%n4h_^WWQs@7b?vZS;LMOfl0v%r5#2j+R6&ASF<3LdA9ukJHLeubHQ>pfJ3$V zd^*f>{}7aE;gET(RCd(O%ZaT(ymY^=C>}S(jBN$~69udt2S`Gr38uTcOo01n2QqTI zmbcXgb}$T{@76-b!Za$lB#+G^Ar4a1)zW)(jHQIs4%o9$l$eE@nHYUA7-p9HIZxE@ zGP`Zs-`wx*2M0cL@3Uj{97g~C`%{iV_QEXd!A+Rp8rUM;SbaSaN|4HJ5fmnmo*IS$ zaoXeV^D}eFW`kv{^iUo1l1{ozr66b?36~)1^w;;yt-&l7^mI~Mnx)mdU_m3Y2i>Hd>H_4p(Z5q8N-0?%2ZI8tkl>2fvBUEj~?wR9M)fU z7^}NV3FZHlOVnZOXaVzzo#H*xZDkIl1A-24C=gQI2h$TETz7rFeFhzr#Rptc1JJQV z4TVNG;4+8TJ#w43e1E;yEu*xySUsb`AE5W%H=w*w5z}rE=CKyT39uM!{t6Ib8EDkv zZf1QR$pP3l;&ZY;uZ4kQHMQA0-T`dCcMY`VHy?-OJxe^P<)r!S5W2bl#g6qW5hDE~ z+_j!IKpSKT7Cl@`T$rHlEgii5^+@4$Ru# z<3Wit$$~}<&j3y)oETqMh0Tqa2_4)&!^4a2%#ub)s!=Q=+M#V0e7WOX?m}FYKuiI8 zW=Wi=E0;t0c4gHt(vyKC?du(!#i@D2V6&{`^#}hxYU}GWR#aCHWXE2AvR%=q>+{oI zZsvu!gb6N4ifu8D7|QlM{y86JMCn4=JU_3RM2mBG0k7S7A>IQNBRg9ST-b!f#1?{} zYwHnm-OTMjY2fBhg<_yaH1Znr7$_4=5w!?*2XQ>9A0-L7nS5L<&y58=sLl&&$^GF2 zNG<-wT9QfK7;5MV9`&e(3fglB-AsRKZVx`H=L&PoK8yldDj~Ko02Q2MY4YbWd3m1- zP%6aqw)Mq4w*a?voiO=YEb%-&J>6~8p*mMW3txJS!Ak#4v8-?ci_P%h@ z(6}DhRJ?MUB!YnWtLwFt2 z>~hHlg9i{V6#&$$`)dKe1XEf<`Q_z|JA)Rt4bDp04^8q#PFuC1r5E0YBmh*=Vb5St zg(YMdB4`LWVym1*;m1dUaYnR%6>v3iBGa}f4~-jfXFWg0_s># zJwPuoeREA#{>|7-QpS`sYpm>wba$EHiPQ)UELiOzdodIhjMNLI31I-&Qei(_29v7{ zAc)syw-iM^+fxhH=k=#2hta^RfAXsFM=H|;0jf1%{%H<1Ls4m{tl9aS377eEgOO+b zhEgD>Y@=5&u5`FicX&G%85$Z=x5E`JdoUL!Fid#OoS6r*9M2tzKMncCO@Gtuh(To| z?`O-e?`Fb64AI?yIfxG!fMOCzuj_nUsd$G>6{f%|2_rZ=v++9r#`tn{ssW^ zvsc1t-XbOMW`4RynqNCwZ4I@}-$oIYXe!ynB(;_eDO-duBpCm0!MNx0~)vkWvt5Lr3gdq>LSq_Vjn?HE9$$mlwg}C7urApy0n-Y z(d44O+{W2*Spx$DFOcz*`;4%^MAUVe3_p8(`h@q1Jri9}us5%8I(!q%w#kS9-Pl_~ zlBdbdV4S*3i`E4Wcr`woUQ^4@PoA*y^m9xq_V)I7R_Zr1fRM1j;Nuq1s;ZI2 zicg`@E1)^k11i-ul!p{5&gpvsA12iF$Ll3GH4Unnj#wORTXV(%OIqGk=E)W>FGsAe zqn+LdrvDJP%#9j7)l5QvM=kpNmkTjV=~$GDs9CSC^{6C=_iWq>L5C_GGqZT*xkYc< zRl662mtwF($2f)4eI2Xy=`TTw7bh3N$omCCG0o=EjxNGA)N7uduoMK{9tHn^C6E|z zfNLDAe?txQ@e~|h&0VwmVW>5b8yV^S;GkeeqXjNscJ6s|jR@EMh+?+?=fDA_z%ar%L9HMt~(JiI_PO#u`*%MVJpro-~t%uPS4lxEu)+hz1*A>_~$W zQIlTa6pS0sJ(JTvR!MNLr}J5nq$u{bs9 zy_e{7j||Q+x9Fk(lS0~tpqMX32x4i)z9A`rK_y|&bLv8zYtH~EI6l zOFs9`x=JWq;jw(b!DD$O>#~F7>n}jE7z`$b2 z6YGPJzcFg(>!M#Se8|tB{sU?%3+%&OrU{_8eFq91tmrd={+LE`bm@%rv&GzGI!Q@M zz0zIPL*tK&P;1K16uR3H8!TI)AuZ-DjH-Yib_O^oW0^Vvh68HmGko9V<+VyVii!L9 z`g((Mv}!ADA{P0VFnFK&7Ly{pCKr2sr!>|Zw4OfCOKa<@mK7Dz5wz4^fGPzty?(-| z4qbSgs^8CBJSx7+mC4lrK78-nEef&Z*KiiSzgD(}KTbIZ1c@Kdk7EtgWzzqKbNUbQ zKkma-*^lORaQJqC-)JHvog+P6^!Kfm5t*!ve_NMQZo(@ea#fJABnv%|?EUk{cq_6M zOimJXjn4Dwp5$uc_5*{1dC4MX*F}}?X&exvzhC=)a*5t*)Y&8%U|vmgbF*U0^Kirq zNM5y_oK{ns%mM-wCfso*+Wz=EN&tb&0N2aH=dm1PhKl?2=;NLBiorW(X0f@N$&dRN z-AXfW5T?Z@K-{ums+GQ!u08X{wdXtF@q$&xr#T;Sek}fOr$8*eIr}}{<(IA+8O<3B zdQaAs#zRMIIo~UZdKC}9PzCMH>~6@>^QOD$0mTvX8__C38W4;R z67QMz&7_KPVF5PC)`JPj;uQQv6WkZ!&r4a4yVEWmx=%zgZAjgUm`0%mJ6naS|A_VW z_h&_=a-I|JQJn;o%OXL*Eq~TYfDg~Qh)NO1Kr(B;ijfI6b9l0AhiqWhg`GeV78=Zz>mY z10npL^psuvM0E+2?ip7fp1Jo*AdOSCO zp0^cbs8)h@`}$YE-uHK>U3z`^cY)rTp063U`Gl#3BfOpBD@hyUnXLA^pJENp+l zBH*JS#QU0Q9Uf#E-`c2)qiRLMC!!C>V8jL_^v9be@;V(Jpp#Nr zPON7)*dwir(Q>zelKgVRjxa?%j)qK;oHRxnkG1@MEEAs}_Rri4O3!GoMr1rl@HbS& zrXYz+NqzBxsV>{+E0GaO_WIWCrSAUXS*2-TRm;%7?X#eo%E(_@Mw7@4wKL#9#KWnO}mH76lP%Uqm3UxxC-Rk4oI>*9`T& zIVXB$r=IkI@lfQz)g&;$n(k{8SsRlzZ_Zv$8mF=0V08Ks~;s z1a@5uo;|t%2rY$MJ&>O*)(5N~7&?4kn^fBn%p^y6~Ux=7=V^1saoRa(8(Vx+>R5t zv!;W9FI@8+63CVL!RqBt-yI0_sBa$9C6>L*aho>};5)O1o231AF1qEBJw^hfgQh5? zIe=!_R#sQJKgFmgMuEvdRS(Wj3xm0|qCIr;CXFWSGg>$wx|g=Lj+)gU%W|M&TxNP@Dup@A5h@ zuvt_RFQ@9dZLZ9^&VDQr(UP4L1@?db<=~k}f@;=jW2I_xCHeRq8(0>l8RZu=cDq#j z%#}Z+hrl(REilW^%R3GgrR?W{0pnD5L7^9Z{AVlOm2>t#;?~T`K`BMcbkF(oJ5|mK zzq;$7FnnO#Yw^6g>Ho4`jOzxQeYFwSZ9JU6v0#ZYGUJ6W*RbmdC}%&%dH$D~|qhPp?E-8f}Nx zdo|{i8aMYAahS+lAG2u7B$1Lk9aaFO;WyIbZt;6c{{dVt_}14uwEo0D%O*WNvN2Ku z>@~UC)e{Y6E;YaxCb5Z7^h%7nFKuO-F9JwW^&?43^BskJEiG0Ew+@mtwojWnj953q zH3mc((Fe7JG)bRLq28;JkND*!g$^pLdp}h66K5QeknA436h$BEM^`(v3%&IG0sC8+<(P>GLG!Ev5ws+wXztxlZaY}uwP{O4rRY%4)2u&O`2GG2<;yIP@GJT)4)xnRpudOwd3x+D?vft< zD4g;&K!c>plAe33Hhnq>IJBnA)4l8i-ghk2x7kIR{e-a7u3@*V26FFujMN$EAxPcu zuOk-oJr-Aa+bLI|hPnUvak55j85`y~fas&vdz)tL=}((-l-)ny7XmCZ*JEV=G71I6 z&_^~eZ=I44R5ugJBv}jWYAx_gLFCr*Y7kHiM1h`Q#%{eZtvr=iPE+sZS@i z7Y5$3Lj^UYP*18ndz<$pY-VKvZR3oZni?(JnXHNI7nCP}61@TqepwRxKH4iFnysm= zt^FR74gwLVka`v~YV0(3Z*yK`?8%bX;ohB@JB6#6Zyrk64qVviHXtdK6cBeUMz3h! zWkv0rOznKPUH92En^d4AV25TAYI`~szBT;Rvc_g1la$>3^HJ!=lKW9MTd#QHBfIuE z%ZP{wJUfbV*I(^DRP^<3Gr5aq!NI{PqI>O)h62}iCzHD(bfd?Tr-LGLXZ$~$0sf3r%9^j>!Q+!u*(2{ zc~gt)c-d31Rj)ulgOe#*s#SuWIa=Ev&H)7At?=shDD~!_yea@k(3c8tq?K65Pa_NF zu6wOMn^5KMgijt2(>6vE!bN}u=dKo8rhoF}NiUkRxlfy5-W7j{0ZbgY;%0U@QEMwJ zi(uq5YuYa&jk^uPve6I#A0T@~YtaZp4eImuu*gsV^S?Uj!`QZby%JORt%WA_n>A_8 zg)7OodDVlG;p6- zn;@jQ@IF_y-u2MUpJUlgo(pXrYjW+BUKD=p-);^Ex6O;?7IL(f{`yyGTpC8eP6iH^ z<%VHW90_O2vz(X-G1EaU&{wlO>IFr#o+Jpa$spfC2r#*OQ+n16^Ef3mtsxtkO^DLM%HHn&4tm+)|Qpmy&PjZD<* z;%zX*tDaAf#-n*66Si{!IaRw9ef6ORqDA}Y+x1^`*6lz50Dj(YqmqMmca@<7sg=bh zQlS$VxQ4~8+?wTYw<9f9&Q?z)P)HAa^A$#Bky2;5uB zY`Y;*r^3#{G7e}2tAd}2EUo=iqOsI!4*TYUdT7f>i?Ia0&&(38I^o1WVbZ+OpHaQb zyUZp7i2#-Nfu4$2VoD8X<2*4i_o>paSYnbpo;X7-^Wv;bYSLbTBDj?8Jjd@($-H)hqlqvSL3ivq1=VJk;^LoCTu?12=lb=Pk9w-!S^mRzQ zUECqcPv)B&KrEbMXGiNN;t=dbFmzTIO^Mk2|Yi53aOPB1aia9MAZG(A-dl0yC zjDyizNr3heP4k@@wtf-Zr{)~e*y;W3E>PNQ0su-2MYTq6)O3*LjG=PQPk_cXE?)>c z)?K$)EVLuh-``&xzi}91EFB#kRbGb4tX$WT-a@$CJK};<-~RX;e`jX?N8%(M)RD9P ze9@+7U8|{U#jc5-@hi}sNmMve`wRs!yO z*`E{w0cxt`HA8m$sXdUNTDL1Z*?U!%wY>yaH$gXd_fXnX{@_9)ua}3g0jsDk(EP4B zu#OroNi@U}uVeFmM%^u$zStl_%o)Te~Oc#p%+l(x{7f#P9+@PAm$3%#?fmpB2a z%z)toiTJ;B(6yrVJ{fbIt?V35aZZa|krB?b@i9n8UiYv>9fmG)Fqkz`)}C_oP1cS4 zp<41aM~F(S<;HWzk%$v{I_u0RR@Z8?LtB4!4^07_oq!xCOvQzL;UY(qPL4G0iSi}1 zx~j#O30TZ8U$RLwBp!G`+V}_w<+otl)04$&Z6Lq;cHsU@h1?sJYEKuJjS|@8js5`v zCz(&3vBjBVmgkJ6$nJ^-*tsYZkoOobX3*i)Wx6+Ch|b^UW$#@+uFd_+jmIPj(NOtf z$QNLnc&2!db==jQQC#n6MAkcYVA=n{=IIWU{`vE#iN=!V{u4jBXx@@g^?yH%rIhCS znL7Nr&%>T%#jSj~qG?9dPBZd~c|CDNf0}~~-KSBe`!hjWB4?tAqdkOrExzvQ#BvNo z@Mi_2h}%q~*M%!GIw)p_`5I40Alod{YxHh`t)MnWzK}>HZFE4sw|DJ$Yn*k5X#e=L z94RP)#U=g@=6l%6H)qA*rasK86V1<_nYmTBvCcXW52Oga3fE{Gchg^}^!Zj%P2S~0 zhP%*j6JiAA)rm8022QBmlc*3wk3wwD{bTK@k0gb9n_iu=ZB)+HxhF5eZP zS-kcg#Ntl8Wr*CGX$wzFA5QATr*z@)%Q zEtLbg1;?;J&c4&sIw{jS%m6W(^$*_L-QAtUg?>9QD^CY4(Gg)(80RU|xH<{3J;6@Q zGPhBh?tY(!K{I*3+F9YErJwP2#bnKhqBsT2&FI4jQ2$;d7a1^^WNWhlu~=eHT96~& z1;)3)b}GW25M;v@4WNV5^?(3_8RM_Fc?byj=OxK6|EOy-552do*)|50)_2hoMQGp* zounP_Mtn=_iCt;6K8Do4S#Q35K#l?;k$^O#X^y_<_u6f~@0^odbt@S#LZ@(M;@V;0 z*cIx(dWtv35@)XUF83h!@)qSZ`>egMI5{~*IhwaQOc@sLV@HZMCkrqIc2lV!T5xRE zF^<}V;NXb!S3$b{d2O$?3WR*At2+AkMvK_*E9XNRD3+P$#KP09CnR)6cF$0@ozQB~ zUg^52Sd={ghOyhXh*PJ($hV0ScD2xI;w1fyDgkL1!wllb!b>y$gz(2{V`q(FL3l`| zrxeLl;MI6p@_Q(2@4g(~>uOsu%xspRbIhiLaO2l7xJE}z`>}N=u3kRpeR$pT=O;bX z&K;fq^~?Q|o|0sVOovZ)no&Jb;v9}A@;So)lCp~9a+nfE4deC^!h7kIQ7!~}>@(aX zrd|gZg$U9tYG6b3B#&iwdjKCQY-eYuxwW;`P%xW`3HaZ!q>bV_(E;lv-KUwPq=mV; zO~3Ls>)}x=$mKnnH&2asAXC~gQQNn&HkuNDgyQ3t7~;V?m9wuE`EtjZ9vh47Lk6aM zZAl--`nM;Jv)Ev)Tk1EW*2e6nw1BvpBfhltDa%{z;Kt#|V&UIe)*O)!$%C2HxxHHB zbwyb~K;ZpLn?*C^U%JjY*Q??07WTlQ*as$fHO%lXsl2}4FR;5Vn}&bi{b(W&X4wHb zz5OGw&)8mTX=@sB-@m9{9iS(<%22(U0fNedsEys=H&|w2xN9Y; z6idp{cRIq1o*S~*`_}k-9->MCIG)M-fwDMcBfm)~da06pD4Pwf7n8Md9<5fMt9uyJ z(0hZr?;1U8j z(XBH_l56HizYY#I_W5IU=77fBj$EYUP{`HQzaOf8z1Gp(TJBRunSi`LLc#zM2@@ijdV z!4uk6iy$5+iWL>mFy{22-+I1@=3>KYRgg8GJ$p9tjx>9QSp*Auuk}#KPB2WLj>Gc4XhfMlU2nR$eu$$2V%R-<(C2$>5jN4Ka29CDX_Qdc>UU_~MeIPfMHO^AVx zqzwvW4yMS**Q?OMWp{Aju}BTw2gpUd&$mA%n1K(awdFZ5)SCj$*I8PX7}6ezIA`Eg zfr&x}6_!_Ft?oSoHx~~jfinKmi+Rm$j^@*xeK#Q@^x03TV68EIhm7v(>T(f~ncu(j zGCCbR=^kmhMle1y<%rZm_1y{QZN4a;YLGuID^gU;+2`LMRDsak$RS0192p6j22;Cj z?{qsIpac3MvE39)lOW~P&fKp?Q z7%_t)9>qyfUQ>;l^~&SPgX-P49(AS2QM@?&0$~l%l`f|l zfofw_!>iS3?g(-{W$pX=jWy->7hMXppBbW zs9(uv?QBaPGt#pHYJh20&t<*8H$nwbVQepG&*@kgQGQ94n5rf&3#tEZxEwpHQIaO~ zWmh@U{NB810`H%rA^B_8FwETs)5FPe3wNjvsex*Ve!P)K^qu}nmkcB|x)1x_kUiu` zs4)~z9TO{hhVZjWb{##7Q^v|M?W65ImfE?T^^`D_{=lf5L7>B&SorN&{;Y=PYO%h! zpEHyda{3J5P8xh4)?Ym(R55Zxp~cE#;|S3dyW9m8Koa$l&WTOYJ&7bucWrgu-Cq0` zjBb_!;kdJ!p(N1L&uH{MW6~*BINcxQn%hBfBRV@Okp7x5e*Ql$z&3dn(_T^WN<#+5 zq$+Lm8aZFxa{nfc<-vqA5uR$0pjKA&JAe9i&^}>`^iWGekS7-N%;Z67I7&*{&Fhux z=P3W)L-iT(TJRIpE*jVZ!9pwYLQ1E@C$hcCmM1t@2@P7v8SCXG6#iguUq4D7JYkVR zhDU6%Q`47(z@h_DRbg4M?s{R}UaNCEwC!mjj5F z?UYHmafQ^#;|i~P-~ZmYOKfGaIe;}xFMQY6DMy^4Up#_nfrKNEy0l>Ri4}>G2h9%l ziK!MpU%Gq4piyBtaC2)*sA{$UH6UK15X_xuRSxI~u)1`dv3(+7KehF7b|nDnbdzt1 zpY)@^McN+GWFO~8yT)h)@Mt5=SsMg&^YcMw1*8QmL|>j0^Hq-wf=yk7iaeXzIe8w! zfAt3T^AUJ+$e#O32F#J`3V9EiI3l^QV*rvbl>NJfAqetzuh z!vo_e$#$v|QS1u!UF8cpqmH$HM_Gf#nXy9~66M^LCaKQZCkPryR+~Vy>rJgM@(eF3 z5zher>A&Docb&Fu_Btgn!Pv=Z8jYNlDEUW^IU>rIQua0`1V`xF8{pw!Pil zD&=bIzM7srd)7*VYg02aRpglLr?l-7UTMgHYjC`PU7!!xo+so1e!^%b@w(HLcS6-n zX=u(AvaTMPiD&=~M@TuhvZ|tj(SVC!>eaS$y{xr0JbOCGiAH7L3(vCA{`@*bnWGP@ zS1!cP##Xw!@=#0lSg!XiIYVB%sZ$ChOZssM;|QIjjY&vVC!zk~h6|VT5vEg*AiYRM zhS7v_zi>xabV(i|^dL8)iJlHqd*@4xPV&ylZq*1ZJKBz#wgxIOyzr#796}k;c(cUt zB$}^x^mZDHLfsI8Q+!8ZpKM~N0_5RHsXnPLgV<0Q%5DF@%q{0M_1ac!#TstgT-XGF>N0CGae+~Fn4KUF>vfe6? z`l^Kv-05AR0cA!@bgmBUZhI*UP6kF4{TFXoZhB3--JtwBh)*Pi(6pSv-GNZe3>K!Y zJDMZ%wo^6=q?Q9O?B2;WL9H|f=1Qx%;F;uT5L<2xniXppbWVhTIKRGB*ht#=hJSS@ z8;EEh`F699Bbg*WMD0!)-6@UKPRox&LuUQQo+ZR`O`$<3%ZnZ2Osl1{3M;k9#z%Kqw~6Q3)&82U;6q)j_uy5IC_!=pMK`oWldJ2 zwDu4bcJHfC?%v{GQz?A=gASPrBNcwlY?5INm*d}XmT|6;MnjAMj`;YVV11bycCLyY{QGk@uqgPvEsu#KEF%UGDIJ?^6u{@gVeb$eFodx1_ja9^ z8q4^ouB{&$f5#(=i|;P8-2R-n9rEC`WPl4g`CcD4UWxkg+W$peM~X(TKMy`EYr_Y< zsjbNO`$V2DQJebvTZ5Ytl3jLH#F>7a|ASe6Z7!W)O3`;}8aUbeES)G$>Ooh0F-4pJ zuQq|x>)phP?D*Er)oFUp?LHryZXY$r= zJ05!WrSbV2e;$$RICyuvqX+M7P(syF<`Af%LE;%6u}oK5&-nP{M|&RbJEVCUOJQAe zeMNK~dGL@Ym@ik%%4oYMaS`yzpgB^dXbkzY;d6*HuM>aBDCOx8edSaL9qJNDUNkmP({A4+x)ZLA_V?Gv!$Yv7zLJrm$411E0+p#?3ARZ! zvHpWUn@9{y7q=};mn>zQ6`iv705eeqK7=^^SPojSw8e|abgLOielnG22t}bj&MJ28 ztN|HWPZHG3kByit+%Q;RI9qHCXV9YFXDPaTG=d%2gT7rA%KaAbP4Vj0eq2N9tPE#K zdHMMQ-!4g3Dp#}X`Y9%q2tS)2xa$Ji?4!u{39N?(>YaN30nPb$yPdoMTP$2j!eYL$ zt9hV$^(J6+2cRq4c0S46_|{RRG>1(-InC%Dpq-XDLUqM}+7)k0cvS|3@iL>i zB&4XcUjIR7>als~l(o8m7&6`efdqIm_byBDvn>Nla5fM6>BCWMYVTO-qaKiz^J)*i zIoYf34b8{%@PrYa+DB};P_pzK*u2-r-ZgqKMQhs-f`ORui>k^B2NFU;@5!}XAnELc z#~(tq6K8l~_S#?;&Y=3vBcWC-`XXpBaNQ55!a3;Bd9J@b(TX1$8}m}jz*AUC29luz z8UeF;clOt>uk0^sO8wUPS+CX;3>Gg~ZyLP@35h|5HhNCpK-#lF3FttG;aEI<2(%_)BPQWdh~Gb2YFOsZ7ut- zdxlYYTJY*8vrnZ4Ip-vGp@mUA`VfrZQ#v1P&KgGWZ2}IRyQZ7Px0#wch$N#7B1%x%~H=laby;##PS)R?zu9+~E_tDr|@o zL96AGghbJ*VY+Dv6#01HdSn~>Kq8Y1n>v`CMM^XBhqZd64UBC=oG4j^RztFz^Pd|N zs8oOtLqpS*MJz1uci^NTk0`0viAtqHSTThfmhFWmtbqe4k<4zY`J$Y4Yv}#L!qc4i zKK3(^jaQ&S{yF^XNL5;PKLq}GE?sk&Uq@Y>P%pcE{8BM6=2}-iUg(dCiqefR66Sb8 z9iYV@=3j7JU`f4|-6OKC%TzCMgdpC#+}ciAYQ;tpfvO?^)Q>-a-T(gS`?N^2w@#+Y zx^0xN^=X%3mbqj}>v1!OINua{qcj63wghpC3<_=Tpt5^5w&SVB!N|3v1_>^jo}PaD zN-pnDLMp+WZh3Cy*l!Mwi_gvc*?ESY76m z>CPso-Cm?-Irpp$4x5?JXZy{!?-IvV5;sAk_$y)RunX{98n*7cLD>7^Pd_2FyDiIk zh9kMkvzmi)7u1zJ&jaE}6HQ?h(asRFH0p3FQ)Je@n%)CJjPGOS7cm<*9@!@YA5aUT zZr$fP$8KXJq12BkLb-cM;!V5fO*DF6)M+g_cIUa-S&|(*4Ff!l4D@rOabjm*m@=ZZ z)$#;C<@x~l+85nIzu))i*Am!}5%*rnbloBr-MesEN>3`i=#=oK#UELR^(6Cwyj2iF z*=GYbp0~Bm+439(xQ*{l**AYS`P)>g71;U(nCJsZAYa*o?}VaanuwKd&R=LY89;k< zKodTFQh`JfBa9yKeRNJ&7%fq9<8=({{@MrokfDIb&}a|WHa9G%masO%D zZwMuN7g-G)c>ZMHse9y+QxG(DcpkcKGIMKV0apCbalTmYXQEbOd`i+};PqXOAJj>L zCbT*gIT4A0;w-%tmh8~g43kG&-4tgpuc26eUt%TspGV+fFeW^OQ|N_HfF-1g&hDZ3 z7N^-#D6px6Aw9nQ^T)HUN+58TNbH|gEb#w$J~1iw(b+nZ`Jf&#<-aFuJZ}=?twJ%g z3NTrxy3-C{pU8Z(tU;aQszY+uem$v?PCvYKjpwI`mjd3G!k@DI_`L`knlLYbWO;e5 z(t1aVmxaFkL*v5E0JSB_}Mkh{mTANaD?QmZN zicL=^$G;F1p5jlB-=e;uXpvg~VJXslvp;ZPPjKL0zIrN))&@EzP#a+5lr=SF!aoub zJjyZ5zVcaz8$FhRGNGyxu^W0K=e-_2Tx-}$`2UYP)8@KFM^Tm4v+I;Lu!3YO;i5YQ z^It|?!q@p@2|c;9VS8I+LR2r*jG~sXx@V`-qF_I;Eon;IC}y5_H*DqTCq^8ncd9oB zPiX}?^E$DupK=kZM_A}Z{JMnn*?3Vug;`4_RIABH{V3eg^}{l0Mk*7xIqSwCe<{Fp zzw5o+%}l1F$XwOe?Suey^qB5bU*~GLKZ{bZ^UEm@@UNf$Mnk;3Y-}R=J46T>Llqe6 zdY&{8*Si>eI2{O1#!@9UV3@oJ{IroL4xK4)$M$yDXgYjU+7v8#=ooFYR$wyE%T?7~ z$%xL#;Bl^=9d;@pQ`?qT;0Ay~I6=%p4nFn_`kPMyRnxoHw07ioMUco_?O+A%br!f* z2~(k@?gp$d^+Uv2UX3rufXX9&6i-;}xo+|#A*5^tb)@2+lP zvHNLhAkhP(Bpjvo=gAMgq3Wv-Wx7)iEj*u`7SX#HQwOUonZKch4)-SH*W=g|qqV78 zWalQhdwN+kd0M@6bsJIEf(j}u!_naLqc)>B z+3s6LOwdl8AF6EvO|tzMm>9kUk$IU7=E}+tlrdf{f8Xwg{Tn{(R=qPycmVD#7fW@B zU&Q@j@-%vntYJK=FY7&jCgFzO#bAMJ2lr{9`u;pyr~#AFmS4aV8HxbCt~!t{#$_+R z=I8zr-nRh@Hhpo)%~)k5hYwehZ&%< zs-bZkZY?Lg;E4C7Zf$u#i2Q&5I39v6DFE`MQJ^xP$m;%iW&Rae8Ezc_mMcdnunFlj zwQh^qt3{TD{TC4MNwX~9pEnGyfc5vstF56yZAKO2rqTrQk;Yk|$FLeqTHYWhuhmIV z1O8;1QBzir@Mv#<)fd>}D@!_C8Xm_Iaw(Oe2~6A`ZtM^g zLok5P@OT*^rggycX9iPW%Z+&Te52Y#>TWr`T$3cr{FKsTdb))`!_*|n3 zp;N~u89&E4jwk~wH6-6`Q3j80oc#goHFf`@ppTqKz+{LgtSaQB&;i zBSBhzkA=oYMlOUO>7rj81cEkt3np~q&xs4bdc629qdfaGlq{3=?bZ}Z-v4D)upEvs zbdz27<0g_`QWiDXb^jO=g#Kk%L4Q_WKzluEA^h3x!&KCo9^zFNXN%MBZt4>%hRkKAnEbT_~1*H@B*}{W#rEb2|buwBu_^ zopMT|ZBRxHF8;n8t=SP^^~O&AI)BFrD*^!k^3&_F)anAaB7td$dkY0PPO_as zfm(){yWWI}w|OWi)9QL3aDt|omls3QiGt85{@`&)b+6#FV|WY zXa1p`^1;IX->>0(q5N7RF-z5L>$XGo%c|SSMW<>c|>iMsrdw2;P z39eOD3h1;6Ge9A3li5X=40OXqq*hL9#Q#-N#B)$za-q0e-=0fj!A23+6Hbg?0tLLQ zy_Z+56v6W_Z??52u(0bueN7&-5_|>M<(cD6Vbhqivawz3rTF%zL3wma_ys^1FG)`M z!ykaN;68GEa~aT`b&wcV{V3)hzIZ%yi!{pt1vV5`P~2$_u#eY55zC5w-RNPe_g5hB zE6Ni14VR_TrJ5h=<8t2E#Ho3hCt|tGo=5aLHF0SL?d~H9~h5}03wgSJ! zb78(3Rb%n$-{fHz^P@;#v({*WG~Pc)kF{`{vaF?*UAo+D5mNsU%0~3;$OY zgP)6#?NtArbE6SKK*o^m=39xu-S@{}4zW5WDOrCAKx@QsvGd3LYo*yxBb@A<29)Gi$T zVDSSK;w*`!49{fIuR!Sd81CdC!_`V|P_?-Nv&_}on*)|5T*X77C}u1G6U`gQ+-h43 zZEf?hMH;VSz{f`&YQ;TRiDx+a5)jXoKFgmBOBzWv&2tsae?t?t_2vWjv``{kOmN(0 z-&@1)<=!DOFfcrZli$~p%xl>_KYhc$+`5SKuY~z1cz|IRg$gni`2DfOYy&5G55+){ z%*!CSX$e~PHM8{5Rm<#_6ykQ|z_(2Y0V`5u`@E?m^>6f?;}Ps93J zVV-!=0?2+JQjY{;aJ+=|Tp(7&)~~uRi=>*_{g;M;|6T}4b~h8%<|KxeHI{zxA?P`{ zN-Y8d*hVH7!S>6yd-e4RK30do3zEvv2?ALP?~{p`BlN(1yVw15do!COW;9isS2Vuu zHXg9@bR#EuKLAQJ!3a);O&$vk_(izXZ6Tj}+6SqE4kvKKT5}9X&q(pJjk!Rre*(Tr z*%cWcn9dWJ<)dU%l)iO*8996{!tW`+bP3hoWfVZ)ub_z(4}3wx@C0g*+)f7~snh>j ze-n6;1ZO4p#w21FKPy9VAHZ_scC2W_q3X8nZZXE6S0}@XhSbLePp+ z4%=K2!&N@v@ZmH$mV7Rh{)H^U2kVMk_AJht z$~^I)!lmHC62$eQ_KDr?tjEnVEwJ;@i8Y+^pJ1Zv1&&Fe1+B{U4P>;LE>x;O@!dxahm zqDTb2x8-hCfJjBkiUQ6=w8$T@fN4@t2}O152zC!VTJ`WeCWi2x#NYh*`wgiQFCxH9 zqP!gIhYkP#{2hNR)(?CWZ(FCz48LgvJ+84_>i@6?Zi+b$xpBRY?_gi zVUBY3kpFQJz0-V>`2H+a-lUFc>5_0JVF$G&-b@)(-tfndb#X-ge%sp;+)&XeB@lDL z;BO1yLb_8AXEp372QB97sV#2!xaw5F`o)CN(NPzY{uH?OJ;>EHI+?X$-n)F!)6MMy zV5&IPb4ljwZy%gbZHC)mHL%wEPxatyLU)jw_dk6LGK*xk0iu4D3Dl9t(EBEq<&l1_ z-M@vwN|%fdT3GYNw*&p`EYUZW{`@+K7pxt;y%&Sgn=B41Wk$|u3)b(`{x)!LU^mXU_;>r&XIkwSQny(ci+q*)E^EV1YNCPJWEd*%laxgi&b!a-_qW;TTEQE4fIR zT9EexkeCc7FX%G2kjbmmTBc0VC@Xp$xr4Lgi3F!pS|V@aWoaYCQ>@4YX5A4!r3?qJ zkPsG3>LAW^BN>=wV{{7naPd9s&!2Q~YqU?UnOX^*C$<*xJ_|NSIc#D$ zrDS-W3a|+_;AXWN1jFnb@<|vQcqL<12zX>kBsNhT8G_Usl>jgo-5>Qb0$r4(uslrd zoE{--4HzJngm5cKvcB6+*$(r?&MJKf!4)PfoZ%4THt90{}Fa}hX?9wHgv^Li0O-Ag{bdxpf+8Kkx zp3GS!B~%GbmBW-^^{T~4qqY5%8QA!>y}YZvehWUB1@vE+<2^>qIPvv^!O-BcBOE#P zXyA}5COzVtF@hjL=W(Zvv2@Q(< x5PD==iHm90@_43;i_5H7k%jdmaa{Y>-cucq&DrJ133v$PyzWI@jke9L{|7#S=KBBu literal 0 HcmV?d00001 diff --git a/src/android/app/src/main/res/layout/dialog_auto_map.xml b/src/android/app/src/main/res/layout/dialog_auto_map.xml new file mode 100644 index 000000000..95cb8d138 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_auto_map.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + +