From be259fc9b85c6452f06d928b2d50a39a6c925a3c Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Tue, 30 Dec 2025 21:13:24 +0100 Subject: [PATCH 01/11] qt: Do not refresh game directory while powered on (#1553) --- src/citra_qt/game_list.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/citra_qt/game_list.cpp b/src/citra_qt/game_list.cpp index 601bb2349..c275090dc 100644 --- a/src/citra_qt/game_list.cpp +++ b/src/citra_qt/game_list.cpp @@ -33,6 +33,7 @@ #include "citra_qt/uisettings.h" #include "common/logging/log.h" #include "common/settings.h" +#include "core/core.h" #include "core/file_sys/archive_extsavedata.h" #include "core/file_sys/archive_source_sd_savedata.h" #include "core/hle/service/am/am.h" @@ -1054,6 +1055,13 @@ const QStringList GameList::supported_file_extensions = { }; void GameList::RefreshGameDirectory() { + + // Do not scan directories when the system is powered on, it will be + // repopulated on shutdown anyways. + if (Core::System::GetInstance().IsPoweredOn()) { + return; + } + if (!UISettings::values.game_dirs.isEmpty() && current_worker != nullptr) { LOG_INFO(Frontend, "Change detected in the applications directory. Reloading game list."); PopulateAsync(UISettings::values.game_dirs); From 40d512cafdd59ceb85e32e56d2651065a3f43492 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Tue, 30 Dec 2025 22:21:44 +0100 Subject: [PATCH 02/11] plgldr: Fix plugin memory management (#1554) --- src/core/file_sys/plugin_3gx.cpp | 145 +++++++++++++------------ src/core/hle/kernel/svc.cpp | 26 +++++ src/core/hle/service/plgldr/plgldr.cpp | 16 ++- src/core/hle/service/plgldr/plgldr.h | 7 +- 4 files changed, 124 insertions(+), 70 deletions(-) diff --git a/src/core/file_sys/plugin_3gx.cpp b/src/core/file_sys/plugin_3gx.cpp index fc7374d0b..84b042962 100644 --- a/src/core/file_sys/plugin_3gx.cpp +++ b/src/core/file_sys/plugin_3gx.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. +// Originally MIT-licensed code from The Pixellizer Group + // Copyright 2022 The Pixellizer Group // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and @@ -22,6 +24,7 @@ #include "core/file_sys/file_backend.h" #include "core/file_sys/plugin_3gx.h" #include "core/file_sys/plugin_3gx_bootloader.h" +#include "core/hle/kernel/config_mem.h" #include "core/hle/kernel/vm_manager.h" #include "core/loader/loader.h" @@ -173,10 +176,10 @@ Loader::ResultStatus FileSys::Plugin3GXLoader::Map( } const std::array mem_region_sizes = { - 5 * 1024 * 1024, // 5 MiB - 2 * 1024 * 1024, // 2 MiB - 3 * 1024 * 1024, // 3 MiB - 4 * 1024 * 1024 // 4 MiB + 5 * 1024 * 1024, // 5 MiB + 2 * 1024 * 1024, // 2 MiB + 10 * 1024 * 1024, // 10 MiB + 5 * 1024 * 1024 // 5 MiB (reserved) }; const bool is_mem_private = header.infos.flags.use_private_memory != 0; @@ -184,65 +187,64 @@ Loader::ResultStatus FileSys::Plugin3GXLoader::Map( // Map memory block. This behaviour mimics how plugins are loaded on 3DS as much as possible. // Calculate the sizes of the different memory regions const u32 block_size = mem_region_sizes[header.infos.flags.memory_region_size.Value()]; + const u32 exe_offset = 0; const u32 exe_size = (sizeof(PluginHeader) + text_section.size() + rodata_section.size() + data_section.size() + header.executable.bss_size + 0x1000) & ~0xFFFu; + const u32 bootloader_offset = exe_offset + exe_size; + const u32 bootloader_size = bootloader_memory_size; + const u32 heap_offset = bootloader_offset + bootloader_size; + const u32 heap_size = block_size - heap_offset; - // Allocate the framebuffer block so that is in the highest FCRAM position possible - auto offset_fb = - kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM)->RLinearAllocate(_3GX_fb_size); - if (!offset_fb) { - LOG_ERROR(Service_PLGLDR, "Failed to load 3GX plugin. Not enough memory: {}", - plg_context.plugin_path); - return Loader::ResultStatus::ErrorMemoryAllocationFailed; + // Allocate a block of memory for the plugin + std::optional offset; + if (kernel.GetMemoryMode() == Kernel::MemoryMode::NewProd || + (plg_context.use_user_load_parameters && + plg_context.user_load_parameters.plugin_memory_strategy == + Service::PLGLDR::PLG_LDR::PluginMemoryStrategy::PLG_STRATEGY_MODE3)) { + // Allocate memory block from the end of the APPLICATION region + offset = + kernel.GetMemoryRegion(Kernel::MemoryRegion::APPLICATION)->RLinearAllocate(block_size); + + // If the reported available APP mem equals the actual size, remove the plugin block size. + if (offset) { + auto& config_mem = kernel.GetConfigMemHandler(); + if (config_mem.GetConfigMem().app_mem_alloc == + kernel.GetMemoryRegion(Kernel::MemoryRegion::APPLICATION)->size) { + config_mem.GetConfigMem().app_mem_alloc -= block_size; + } + } + plg_context.memory_region = Kernel::MemoryRegion::APPLICATION; + } else { + // Allocate memory block from the start of the SYSTEM region + offset = kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM)->LinearAllocate(block_size); + plg_context.memory_region = Kernel::MemoryRegion::SYSTEM; } - auto backing_memory_fb = kernel.memory.GetFCRAMRef(*offset_fb); - plg_ldr.SetPluginFBAddr(Memory::FCRAM_PADDR + *offset_fb); - std::fill(backing_memory_fb.GetPtr(), backing_memory_fb.GetPtr() + _3GX_fb_size, 0); - auto vma_heap_fb = process.vm_manager.MapBackingMemory( - _3GX_heap_load_addr, backing_memory_fb, _3GX_fb_size, - is_mem_private ? Kernel::MemoryState::Private : Kernel::MemoryState::Shared); - ASSERT(vma_heap_fb.Succeeded()); - process.vm_manager.Reprotect(vma_heap_fb.Unwrap(), Kernel::VMAPermission::ReadWrite); - - // Allocate a block from the end of FCRAM and clear it - auto offset = kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM) - ->RLinearAllocate(block_size - _3GX_fb_size); if (!offset) { - kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM)->Free(*offset_fb, _3GX_fb_size); LOG_ERROR(Service_PLGLDR, "Failed to load 3GX plugin. Not enough memory: {}", plg_context.plugin_path); return Loader::ResultStatus::ErrorMemoryAllocationFailed; } - auto backing_memory = kernel.memory.GetFCRAMRef(*offset); - std::fill(backing_memory.GetPtr(), backing_memory.GetPtr() + block_size - _3GX_fb_size, 0); - // Then we map part of the memory, which contains the executable - auto vma = process.vm_manager.MapBackingMemory(_3GX_exe_load_addr, backing_memory, exe_size, - is_mem_private ? Kernel::MemoryState::Private - : Kernel::MemoryState::Shared); - ASSERT(vma.Succeeded()); - process.vm_manager.Reprotect(vma.Unwrap(), Kernel::VMAPermission::ReadWriteExecute); + u32 fcram_offset = offset.value(); + + auto backing_memory_exe = kernel.memory.GetFCRAMRef(fcram_offset + exe_offset); + std::fill(backing_memory_exe.GetPtr(), backing_memory_exe.GetPtr() + exe_size, 0); + + // Map the executable + auto vma_exe = process.vm_manager.MapBackingMemory( + _3GX_exe_load_addr, backing_memory_exe, exe_size, + is_mem_private ? Kernel::MemoryState::Private : Kernel::MemoryState::Shared); + ASSERT(vma_exe.Succeeded()); + process.vm_manager.Reprotect(vma_exe.Unwrap(), Kernel::VMAPermission::ReadWriteExecute); - // Write text section - kernel.memory.WriteBlock(process, _3GX_exe_load_addr + sizeof(PluginHeader), - text_section.data(), header.executable.code_size); - // Write rodata section - kernel.memory.WriteBlock( - process, _3GX_exe_load_addr + sizeof(PluginHeader) + header.executable.code_size, - rodata_section.data(), header.executable.rodata_size); - // Write data section - kernel.memory.WriteBlock(process, - _3GX_exe_load_addr + sizeof(PluginHeader) + - header.executable.code_size + header.executable.rodata_size, - data_section.data(), header.executable.data_size); // Prepare plugin header and write it PluginHeader plugin_header = {0}; plugin_header.version = header.version; plugin_header.exe_size = exe_size; plugin_header.heap_VA = _3GX_heap_load_addr; - plugin_header.heap_size = block_size - exe_size; + plugin_header.heap_size = heap_size; plg_context.plg_event = _3GX_exe_load_addr - 0x4; plg_context.plg_reply = _3GX_exe_load_addr - 0x8; plugin_header.plgldr_event = plg_context.plg_event; @@ -254,39 +256,46 @@ Loader::ResultStatus FileSys::Plugin3GXLoader::Map( } kernel.memory.WriteBlock(process, _3GX_exe_load_addr, &plugin_header, sizeof(PluginHeader)); - // Map plugin heap - auto backing_memory_heap = kernel.memory.GetFCRAMRef(*offset + exe_size); + // Write text section + kernel.memory.WriteBlock(process, _3GX_exe_load_addr + sizeof(PluginHeader), + text_section.data(), header.executable.code_size); + // Write rodata section + kernel.memory.WriteBlock( + process, _3GX_exe_load_addr + sizeof(PluginHeader) + header.executable.code_size, + rodata_section.data(), header.executable.rodata_size); - // Map the rest of the memory at the heap location - auto vma_heap = process.vm_manager.MapBackingMemory( - _3GX_heap_load_addr + _3GX_fb_size, backing_memory_heap, - block_size - exe_size - _3GX_fb_size, - is_mem_private ? Kernel::MemoryState::Private : Kernel::MemoryState::Shared); - ASSERT(vma_heap.Succeeded()); - process.vm_manager.Reprotect(vma_heap.Unwrap(), Kernel::VMAPermission::ReadWriteExecute); + // Write data section + kernel.memory.WriteBlock(process, + _3GX_exe_load_addr + sizeof(PluginHeader) + + header.executable.code_size + header.executable.rodata_size, + data_section.data(), header.executable.data_size); - // Allocate a block from the end of FCRAM and clear it - auto bootloader_offset = kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM) - ->RLinearAllocate(bootloader_memory_size); - if (!bootloader_offset) { - kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM)->Free(*offset_fb, _3GX_fb_size); - kernel.GetMemoryRegion(Kernel::MemoryRegion::SYSTEM) - ->Free(*offset, block_size - _3GX_fb_size); - LOG_ERROR(Service_PLGLDR, "Failed to load 3GX plugin. Not enough memory: {}", - plg_context.plugin_path); - return Loader::ResultStatus::ErrorMemoryAllocationFailed; - } + // Map bootloader const bool use_internal = plg_context.load_exe_func.empty(); MapBootloader( - process, kernel, *bootloader_offset, + process, kernel, fcram_offset + bootloader_offset, (use_internal) ? exe_load_func : plg_context.load_exe_func, (use_internal) ? exe_load_args : plg_context.load_exe_args, header.executable.code_size + header.executable.rodata_size + header.executable.data_size, header.infos.exe_load_checksum, plg_context.use_user_load_parameters ? plg_context.user_load_parameters.no_flash : 0); + // Map plugin heap + auto backing_memory_heap = kernel.memory.GetFCRAMRef(fcram_offset + heap_offset); + std::fill(backing_memory_heap.GetPtr(), backing_memory_heap.GetPtr() + heap_size, 0); + + auto vma_heap = process.vm_manager.MapBackingMemory( + _3GX_heap_load_addr, backing_memory_heap, heap_size, + is_mem_private ? Kernel::MemoryState::Private : Kernel::MemoryState::Shared); + ASSERT(vma_heap.Succeeded()); + process.vm_manager.Reprotect(vma_heap.Unwrap(), Kernel::VMAPermission::ReadWriteExecute); + + plg_ldr.SetPluginFBAddr(Memory::FCRAM_PADDR + fcram_offset + heap_offset); plg_context.plugin_loaded = true; + plg_context.plugin_process_id = process.process_id; plg_context.use_user_load_parameters = false; + plg_context.memory_block = {fcram_offset, block_size}; + return Loader::ResultStatus::Success; } @@ -355,7 +364,7 @@ void FileSys::Plugin3GXLoader::MapBootloader(Kernel::Process& process, Kernel::K std::fill(backing_memory.GetPtr(), backing_memory.GetPtr() + bootloader_memory_size, 0); auto vma = process.vm_manager.MapBackingMemory(_3GX_exe_load_addr - bootloader_memory_size, backing_memory, bootloader_memory_size, - Kernel::MemoryState::Continuous); + Kernel::MemoryState::Private); ASSERT(vma.Succeeded()); process.vm_manager.Reprotect(vma.Unwrap(), Kernel::VMAPermission::ReadWriteExecute); diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp index c0bd6540b..58530f494 100644 --- a/src/core/hle/kernel/svc.cpp +++ b/src/core/hle/kernel/svc.cpp @@ -119,6 +119,11 @@ enum class SystemInfoType { * For the ARM11 NATIVE_FIRM kernel, this is 5, for processes sm, fs, pm, loader, and pxi." */ KERNEL_SPAWNED_PIDS = 26, + /** + * Check various Luma3DS config values. This parameter is not available on real systems, + * but can be used by homebrew applications. + */ + LUMA_CFW_INFO = 0x10000, /** * Check if the current system is a new 3DS. This parameter is not available on real systems, * but can be used by homebrew applications. @@ -270,6 +275,13 @@ enum class SystemInfoMemUsageRegion { BASE = 3, }; +enum class SystemInfoLumaCFWInformation { + REAL_APP_REGION_SIZE = 0x80, // Gets the real APPLICATION region size, + // instead of the one reported by the kernel shared page + // which depends on the memory mode. + IS_N3DS = 0x201, // Checks if the system is a N3DS or not. +}; + /** * Accepted by svcGetSystemInfo param with CITRA_INFORMATION type. Selects which information * to fetch from Citra. Some string params don't fit in 7 bytes, so they are split. @@ -1827,6 +1839,20 @@ Result SVC::GetSystemInfo(s64* out, u32 type, s32 param) { case SystemInfoType::KERNEL_SPAWNED_PIDS: *out = 5; break; + case SystemInfoType::LUMA_CFW_INFO: + switch ((SystemInfoLumaCFWInformation)param) { + case SystemInfoLumaCFWInformation::REAL_APP_REGION_SIZE: + *out = kernel.GetMemoryRegion(MemoryRegion::APPLICATION)->size; + break; + case SystemInfoLumaCFWInformation::IS_N3DS: + *out = Settings::values.is_new_3ds ? 1 : 0; + break; + default: + LOG_ERROR(Kernel_SVC, "unknown GetSystemInfo type=0x10000 region: param={}", param); + *out = 0; + break; + } + break; case SystemInfoType::NEW_3DS_INFO: // The actual subtypes are not implemented, homebrew just check // this doesn't return an error in n3ds to know the system type diff --git a/src/core/hle/service/plgldr/plgldr.cpp b/src/core/hle/service/plgldr/plgldr.cpp index d619a0d0d..0388226ac 100644 --- a/src/core/hle/service/plgldr/plgldr.cpp +++ b/src/core/hle/service/plgldr/plgldr.cpp @@ -2,6 +2,8 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +// Originally MIT-licensed code from The Pixellizer Group + // Copyright 2022 The Pixellizer Group // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and @@ -70,6 +72,9 @@ void PLG_LDR::PluginLoaderContext::serialize(Archive& ar, const unsigned int) { ar & plugin_loaded; ar & is_default_path; ar & plugin_path; + ar & memory_region; + ar & memory_block; + ar & plugin_process_id; ar & use_user_load_parameters; ar & user_load_parameters; ar & plg_event; @@ -148,12 +153,21 @@ void PLG_LDR::OnProcessRun(Kernel::Process& process, Kernel::KernelSystem& kerne } void PLG_LDR::OnProcessExit(Kernel::Process& process, Kernel::KernelSystem& kernel) { - if (plgldr_context.plugin_loaded) { + if (plgldr_context.plugin_loaded && process.process_id == plgldr_context.plugin_process_id) { u32 status = kernel.memory.Read32(FileSys::Plugin3GXLoader::_3GX_exe_load_addr - 0xC); if (status == 0) { LOG_CRITICAL(Service_PLGLDR, "Failed to launch {}: Checksum failed", plgldr_context.plugin_path); } + if (plgldr_context.memory_region != static_cast(0)) { + kernel.GetMemoryRegion(plgldr_context.memory_region) + ->Free(plgldr_context.memory_block.first, plgldr_context.memory_block.second); + plgldr_context.memory_region = static_cast(0); + } + plgldr_context.plugin_loaded = false; + plgldr_context.plugin_process_id = UINT32_MAX; + plgldr_context.memory_changed_handle = 0; + LOG_INFO(Service_PLGLDR, "Plugin unloaded successfully."); } } diff --git a/src/core/hle/service/plgldr/plgldr.h b/src/core/hle/service/plgldr/plgldr.h index 01952b587..ce5a847c4 100644 --- a/src/core/hle/service/plgldr/plgldr.h +++ b/src/core/hle/service/plgldr/plgldr.h @@ -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. +// Originally MIT-licensed code from The Pixellizer Group + // Copyright 2022 The Pixellizer Group // // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and @@ -62,6 +64,9 @@ public: bool plugin_loaded = false; bool is_default_path = false; std::string plugin_path = ""; + u32 plugin_process_id = UINT32_MAX; + Kernel::MemoryRegion memory_region{}; + std::pair memory_block{}; bool use_user_load_parameters = false; PluginLoadParameters user_load_parameters; From f31bd1bcdd615bf8013e5f3bc53313a608362230 Mon Sep 17 00:00:00 2001 From: Kleidis Date: Wed, 31 Dec 2025 15:38:22 +0100 Subject: [PATCH 03/11] android: Add support for compressing and decompressing files (#1458) --- .../java/org/citra/citra_emu/NativeLibrary.kt | 42 +++++ .../citra/citra_emu/adapters/GameAdapter.kt | 28 ++- .../CompressProgressDialogFragment.kt | 89 +++++++++ .../citra_emu/fragments/GamesFragment.kt | 62 ++++++- .../citra_emu/fragments/SearchFragment.kt | 24 ++- .../java/org/citra/citra_emu/model/Game.kt | 1 + .../org/citra/citra_emu/utils/GameHelper.kt | 1 + .../CompressProgressDialogViewModel.kt | 33 ++++ src/android/app/src/main/jni/id_cache.cpp | 9 +- src/android/app/src/main/jni/id_cache.h | 3 +- src/android/app/src/main/jni/native.cpp | 170 ++++++++++++++++++ .../src/main/res/layout/dialog_about_game.xml | 27 ++- .../res/layout/dialog_compress_progress.xml | 27 +++ .../app/src/main/res/values/strings.xml | 15 ++ src/core/hle/service/fs/archive.cpp | 11 +- src/core/hle/service/fs/archive.h | 3 +- 16 files changed, 532 insertions(+), 13 deletions(-) create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/CompressProgressDialogFragment.kt create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/viewmodel/CompressProgressDialogViewModel.kt create mode 100644 src/android/app/src/main/res/layout/dialog_compress_progress.xml diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index bce4ac490..f62510098 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -28,6 +28,7 @@ import org.citra.citra_emu.activities.EmulationActivity import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.RemovableStorageHelper +import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel import java.lang.ref.WeakReference import java.util.Date @@ -600,6 +601,47 @@ object NativeLibrary { */ external fun logDeviceInfo() + enum class CompressStatus(val value: Int) { + SUCCESS(0), + COMPRESS_UNSUPPORTED(1), + COMPRESS_ALREADY_COMPRESSED(2), + COMPRESS_FAILED(3), + DECOMPRESS_UNSUPPORTED(4), + DECOMPRESS_NOT_COMPRESSED(5), + DECOMPRESS_FAILED(6), + INSTALLED_APPLICATION(7); + + companion object { + fun fromValue(value: Int): CompressStatus = + CompressStatus.entries.first { it.value == value } + } + } + + // Compression / Decompression + private external fun compressFileNative(inputPath: String?, outputPath: String): Int + + fun compressFile(inputPath: String?, outputPath: String): CompressStatus { + return CompressStatus.fromValue( + compressFileNative(inputPath, outputPath) + ) + } + + private external fun decompressFileNative(inputPath: String?, outputPath: String): Int + + fun decompressFile(inputPath: String?, outputPath: String): CompressStatus { + return CompressStatus.fromValue( + decompressFileNative(inputPath, outputPath) + ) + } + + external fun getRecommendedExtension(inputPath: String?, shouldCompress: Boolean): String + + @Keep + @JvmStatic + fun onCompressProgress(total: Long, current: Long) { + CompressProgressDialogViewModel.update(total, current) + } + @Keep @JvmStatic fun createFile(directory: String, filename: String): Boolean = diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index d03ff2936..a92f93401 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -57,7 +57,12 @@ import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.GameIconUtils import org.citra.citra_emu.viewmodel.GamesViewModel -class GameAdapter(private val activity: AppCompatActivity, private val inflater: LayoutInflater, private val openImageLauncher: ActivityResultLauncher?) : +class GameAdapter( + private val activity: AppCompatActivity, + private val inflater: LayoutInflater, + private val openImageLauncher: ActivityResultLauncher?, + private val onRequestCompressOrDecompress: ((inputPath: String, suggestedName: String, shouldCompress: Boolean) -> Unit)? = null +) : ListAdapter(AsyncDifferConfig.Builder(DiffCallback()).build()), View.OnClickListener, View.OnLongClickListener { private var lastClickTime = 0L @@ -441,6 +446,27 @@ class GameAdapter(private val activity: AppCompatActivity, private val inflater: bottomSheetDialog.dismiss() } + val compressDecompressButton = bottomSheetView.findViewById(R.id.compress_decompress) + if (game.isInstalled) { + compressDecompressButton.setOnClickListener { + Toast.makeText( + context, + context.getString(R.string.compress_decompress_installed_app), + Toast.LENGTH_LONG + ).show() + } + compressDecompressButton.alpha = 0.38f + } else { + compressDecompressButton.setOnClickListener { + val shouldCompress = !game.isCompressed + val recommendedExt = NativeLibrary.getRecommendedExtension(holder.game.path, shouldCompress) + val baseName = holder.game.filename.substringBeforeLast('.') + onRequestCompressOrDecompress?.invoke(holder.game.path, "$baseName.$recommendedExt", shouldCompress) + bottomSheetDialog.dismiss() + } + } + compressDecompressButton.text = context.getString(if (!game.isCompressed) R.string.compress else R.string.decompress) + bottomSheetView.findViewById(R.id.menu_button_open).setOnClickListener { showOpenContextMenu(it, game) } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/CompressProgressDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CompressProgressDialogFragment.kt new file mode 100644 index 000000000..bcd97ae03 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/CompressProgressDialogFragment.kt @@ -0,0 +1,89 @@ +// 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.app.Dialog +import android.os.Bundle +import android.view.View +import android.widget.ProgressBar +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.lifecycle.Lifecycle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import org.citra.citra_emu.R +import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel +import org.citra.citra_emu.NativeLibrary + +class CompressProgressDialogFragment : DialogFragment() { + private lateinit var progressBar: ProgressBar + private var outputPath: String? = null + private var isCompressing: Boolean = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + isCompressing = it.getBoolean(ARG_IS_COMPRESSING, true) + outputPath = it.getString(ARG_OUTPUT_PATH) + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val view = layoutInflater.inflate(R.layout.dialog_compress_progress, null) + progressBar = view.findViewById(R.id.compress_progress) + val label = view.findViewById(R.id.compress_label) + label.text = if (isCompressing) getString(R.string.compressing) else getString(R.string.decompressing) + + isCancelable = false + progressBar.isIndeterminate = true + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + combine(CompressProgressDialogViewModel.total, CompressProgressDialogViewModel.progress) { total, progress -> + total to progress + }.collectLatest { (total, progress) -> + if (total <= 0) { + progressBar.isIndeterminate = true + label.visibility = View.GONE + } else { + progressBar.isIndeterminate = false + label.visibility = View.VISIBLE + progressBar.max = total + progressBar.setProgress(progress, true) + } + } + } + } + + val builder = MaterialAlertDialogBuilder(requireContext()) + .setView(view) + .setCancelable(false) + .setNegativeButton(android.R.string.cancel) { _: android.content.DialogInterface, _: Int -> + outputPath?.let { path -> + NativeLibrary.deleteDocument(path) + } + } + + return builder.show() + } + + companion object { + const val TAG = "CompressProgressDialog" + private const val ARG_IS_COMPRESSING = "isCompressing" + private const val ARG_OUTPUT_PATH = "outputPath" + + fun newInstance(isCompressing: Boolean, outputPath: String?): CompressProgressDialogFragment { + val frag = CompressProgressDialogFragment() + val args = Bundle() + args.putBoolean(ARG_IS_COMPRESSING, isCompressing) + args.putString(ARG_OUTPUT_PATH, outputPath) + frag.arguments = args + return frag + } + } +} diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt index b224c5c15..9ade73c83 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/GamesFragment.kt @@ -30,14 +30,17 @@ import androidx.recyclerview.widget.GridLayoutManager import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialFadeThrough +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.citra.citra_emu.CitraApplication +import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.R import org.citra.citra_emu.adapters.GameAdapter import org.citra.citra_emu.databinding.FragmentGamesBinding import org.citra.citra_emu.features.settings.model.Settings import org.citra.citra_emu.model.Game +import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel import org.citra.citra_emu.viewmodel.GamesViewModel import org.citra.citra_emu.viewmodel.HomeViewModel @@ -56,6 +59,58 @@ class GamesFragment : Fragment() { gameAdapter.handleShortcutImageResult(uri) } + private var shouldCompress: Boolean = true + private var pendingCompressInvocation: String? = null + + companion object { + fun doCompression(fragment: Fragment, gamesViewModel: GamesViewModel, inputPath: String?, outputUri: Uri?, shouldCompress: Boolean) { + if (outputUri != null) { + CompressProgressDialogViewModel.reset() + val dialog = CompressProgressDialogFragment.newInstance(shouldCompress, outputUri.toString()) + dialog.showNow( + fragment.requireActivity().supportFragmentManager, + CompressProgressDialogFragment.TAG + ) + + fragment.lifecycleScope.launch(Dispatchers.IO) { + val status = if (shouldCompress) { + NativeLibrary.compressFile(inputPath, outputUri.toString()) + } else { + NativeLibrary.decompressFile(inputPath, outputUri.toString()) + } + + fragment.requireActivity().runOnUiThread { + dialog.dismiss() + val resId = when (status) { + NativeLibrary.CompressStatus.SUCCESS -> if (shouldCompress) R.string.compress_success else R.string.decompress_success + NativeLibrary.CompressStatus.COMPRESS_UNSUPPORTED -> R.string.compress_unsupported + NativeLibrary.CompressStatus.COMPRESS_ALREADY_COMPRESSED -> R.string.compress_already + NativeLibrary.CompressStatus.COMPRESS_FAILED -> R.string.compress_failed + NativeLibrary.CompressStatus.DECOMPRESS_UNSUPPORTED -> R.string.decompress_unsupported + NativeLibrary.CompressStatus.DECOMPRESS_NOT_COMPRESSED -> R.string.decompress_not_compressed + NativeLibrary.CompressStatus.DECOMPRESS_FAILED -> R.string.decompress_failed + NativeLibrary.CompressStatus.INSTALLED_APPLICATION -> R.string.compress_decompress_installed_app + } + + MaterialAlertDialogBuilder(fragment.requireContext()) + .setMessage(fragment.getString(resId)) + .setPositiveButton(android.R.string.ok, null) + .show() + + gamesViewModel.reloadGames(false) + } + } + } + } + } + + private val onCompressDecompressLauncher = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/octet-stream") + ) { uri: Uri? -> + doCompression(this, gamesViewModel, pendingCompressInvocation, uri, shouldCompress) + pendingCompressInvocation = null + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enterTransition = MaterialFadeThrough() @@ -81,7 +136,12 @@ class GamesFragment : Fragment() { gameAdapter = GameAdapter( requireActivity() as AppCompatActivity, inflater, - openImageLauncher + openImageLauncher, + onRequestCompressOrDecompress = { inputPath, suggestedName, shouldCompress -> + pendingCompressInvocation = inputPath + onCompressDecompressLauncher.launch(suggestedName) + this.shouldCompress = shouldCompress + } ) binding.gridGames.apply { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SearchFragment.kt index 94821023f..dab5ea745 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SearchFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SearchFragment.kt @@ -7,11 +7,13 @@ package org.citra.citra_emu.fragments import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat @@ -26,18 +28,19 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.JaroWinkler +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R +import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.adapters.GameAdapter import org.citra.citra_emu.databinding.FragmentSearchBinding import org.citra.citra_emu.model.Game +import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel import org.citra.citra_emu.viewmodel.GamesViewModel import org.citra.citra_emu.viewmodel.HomeViewModel import java.time.temporal.ChronoField import java.util.Locale -import android.net.Uri -import androidx.activity.result.contract.ActivityResultContracts class SearchFragment : Fragment() { private var _binding: FragmentSearchBinding? = null @@ -53,6 +56,15 @@ class SearchFragment : Fragment() { gameAdapter.handleShortcutImageResult(uri) } + private var shouldCompress: Boolean = true + private var pendingCompressInvocation: String? = null + private val onCompressDecompressLauncher = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/octet-stream") + ) { uri: Uri? -> + GamesFragment.doCompression(this, gamesViewModel, pendingCompressInvocation, uri, shouldCompress) + pendingCompressInvocation = null + } + private lateinit var preferences: SharedPreferences companion object { @@ -85,7 +97,13 @@ class SearchFragment : Fragment() { gameAdapter = GameAdapter( requireActivity() as AppCompatActivity, inflater, - openImageLauncher + openImageLauncher, + onRequestCompressOrDecompress = { inputPath, suggestedName, shouldCompress -> + pendingCompressInvocation = inputPath + onCompressDecompressLauncher.launch(suggestedName) + this.shouldCompress = shouldCompress + } + ) binding.gridGamesSearch.apply { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt index 9ff7600ec..b74635e96 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt @@ -27,6 +27,7 @@ class Game( val isVisibleSystemTitle: Boolean = false, val icon: IntArray? = null, val fileType: String = "", + val isCompressed: Boolean = false, val filename: String, ) : Parcelable { val keyAddedToLibraryTime get() = "${filename}_AddedToLibraryTime" diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt index ffbeaf394..7b7ca9fdb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt @@ -90,6 +90,7 @@ object GameHelper { gameInfo?.getIsVisibleSystemTitle() ?: false, gameInfo?.getIcon(), gameInfo?.getFileType() ?: "", + gameInfo?.getFileType()?.contains("(Z)") ?: false, if (FileUtil.isNativePath(filePath)) { CitraApplication.documentsTree.getFilename(filePath) } else { diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/CompressProgressDialogViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/CompressProgressDialogViewModel.kt new file mode 100644 index 000000000..7a6e71e52 --- /dev/null +++ b/src/android/app/src/main/java/org/citra/citra_emu/viewmodel/CompressProgressDialogViewModel.kt @@ -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. + +package org.citra.citra_emu.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +object CompressProgressDialogViewModel: ViewModel() { + private val _progress = MutableStateFlow(0) + val progress = _progress.asStateFlow() + + private val _total = MutableStateFlow(0) + val total = _total.asStateFlow() + + private val _message = MutableStateFlow("") + val message = _message.asStateFlow() + + fun update(totalBytes: Long, currentBytes: Long) { + val percent = ((currentBytes * 100L) / totalBytes).coerceIn(0L, 100L).toInt() + _total.value = 100 + _progress.value = percent + _message.value = "" + } + + fun reset() { + _progress.value = 0 + _total.value = 0 + _message.value = "" + } +} \ No newline at end of file diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index 8bc0f976e..d7d4a109a 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -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. @@ -40,6 +40,7 @@ static jfieldID s_game_info_pointer; static jclass s_disk_cache_progress_class; static jmethodID s_disk_cache_load_progress; +static jmethodID s_compress_progress_method; static std::unordered_map s_java_load_callback_stages; static jclass s_cia_install_helper_class; @@ -131,6 +132,10 @@ jmethodID GetDiskCacheLoadProgress() { return s_disk_cache_load_progress; } +jmethodID GetCompressProgressMethod() { + return s_compress_progress_method; +} + jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage) { const auto it = s_java_load_callback_stages.find(stage); ASSERT_MSG(it != s_java_load_callback_stages.end(), "Invalid LoadCallbackStage: {}", stage); @@ -205,6 +210,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_disk_cache_load_progress = env->GetStaticMethodID( s_disk_cache_progress_class, "loadProgress", "(Lorg/citra/citra_emu/utils/DiskShaderCacheProgress$LoadCallbackStage;II)V"); + s_compress_progress_method = + env->GetStaticMethodID(s_native_library_class, "onCompressProgress", "(JJ)V"); // Initialize LoadCallbackStage map const auto to_java_load_callback_stage = [env, load_callback_stage_class](const std::string& stage) { diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h index 71a1cb67c..d7aeb8074 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.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. @@ -40,6 +40,7 @@ jfieldID GetGameInfoPointer(); jclass GetDiskCacheProgressClass(); jmethodID GetDiskCacheLoadProgress(); +jmethodID GetCompressProgressMethod(); jobject GetJavaLoadCallbackStage(VideoCore::LoadCallbackStage stage); jclass GetCiaInstallHelperClass(); diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index e52c09a65..a0dc4d864 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -34,10 +34,12 @@ #include "common/scope_exit.h" #include "common/settings.h" #include "common/string_util.h" +#include "common/zstd_compression.h" #include "core/core.h" #include "core/frontend/applets/default_applets.h" #include "core/frontend/camera/factory.h" #include "core/hle/service/am/am.h" +#include "core/hle/service/fs/archive.h" #include "core/hle/service/nfc/nfc.h" #include "core/hw/unique_data.h" #include "core/loader/loader.h" @@ -74,6 +76,17 @@ namespace { ANativeWindow* s_surface; ANativeWindow* s_secondary_surface; +enum class CompressionStatus : jint { + Success = 0, + Compress_Unsupported = 1, + Compress_AlreadyCompressed = 2, + Compress_Failed = 3, + Decompress_Unsupported = 4, + Decompress_NotCompressed = 5, + Decompress_Failed = 6, + Installed_Application = 7, +}; + std::shared_ptr vulkan_library{}; std::unique_ptr window; std::unique_ptr secondary_window; @@ -464,6 +477,163 @@ jstring Java_org_citra_citra_1emu_NativeLibrary_getHomeMenuPath(JNIEnv* env, return ToJString(env, ""); } +static CompressionStatus GetCompressFileInfo(Loader::AppLoader::CompressFileInfo& out_info, + size_t& out_frame_size, const std::string& filepath, + bool compress) { + + if (Service::FS::IsInstalledApplication(filepath)) { + return CompressionStatus::Installed_Application; + } + + Loader::AppLoader::CompressFileInfo compress_info{}; + compress_info.is_supported = false; + size_t frame_size{}; + auto loader = Loader::GetLoader(filepath); + if (loader) { + compress_info = loader->GetCompressFileInfo(); + frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_FRAME_SIZE; + } else { + bool is_compressed = false; + if (Service::AM::CheckCIAToInstall(filepath, is_compressed, compress ? true : false) == + Service::AM::InstallStatus::Success) { + compress_info.is_supported = true; + compress_info.is_compressed = is_compressed; + compress_info.recommended_compressed_extension = "zcia"; + compress_info.recommended_uncompressed_extension = "cia"; + compress_info.underlying_magic = std::array({'C', 'I', 'A', '\0'}); + frame_size = FileUtil::Z3DSWriteIOFile::DEFAULT_CIA_FRAME_SIZE; + if (compress) { + auto meta_info = Service::AM::GetCIAInfos(filepath); + if (meta_info.Succeeded()) { + const auto& meta_info_val = meta_info.Unwrap(); + std::vector value(sizeof(Service::AM::TitleInfo)); + memcpy(value.data(), &meta_info_val.first, sizeof(Service::AM::TitleInfo)); + compress_info.default_metadata.emplace("titleinfo", value); + if (meta_info_val.second) { + value.resize(sizeof(Loader::SMDH)); + memcpy(value.data(), meta_info_val.second.get(), sizeof(Loader::SMDH)); + compress_info.default_metadata.emplace("smdh", value); + } + } + } + } + } + + if (!compress_info.is_supported) { + LOG_ERROR(Frontend, + "Error {} file {}, the selected file is not a compatible 3DS ROM format or is " + "encrypted.", + compress ? "compressing" : "decompressing", filepath); + return compress ? CompressionStatus::Compress_Unsupported + : CompressionStatus::Decompress_Unsupported; + } + if (compress_info.is_compressed && compress) { + LOG_ERROR(Frontend, "Error compressing file {}, the selected file is already compressed", + filepath); + return CompressionStatus::Compress_AlreadyCompressed; + } + if (!compress_info.is_compressed && !compress) { + LOG_ERROR(Frontend, + "Error decompressing file {}, the selected file is already decompressed", + filepath); + return CompressionStatus::Decompress_NotCompressed; + } + + out_info = compress_info; + out_frame_size = frame_size; + return CompressionStatus::Success; +} + +jint Java_org_citra_citra_1emu_NativeLibrary_compressFileNative(JNIEnv* env, jobject obj, + jstring j_input_path, + jstring j_output_path) { + const std::string input_path = GetJString(env, j_input_path); + const std::string output_path = GetJString(env, j_output_path); + + Loader::AppLoader::CompressFileInfo compress_info{}; + size_t frame_size{}; + CompressionStatus stat = GetCompressFileInfo(compress_info, frame_size, input_path, true); + if (stat != CompressionStatus::Success) { + return static_cast(stat); + } + + auto progress = [](std::size_t processed, std::size_t total) { + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetCompressProgressMethod(), static_cast(total), + static_cast(processed)); + }; + + bool success = + FileUtil::CompressZ3DSFile(input_path, output_path, compress_info.underlying_magic, + frame_size, progress, compress_info.default_metadata); + if (!success) { + FileUtil::Delete(output_path); + return static_cast(CompressionStatus::Compress_Failed); + } + + return static_cast(CompressionStatus::Success); +} + +jint Java_org_citra_citra_1emu_NativeLibrary_decompressFileNative(JNIEnv* env, jobject obj, + jstring j_input_path, + jstring j_output_path) { + const std::string input_path = GetJString(env, j_input_path); + const std::string output_path = GetJString(env, j_output_path); + + Loader::AppLoader::CompressFileInfo compress_info{}; + size_t frame_size{}; + CompressionStatus stat = GetCompressFileInfo(compress_info, frame_size, input_path, false); + if (stat != CompressionStatus::Success) { + return static_cast(stat); + } + + auto progress = [](std::size_t processed, std::size_t total) { + JNIEnv* env = IDCache::GetEnvForThread(); + env->CallStaticVoidMethod(IDCache::GetNativeLibraryClass(), + IDCache::GetCompressProgressMethod(), static_cast(total), + static_cast(processed)); + }; + + bool success = FileUtil::DeCompressZ3DSFile(input_path, output_path, progress); + if (!success) { + FileUtil::Delete(output_path); + return static_cast(CompressionStatus::Decompress_Failed); + } + + return static_cast(CompressionStatus::Success); +} + +jstring Java_org_citra_citra_1emu_NativeLibrary_getRecommendedExtension( + JNIEnv* env, jobject obj, jstring j_input_path, jboolean j_should_compress) { + const std::string input_path = GetJString(env, j_input_path); + + std::string compressed_ext; + std::string uncompressed_ext; + + auto loader = Loader::GetLoader(input_path); + if (loader) { + auto compress_info = loader->GetCompressFileInfo(); + if (compress_info.is_supported) { + compressed_ext = compress_info.recommended_compressed_extension; + uncompressed_ext = compress_info.recommended_uncompressed_extension; + } + } else { + bool is_compressed = false; + if (Service::AM::CheckCIAToInstall(input_path, is_compressed, true) == + Service::AM::InstallStatus::Success) { + compressed_ext = "zcia"; + uncompressed_ext = "cia"; + } + } + + if (compressed_ext.empty()) { + return env->NewStringUTF(""); + } + + return env->NewStringUTF(j_should_compress ? compressed_ext.c_str() : uncompressed_ext.c_str()); +} + void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv* env, [[maybe_unused]] jobject obj, jstring j_directory) { diff --git a/src/android/app/src/main/res/layout/dialog_about_game.xml b/src/android/app/src/main/res/layout/dialog_about_game.xml index feb95cf2d..2ba1e3473 100644 --- a/src/android/app/src/main/res/layout/dialog_about_game.xml +++ b/src/android/app/src/main/res/layout/dialog_about_game.xml @@ -45,7 +45,8 @@ android:layout_height="wrap_content" android:textAlignment="viewStart" android:textSize="15sp" - android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" + android:textStyle="bold" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:text="Application Title" /> @@ -166,7 +167,6 @@ android:layout_marginTop="16dp" android:gravity="start|center" android:orientation="horizontal" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/horizontal_layout"> @@ -179,6 +179,29 @@ android:contentDescription="@string/cheats" android:text="@string/cheats" /> + + + + + diff --git a/src/android/app/src/main/res/layout/dialog_compress_progress.xml b/src/android/app/src/main/res/layout/dialog_compress_progress.xml new file mode 100644 index 000000000..bdf8dd669 --- /dev/null +++ b/src/android/app/src/main/res/layout/dialog_compress_progress.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 11c4943c8..e861035d5 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -912,4 +912,19 @@ Quicksave - %1$tF %1$tR No Quicksave available. + + Compress + Compressing… + Decompress + Decompressing… + Compression completed successfully. + Compression not supported for this file. + File is already compressed. + Compression failed. + Decompression completed successfully. + Decompression not supported for this file. + File is not compressed. + Decompression failed. + Already installed applications cannot be compressed or decompressed. + diff --git a/src/core/hle/service/fs/archive.cpp b/src/core/hle/service/fs/archive.cpp index 2a48a741a..131cb58c0 100644 --- a/src/core/hle/service/fs/archive.cpp +++ b/src/core/hle/service/fs/archive.cpp @@ -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. @@ -30,11 +30,16 @@ namespace Service::FS { +bool IsInstalledApplication(std::string_view path) { + return path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "title", 0) == 0 || + path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "Nintendo 3DS", 0) == 0; +} + MediaType GetMediaTypeFromPath(std::string_view path) { - if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), 0) == 0) { + if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "title", 0) == 0) { return MediaType::NAND; } - if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir), 0) == 0) { + if (path.rfind(FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "Nintendo 3DS", 0) == 0) { return MediaType::SDMC; } return MediaType::GameCard; diff --git a/src/core/hle/service/fs/archive.h b/src/core/hle/service/fs/archive.h index 387b11dba..2e017f3e0 100644 --- a/src/core/hle/service/fs/archive.h +++ b/src/core/hle/service/fs/archive.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. @@ -51,6 +51,7 @@ enum class ArchiveIdCode : u32 { /// Media types for the archives enum class MediaType : u32 { NAND = 0, SDMC = 1, GameCard = 2 }; +bool IsInstalledApplication(std::string_view path); MediaType GetMediaTypeFromPath(std::string_view path); enum class SpecialContentType : u8 { From 43e044ad9ad9dd4f124dc0acf8abd7f69b59778e Mon Sep 17 00:00:00 2001 From: Chance Date: Wed, 31 Dec 2025 12:29:53 -0600 Subject: [PATCH 04/11] Fix simple typo that prevented debug builds (MSVC) --- src/common/file_util.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 49589d6f1..2a49d26b5 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -305,7 +305,7 @@ bool DeleteDir(const std::string& filename) { } bool Rename(const std::string& srcFullPath, const std::string& destFullPath) { - LOG_TRACE(Common_Filesystem, "{} --> {}", srcPath, destFullPath); + LOG_TRACE(Common_Filesystem, "{} --> {}", srcFullPath, destFullPath); #ifdef _WIN32 if (_wrename(Common::UTF8ToUTF16W(srcFullPath).c_str(), Common::UTF8ToUTF16W(destFullPath).c_str()) == 0) From 4ac18f5e18ef80ff547621a4928b274de7635176 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Fri, 2 Jan 2026 15:03:23 +0100 Subject: [PATCH 05/11] core: Fix memory mode handling and n3ds exclusive app detection (#1560) --- src/android/app/src/main/jni/game_info.cpp | 2 +- src/citra_qt/game_list_worker.cpp | 2 +- src/core/core.cpp | 16 +++++---------- src/core/hle/kernel/kernel.cpp | 5 ++++- src/core/loader/artic.cpp | 10 ++++++++++ src/core/loader/artic.h | 2 ++ src/core/loader/loader.h | 4 ++++ src/core/loader/ncch.cpp | 17 ++++++++++++++++ src/core/loader/ncch.h | 2 ++ src/core/loader/smdh.h | 23 +++++++++++++++++----- 10 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/android/app/src/main/jni/game_info.cpp b/src/android/app/src/main/jni/game_info.cpp index 99ebfd3ae..ebf61820a 100644 --- a/src/android/app/src/main/jni/game_info.cpp +++ b/src/android/app/src/main/jni/game_info.cpp @@ -222,7 +222,7 @@ jboolean Java_org_citra_citra_1emu_model_GameInfo_getIsVisibleSystemTitle(JNIEnv return false; } - return smdh->flags & Loader::SMDH::Flags::Visible; + return smdh->flags.visible; } jstring Java_org_citra_citra_1emu_model_GameInfo_getFileType(JNIEnv* env, jobject obj) { diff --git a/src/citra_qt/game_list_worker.cpp b/src/citra_qt/game_list_worker.cpp index b6376261f..7e5c4650f 100644 --- a/src/citra_qt/game_list_worker.cpp +++ b/src/citra_qt/game_list_worker.cpp @@ -92,7 +92,7 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign if (Loader::IsValidSMDH(smdh)) { if (system_title) { auto smdh_struct = reinterpret_cast(smdh.data()); - if (!(smdh_struct->flags & Loader::SMDH::Flags::Visible)) { + if (!smdh_struct->flags.visible) { // Skip system titles without the visible flag. return true; } diff --git a/src/core/core.cpp b/src/core/core.cpp index e960571a7..d00de677c 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -362,8 +362,7 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st ASSERT(n3ds_hw_caps.first); app_n3ds_hw_capabilities = n3ds_hw_caps.first.value(); - if (!Settings::values.is_new_3ds.GetValue() && - app_n3ds_hw_capabilities.memory_mode != Kernel::New3dsMemoryMode::Legacy) { + if (!Settings::values.is_new_3ds.GetValue() && app_loader->IsN3DSExclusive()) { return ResultStatus::ErrorN3DSApplication; } @@ -374,14 +373,10 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st // proper memory mode. if (used_default_mem_mode) { - // If we are on the Old 3DS prod mode, the application is not a New 3DS application and - // the application memory mode does not match, we need to adjust it. We do not need - // adjustment if we are on the New 3DS prod mode, as that one overrides all the Old 3DS - // memory modes. - if (system_mem_mode == Kernel::MemoryMode::Prod && - app_n3ds_hw_capabilities.memory_mode == Kernel::New3dsMemoryMode::Legacy && - app_mem_mode != system_mem_mode) { - + // If we are on the Old 3DS prod mode and the application memory mode does not match, we + // need to adjust it. We do not need adjustment if we are on the New 3DS prod mode, as that + // one overrides all the Old 3DS memory modes. + if (system_mem_mode == Kernel::MemoryMode::Prod && app_mem_mode != system_mem_mode) { system_mem_mode = app_mem_mode; } @@ -389,7 +384,6 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st // memory mode (only CTRAging is known to do this), adjust the memory mode. else if (system_mem_mode == Kernel::MemoryMode::NewProd && app_n3ds_hw_capabilities.memory_mode == Kernel::New3dsMemoryMode::NewDev1) { - system_mem_mode = Kernel::MemoryMode::NewDev1; } } diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp index d49bcb2d1..21ca655db 100644 --- a/src/core/hle/kernel/kernel.cpp +++ b/src/core/hle/kernel/kernel.cpp @@ -7,6 +7,7 @@ #include #include "common/archives.h" #include "common/serialization/atomic.h" +#include "common/settings.h" #include "core/hle/kernel/client_port.h" #include "core/hle/kernel/config_mem.h" #include "core/hle/kernel/handle_table.h" @@ -172,7 +173,9 @@ void KernelSystem::ResetThreadIDs() { void KernelSystem::UpdateCPUAndMemoryState(u64 title_id, MemoryMode memory_mode, New3dsHwCapabilities n3ds_hw_cap) { - SetRunning804MHz(n3ds_hw_cap.enable_804MHz_cpu); + if (Settings::values.is_new_3ds) { + SetRunning804MHz(n3ds_hw_cap.enable_804MHz_cpu); + } u32 tid_high = static_cast(title_id >> 32); diff --git a/src/core/loader/artic.cpp b/src/core/loader/artic.cpp index 30f62dc57..9c7aefbeb 100644 --- a/src/core/loader/artic.cpp +++ b/src/core/loader/artic.cpp @@ -134,6 +134,16 @@ Apploader_Artic::LoadNew3dsHwCapabilities() { return std::make_pair(std::move(caps), ResultStatus::Success); } +bool Apploader_Artic::IsN3DSExclusive() { + std::vector smdh_buffer; + if (ReadIcon(smdh_buffer) == ResultStatus::Success && IsValidSMDH(smdh_buffer)) { + SMDH* smdh = reinterpret_cast(smdh_buffer.data()); + return smdh->flags.n3ds_exclusive != 0; + } + + return false; +} + ResultStatus Apploader_Artic::LoadExec(std::shared_ptr& process) { if (!is_loaded) diff --git a/src/core/loader/artic.h b/src/core/loader/artic.h index 13b92c189..a6de6e0fd 100644 --- a/src/core/loader/artic.h +++ b/src/core/loader/artic.h @@ -56,6 +56,8 @@ public: std::pair, ResultStatus> LoadNew3dsHwCapabilities() override; + bool IsN3DSExclusive() override; + ResultStatus IsExecutable(bool& out_executable) override; ResultStatus ReadCode(std::vector& buffer) override; diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h index 2c5ef952f..392b6025d 100644 --- a/src/core/loader/loader.h +++ b/src/core/loader/loader.h @@ -163,6 +163,10 @@ public: ResultStatus::Success); } + virtual bool IsN3DSExclusive() { + return false; + } + /** * Get whether this application is executable. * @param out_executable Reference to store the executable flag into. diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index ef33c42d3..0b2058293 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -125,6 +125,23 @@ AppLoader_NCCH::LoadNew3dsHwCapabilities() { return std::make_pair(std::move(caps), ResultStatus::Success); } +bool AppLoader_NCCH::IsN3DSExclusive() { + if (!is_loaded) { + ResultStatus res = base_ncch.Load(); + if (res != ResultStatus::Success) { + return false; + } + } + + std::vector smdh_buffer; + if (ReadIcon(smdh_buffer) == ResultStatus::Success && IsValidSMDH(smdh_buffer)) { + SMDH* smdh = reinterpret_cast(smdh_buffer.data()); + return smdh->flags.n3ds_exclusive != 0; + } + + return false; +} + ResultStatus AppLoader_NCCH::LoadExec(std::shared_ptr& process) { using Kernel::CodeSet; diff --git a/src/core/loader/ncch.h b/src/core/loader/ncch.h index d36e4c249..c5b1b45d5 100644 --- a/src/core/loader/ncch.h +++ b/src/core/loader/ncch.h @@ -50,6 +50,8 @@ public: std::pair, ResultStatus> LoadNew3dsHwCapabilities() override; + bool IsN3DSExclusive() override; + ResultStatus IsExecutable(bool& out_executable) override; ResultStatus ReadCode(std::vector& buffer) override; diff --git a/src/core/loader/smdh.h b/src/core/loader/smdh.h index 2913b195b..04a97f5d4 100644 --- a/src/core/loader/smdh.h +++ b/src/core/loader/smdh.h @@ -7,6 +7,7 @@ #include #include #include +#include "common/bit_field.h" #include "common/common_funcs.h" #include "common/common_types.h" #include "common/swap.h" @@ -37,7 +38,23 @@ struct SMDH { u32_le region_lockout; u32_le match_maker_id; u64_le match_maker_bit_id; - u32_le flags; + union { + u32_le raw; + + BitField<0, 1, u32> visible; + BitField<1, 1, u32> autoboot; + BitField<2, 1, u32> allow_3D; + BitField<3, 1, u32> require_eula; + BitField<4, 1, u32> autosave; + BitField<5, 1, u32> extended_banner; + BitField<6, 1, u32> rating_required; + BitField<7, 1, u32> uses_savedata; + BitField<8, 1, u32> record_usage; + BitField<10, 1, u32> disable_save_backup; + BitField<12, 1, u32> n3ds_exclusive; + BitField<14, 1, u32> parental_restricted; + } flags; + u16_le eula_version; INSERT_PADDING_BYTES(2); float_le banner_animation_frame; @@ -73,10 +90,6 @@ struct SMDH { Taiwan = 6, }; - enum Flags { - Visible = 1 << 0, - }; - /** * Checks if SMDH is valid. */ From a826f5ffa145be08ead7c23184415f07eca70a63 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Fri, 2 Jan 2026 16:26:03 +0100 Subject: [PATCH 06/11] video_core/renderer_vulkan: Fix fb image view usage (#1561) --- src/video_core/renderer_vulkan/vk_texture_runtime.cpp | 10 ++++++---- src/video_core/renderer_vulkan/vk_texture_runtime.h | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index 34ce793ad..b0f07def3 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1369,17 +1369,19 @@ vk::ImageView Surface::ImageView(u32 index) const noexcept { vk::ImageView Surface::FramebufferView() noexcept { is_framebuffer = true; + const u32 index = res_scale == 1 ? 0u : 1u; + // If we already have a framebuffer-compatible view, return it - if (framebuffer_view) { - return framebuffer_view.get(); + if (framebuffer_view[index]) { + return framebuffer_view[index].get(); } // Create a new view with a single mip level for framebuffer compatibility // This is critical to avoid VUID-VkFramebufferCreateInfo-pAttachments-00883 validation errors - framebuffer_view = MakeFramebufferImageView( + framebuffer_view[index] = MakeFramebufferImageView( instance->GetDevice(), Image(), instance->GetTraits(pixel_format).native, Aspect(), 0); - return framebuffer_view.get(); + return framebuffer_view[index].get(); } vk::ImageView Surface::DepthView() noexcept { diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.h b/src/video_core/renderer_vulkan/vk_texture_runtime.h index eb48451ed..ecd7a5ef4 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.h +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.h @@ -201,7 +201,7 @@ public: std::array handles{}; std::array framebuffers{}; Handle copy_handle; - vk::UniqueImageView framebuffer_view; + std::array framebuffer_view; vk::UniqueImageView depth_view; vk::UniqueImageView stencil_view; vk::UniqueImageView storage_view; From 88d1523b3beb4a563a398b786be84515150184e8 Mon Sep 17 00:00:00 2001 From: jbm11208 <81182113+jbm11208@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:42:28 -0500 Subject: [PATCH 07/11] video_core/renderer_vulkan: fix linear filtering toggle (#1513) --- .../renderer_vulkan/vk_texture_runtime.cpp | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp index b0f07def3..6b9b388fa 100644 --- a/src/video_core/renderer_vulkan/vk_texture_runtime.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_runtime.cpp @@ -1484,9 +1484,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { if (is_depth_stencil && !depth_traits.blit_support) { LOG_WARNING(Render_Vulkan, "Depth scale unsupported by hardware"); return; - } // Check if texture filtering is enabled - const bool texture_filter_enabled = - Settings::values.texture_filter.GetValue() != Settings::TextureFilter::NoFilter; + } // Always use consistent source and destination images for proper scaling // When upscaling: source = unscaled (0), destination = scaled (1) @@ -1495,8 +1493,8 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { const vk::Image dst_image = up_scale ? Image(1) : Image(0); scheduler->Record([src_image, aspect = Aspect(), filter = MakeFilter(pixel_format), dst_image, - src_access = AccessFlags(), dst_access = AccessFlags(), blit, - texture_filter_enabled](vk::CommandBuffer render_cmdbuf) { + src_access = AccessFlags(), dst_access = AccessFlags(), + blit](vk::CommandBuffer render_cmdbuf) { // Adjust blitting parameters for filtered upscaling const std::array source_offsets = { vk::Offset3D{static_cast(blit.src_rect.left), @@ -1510,15 +1508,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { static_cast(blit.dst_rect.bottom), 0}, vk::Offset3D{static_cast(blit.dst_rect.right), static_cast(blit.dst_rect.top), 1}, - }; // Ensure we're using the right filter for texture filtered upscaling - vk::Filter actual_filter; - if (texture_filter_enabled) { - // When texture filtering is enabled, always use LINEAR filtering - actual_filter = vk::Filter::eLinear; - } else { - // When texture filtering is disabled, use the filter appropriate for the texture format - actual_filter = filter; - } + }; const vk::ImageBlit blit_area = { .srcSubresource{ @@ -1587,7 +1577,7 @@ void Surface::BlitScale(const VideoCore::TextureBlit& blit, bool up_scale) { vk::DependencyFlagBits::eByRegion, {}, {}, read_barriers); render_cmdbuf.blitImage(src_image, vk::ImageLayout::eTransferSrcOptimal, dst_image, - vk::ImageLayout::eTransferDstOptimal, blit_area, actual_filter); + vk::ImageLayout::eTransferDstOptimal, blit_area, filter); render_cmdbuf.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eAllCommands, From 8c53dcef310b42ffef701d2f79f5854d4dc83394 Mon Sep 17 00:00:00 2001 From: OpenSauce Date: Fri, 2 Jan 2026 23:05:52 +0000 Subject: [PATCH 08/11] renderer_vulkan: Fix incorrect MaxTexelBufferElements return type (#1563) --- src/video_core/renderer_vulkan/vk_instance.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h index d087d49b1..88c8726f4 100644 --- a/src/video_core/renderer_vulkan/vk_instance.h +++ b/src/video_core/renderer_vulkan/vk_instance.h @@ -1,4 +1,4 @@ -// 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. @@ -229,7 +229,7 @@ public: } /// Returns the maximum supported elements in a texel buffer - u32 MaxTexelBufferElements() const { + u64 MaxTexelBufferElements() const { return properties.limits.maxTexelBufferElements; } From b1717b6fc275ed83ca54fac6da5fe10c01677938 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sat, 3 Jan 2026 14:15:49 +0300 Subject: [PATCH 09/11] Android: Keep track of the axis direction when mapping axes (also allows inversion for external pads) (#1483) --- .../citra_emu/activities/EmulationActivity.kt | 3 +++ .../settings/model/view/InputBindingSetting.kt | 15 +++++++++++---- src/android/app/src/main/res/values/strings.xml | 4 ++-- 3 files changed, 16 insertions(+), 6 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 d060c59bb..ca92b308d 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 @@ -342,6 +342,7 @@ class EmulationActivity : AppCompatActivity() { preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1) val guestOrientation = preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1) + val inverted = preferences.getBoolean(InputBindingSetting.getInputAxisInvertedKey(axis),false); if (nextMapping == -1 || guestOrientation == -1) { // Axis is unmapped continue @@ -350,6 +351,8 @@ class EmulationActivity : AppCompatActivity() { // Skip joystick wobble value = 0f } + if (inverted) value = -value; + when (nextMapping) { NativeLibrary.ButtonType.STICK_LEFT -> { axisValuesCirclePad[guestOrientation] = value 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 a2ff73f70..509426537 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 @@ -169,6 +169,7 @@ class InputBindingSetting( .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() } } @@ -202,7 +203,7 @@ class InputBindingSetting( /** * Helper function to write a gamepad axis mapping for the setting. */ - private fun writeAxisMapping(axis: Int, value: Int) { + private fun writeAxisMapping(axis: Int, value: Int, inverted: Boolean) { // Cleanup old mapping removeOldMapping() @@ -210,6 +211,7 @@ class InputBindingSetting( preferences.edit() .putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1) .putInt(getInputAxisButtonKey(axis), value) + .putBoolean(getInputAxisInvertedKey(axis),inverted) // Write next reverse mapping for future cleanup .putString(reverseKey, getInputAxisKey(axis)) .apply() @@ -237,7 +239,7 @@ class InputBindingSetting( * * @param device InputDevice from which the input event originated. * @param motionRange MotionRange of the movement - * @param axisDir Either '-' or '+' (currently unused) + * @param axisDir Either '-' or '+' */ fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) { if (!isAxisMappingSupported()) { @@ -253,8 +255,8 @@ class InputBindingSetting( } else { buttonCode } - writeAxisMapping(motionRange.axis, button) - val uiString = "${device.name}: Axis ${motionRange.axis}" + writeAxisMapping(motionRange.axis, button, axisDir == '-') + val uiString = "${device.name}: Axis ${motionRange.axis}" + axisDir value = uiString } @@ -309,6 +311,11 @@ class InputBindingSetting( */ fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton" + /** + * Helper function to get the settings key for an whether a gamepad axis is inverted. + */ + fun getInputAxisInvertedKey(axis: Int): String = "${getInputAxisKey(axis)}_Inverted" + /** * Helper function to get the settings key for an gamepad axis orientation. */ diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index e861035d5..99c38f608 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -140,8 +140,8 @@ Press or move an input. Input Binding Press or move an input to bind it to %1$s. - Move your joystick up or down. - Move your joystick left or right. + Move your joystick down + Move your joystick right A B SELECT From 48cbeb0993d0b3853d51a2602d6f58c074a17fe1 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sat, 3 Jan 2026 16:09:45 +0300 Subject: [PATCH 10/11] Android: Bug fix for incorrect screen size detection (#1495) --- .../settings/ui/SettingsFragmentPresenter.kt | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) 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 2b823986a..cb41525a1 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 @@ -44,7 +44,6 @@ import org.citra.citra_emu.utils.BirthdayMonth import org.citra.citra_emu.utils.Log import org.citra.citra_emu.utils.SystemSaveGame import org.citra.citra_emu.utils.ThemeUtil -import org.citra.citra_emu.utils.EmulationMenuSettings class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) { private var menuTag: String? = null @@ -111,20 +110,24 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) } /** Returns the portrait mode width */ - private fun getWidth(): Int { - val dm = Resources.getSystem().displayMetrics; - return if (dm.widthPixels < dm.heightPixels) - dm.widthPixels - else - dm.heightPixels + private fun getDimensions(): IntArray { + val dm = Resources.getSystem().displayMetrics + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val wm = settingsActivity.windowManager.maximumWindowMetrics + val height = wm.bounds.height().coerceAtLeast(dm.heightPixels) + val width = wm.bounds.width().coerceAtLeast(dm.widthPixels) + intArrayOf(width, height) + } else { + intArrayOf(dm.widthPixels, dm.heightPixels) + } } - private fun getHeight(): Int { - val dm = Resources.getSystem().displayMetrics; - return if (dm.widthPixels < dm.heightPixels) - dm.heightPixels - else - dm.widthPixels + private fun getSmallerDimension(): Int { + return getDimensions().min() + } + + private fun getLargerDimension(): Int { + return getDimensions().max() } private fun addConfigSettings(sl: ArrayList) { @@ -1405,7 +1408,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_x, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.LANDSCAPE_TOP_X.key, IntSetting.LANDSCAPE_TOP_X.defaultValue.toFloat() @@ -1417,7 +1420,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_y, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.LANDSCAPE_TOP_Y.key, IntSetting.LANDSCAPE_TOP_Y.defaultValue.toFloat() @@ -1429,7 +1432,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_width, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.LANDSCAPE_TOP_WIDTH.key, IntSetting.LANDSCAPE_TOP_WIDTH.defaultValue.toFloat() @@ -1441,7 +1444,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_height, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.LANDSCAPE_TOP_HEIGHT.key, IntSetting.LANDSCAPE_TOP_HEIGHT.defaultValue.toFloat() @@ -1454,7 +1457,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_x, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.LANDSCAPE_BOTTOM_X.key, IntSetting.LANDSCAPE_BOTTOM_X.defaultValue.toFloat() @@ -1466,7 +1469,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_y, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.LANDSCAPE_BOTTOM_Y.key, IntSetting.LANDSCAPE_BOTTOM_Y.defaultValue.toFloat() @@ -1478,7 +1481,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_width, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.LANDSCAPE_BOTTOM_WIDTH.key, IntSetting.LANDSCAPE_BOTTOM_WIDTH.defaultValue.toFloat() @@ -1490,7 +1493,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_height, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.LANDSCAPE_BOTTOM_HEIGHT.key, IntSetting.LANDSCAPE_BOTTOM_HEIGHT.defaultValue.toFloat() @@ -1510,7 +1513,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_x, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.PORTRAIT_TOP_X.key, IntSetting.PORTRAIT_TOP_X.defaultValue.toFloat() @@ -1522,7 +1525,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_y, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.PORTRAIT_TOP_Y.key, IntSetting.PORTRAIT_TOP_Y.defaultValue.toFloat() @@ -1534,7 +1537,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_width, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.PORTRAIT_TOP_WIDTH.key, IntSetting.PORTRAIT_TOP_WIDTH.defaultValue.toFloat() @@ -1546,7 +1549,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_height, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.PORTRAIT_TOP_HEIGHT.key, IntSetting.PORTRAIT_TOP_HEIGHT.defaultValue.toFloat() @@ -1559,7 +1562,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_x, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.PORTRAIT_BOTTOM_X.key, IntSetting.PORTRAIT_BOTTOM_X.defaultValue.toFloat() @@ -1571,7 +1574,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_y, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.PORTRAIT_BOTTOM_Y.key, IntSetting.PORTRAIT_BOTTOM_Y.defaultValue.toFloat() @@ -1583,7 +1586,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_width, 0, 0, - getWidth(), + getSmallerDimension(), "px", IntSetting.PORTRAIT_BOTTOM_WIDTH.key, IntSetting.PORTRAIT_BOTTOM_WIDTH.defaultValue.toFloat() @@ -1595,7 +1598,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.string.emulation_custom_layout_height, 0, 0, - getHeight(), + getLargerDimension(), "px", IntSetting.PORTRAIT_BOTTOM_HEIGHT.key, IntSetting.PORTRAIT_BOTTOM_HEIGHT.defaultValue.toFloat() From 4bdcf342b63e5adff8a1338a34d1ec3ec570b153 Mon Sep 17 00:00:00 2001 From: David Griswold Date: Sat, 3 Jan 2026 23:40:59 +0300 Subject: [PATCH 11/11] video_core: Stereo Settings Improvements: Full Width SBS, Swap Eyes, Select which display(s) to render on (#1212) --- .../citra/citra_emu/display/ScreenLayout.kt | 33 +++++++ .../features/settings/model/BooleanSetting.kt | 4 +- .../features/settings/model/IntSetting.kt | 3 +- .../settings/ui/SettingsFragmentPresenter.kt | 44 +++++++-- src/android/app/src/main/jni/config.cpp | 3 +- src/android/app/src/main/jni/default_ini.h | 12 ++- .../app/src/main/res/values/arrays.xml | 18 +++- .../app/src/main/res/values/strings.xml | 10 +- src/citra_qt/configuration/config.cpp | 4 + .../configuration/configure_enhancements.cpp | 2 + .../configuration/configure_enhancements.ui | 27 +++-- src/citra_sdl/default_ini.h | 4 + src/common/settings.cpp | 4 + src/common/settings.h | 14 ++- src/core/frontend/emu_window.cpp | 93 ++++++++++------- src/core/frontend/emu_window.h | 2 + src/core/frontend/framebuffer_layout.cpp | 99 ++++++++++--------- src/core/frontend/framebuffer_layout.h | 2 + .../renderer_opengl/renderer_opengl.cpp | 98 +++++++++--------- .../renderer_opengl/renderer_opengl.h | 2 +- .../renderer_vulkan/renderer_vulkan.cpp | 79 +++++++-------- .../renderer_vulkan/renderer_vulkan.h | 2 +- 22 files changed, 358 insertions(+), 201 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt index c3877560c..c46dcadd8 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/display/ScreenLayout.kt @@ -63,4 +63,37 @@ enum class SecondaryDisplayLayout(val int: Int) { return entries.firstOrNull { it.int == int } ?: NONE } } +} + +enum class StereoWhichDisplay(val int: Int) { + // These must match what is defined in src/common/settings.h + + NONE(0), // equivalent to StereoRenderOption = Off + BOTH(1), + PRIMARY_ONLY(2), + SECONDARY_ONLY(3); + + companion object { + fun from(int: Int): StereoWhichDisplay { + return entries.firstOrNull { it.int == int } ?: NONE + } + } +} + +enum class StereoMode(val int: Int) { + // These must match what is defined in src/common/settings.h + + OFF(0), + SIDE_BY_SIDE(1), + SIDE_BY_SIDE_FULL(2), + ANAGLYPH(3), + INTERLACED(4), + REVERSE_INTERLACED (5), + CARDBOARD_VR (6); + + companion object { + fun from(int: Int): StereoMode { + return entries.firstOrNull { it.int == int } ?: OFF + } + } } \ No newline at end of file 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 e486a4d96..f06324251 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 @@ -19,6 +19,7 @@ enum class BooleanSetting( INSTANT_DEBUG_LOG("instant_debug_log", Settings.SECTION_DEBUG, false), ENABLE_RPC_SERVER("enable_rpc_server", Settings.SECTION_DEBUG, false), CUSTOM_LAYOUT("custom_layout",Settings.SECTION_LAYOUT,false), + SWAP_EYES_3D("swap_eyes_3d",Settings.SECTION_RENDERER,false), PERF_OVERLAY_ENABLE("performance_overlay_enable", Settings.SECTION_LAYOUT, false), PERF_OVERLAY_SHOW_FPS("performance_overlay_show_fps", Settings.SECTION_LAYOUT, true), PERF_OVERLAY_SHOW_FRAMETIME("performance_overlay_show_frame_time", Settings.SECTION_LAYOUT, false), @@ -87,7 +88,8 @@ enum class BooleanSetting( USE_ARTIC_BASE_CONTROLLER, COMPRESS_INSTALLED_CIA_CONTENT, ANDROID_HIDE_IMAGES, - PERF_OVERLAY_ENABLE // Works in overlay options, but not from the settings menu + PERF_OVERLAY_ENABLE, // Works in overlay options, but not from the settings menu + APPLY_REGION_FREE_PATCH ) fun from(key: String): BooleanSetting? = diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index 5b2016ac9..e26bcd6c6 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -17,7 +17,7 @@ enum class IntSetting( CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0), GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1), RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1), - STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 0), + STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 2), STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0), STEPS_PER_HOUR("steps_per_hour", Settings.SECTION_SYSTEM, 0), CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85), @@ -53,6 +53,7 @@ enum class IntSetting( ORIENTATION_OPTION("screen_orientation", Settings.SECTION_LAYOUT, 2), TURBO_LIMIT("turbo_limit", Settings.SECTION_CORE, 200), PERFORMANCE_OVERLAY_POSITION("performance_overlay_position", Settings.SECTION_LAYOUT, 0), + RENDER_3D_WHICH_DISPLAY("render_3d_which_display",Settings.SECTION_RENDERER,0), ASPECT_RATIO("aspect_ratio", Settings.SECTION_LAYOUT, 0); override var int: Int = defaultValue 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 cb41525a1..1326401d5 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 @@ -16,6 +16,9 @@ import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.citra.citra_emu.CitraApplication import org.citra.citra_emu.R +import org.citra.citra_emu.display.ScreenLayout +import org.citra.citra_emu.display.StereoMode +import org.citra.citra_emu.display.StereoWhichDisplay import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting import org.citra.citra_emu.features.settings.model.AbstractIntSetting import org.citra.citra_emu.features.settings.model.AbstractSetting @@ -948,17 +951,30 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) add(HeaderSetting(R.string.stereoscopy)) + add( + SingleChoiceSetting( + IntSetting.RENDER_3D_WHICH_DISPLAY, + R.string.render_3d_which_display, + R.string.render_3d_which_display_description, + R.array.render3dWhichDisplay, + R.array.render3dDisplayValues, + IntSetting.RENDER_3D_WHICH_DISPLAY.key, + IntSetting.RENDER_3D_WHICH_DISPLAY.defaultValue + ) + ) add( SingleChoiceSetting( IntSetting.STEREOSCOPIC_3D_MODE, R.string.render3d, - 0, + R.string.render3d_description, R.array.render3dModes, R.array.render3dValues, IntSetting.STEREOSCOPIC_3D_MODE.key, - IntSetting.STEREOSCOPIC_3D_MODE.defaultValue + IntSetting.STEREOSCOPIC_3D_MODE.defaultValue, + isEnabled = IntSetting.RENDER_3D_WHICH_DISPLAY.int != StereoWhichDisplay.NONE.int ) ) + add( SliderSetting( IntSetting.STEREOSCOPIC_3D_DEPTH, @@ -981,6 +997,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) ) ) + add( + SwitchSetting( + BooleanSetting.SWAP_EYES_3D, + R.string.swap_eyes_3d, + R.string.swap_eyes_3d_description, + BooleanSetting.SWAP_EYES_3D.key, + BooleanSetting.SWAP_EYES_3D.defaultValue, + isEnabled = IntSetting.RENDER_3D_WHICH_DISPLAY.int != StereoWhichDisplay.NONE.int + ) + ) + add(HeaderSetting(R.string.cardboard_vr)) add( SliderSetting( @@ -991,7 +1018,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) 100, "%", IntSetting.CARDBOARD_SCREEN_SIZE.key, - IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat() + IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat(), + isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int ) ) add( @@ -1003,7 +1031,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) 100, "%", IntSetting.CARDBOARD_X_SHIFT.key, - IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat() + IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat(), + isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int ) ) add( @@ -1015,7 +1044,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) 100, "%", IntSetting.CARDBOARD_Y_SHIFT.key, - IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat() + IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat(), + isEnabled = IntSetting.STEREOSCOPIC_3D_MODE.int == StereoMode.CARDBOARD_VR.int ) ) @@ -1149,7 +1179,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) R.array.aspectRatioValues, IntSetting.ASPECT_RATIO.key, IntSetting.ASPECT_RATIO.defaultValue, - isEnabled = IntSetting.SCREEN_LAYOUT.int == 1, + isEnabled = IntSetting.SCREEN_LAYOUT.int == ScreenLayout.SINGLE_SCREEN.int, ) ) add( @@ -1197,7 +1227,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) "%", FloatSetting.SECOND_SCREEN_OPACITY.key, FloatSetting.SECOND_SCREEN_OPACITY.defaultValue, - isEnabled = IntSetting.SCREEN_LAYOUT.int == 5 + isEnabled = IntSetting.SCREEN_LAYOUT.int == ScreenLayout.CUSTOM_LAYOUT.int ) ) add(HeaderSetting(R.string.bg_color, R.string.bg_color_description)) diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 7cdfe9960..6d310fea6 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -175,7 +175,8 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.custom_second_layer_opacity); ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); ReadSetting("Renderer", Settings::values.disable_right_eye_render); - + ReadSetting("Renderer", Settings::values.swap_eyes_3d); + ReadSetting("Renderer", Settings::values.render_3d_which_display); // Layout // Somewhat inelegant solution to ensure layout value is between 0 and 5 on read // since older config files may have other values diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index c7185f107..08eaf3283 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -170,13 +170,23 @@ bg_green = custom_second_layer_opacity = # 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, 6: Cardboard VR +# 0: Off, 1: Half Width Side by Side, 2 (default): Full Width Side by Side, 3: Anaglyph, 4: Interlaced, 5: Reverse Interlaced, 6: Cardboard VR +# 0 is no longer supported in the interface, as using render_3d_which_display = 0 has the same effect, but supported here for backwards compatibility render_3d = # Change 3D Intensity # 0 - 255: Intensity. 0 (default) factor_3d = +# Swap Eyes in 3d +# true: Swap eyes, false (default): Do not swap eyes +swap_eyes_3d = + +# Which Display to render 3d mode to +# 0 (default) - None. Equivalent to render_3d=0 +# 1: Both, 2: Primary Only, 3: Secondary Only +render_3d_which_display = + # The name of the post processing shader to apply. # Loaded from shaders if render_3d is off or side by side. pp_shader_name = diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 17bda5e66..2a08cd546 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -238,9 +238,8 @@ - @string/off @string/side_by_side - @string/reverse_side_by_side + @string/side_by_side_full @string/anaglyph @string/interlaced @string/reverse_interlaced @@ -248,7 +247,6 @@ - 0 1 2 3 @@ -257,6 +255,20 @@ 6 + + @string/off + @string/render_3d_which_display_both + @string/render_3d_which_display_primary + @string/render_3d_which_display_secondary + + + + 0 + 1 + 2 + 3 + + @string/opengles @string/vulkan diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 99c38f608..529030ed8 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -289,10 +289,18 @@ Warning: Modifying these settings will slow emulation Stereoscopy Stereoscopic 3D Mode + Choose the stereoscopic 3D mode for 3D rendering. Side by Side modes are most common in modern use. Anaglyph and Interlaced modes will always apply to all connected displays. Depth Specifies the value of the 3D slider. This should be set to higher than 0% when Stereoscopic 3D is enabled.\nNote: Depth values over 100% are not possible on real hardware and may cause graphical issues Disable Right Eye Render Greatly improves performance in some applications, but can cause flickering in others. + Swap Eyes + Swaps which eye is shown in which side. Combined with Side by Side mode makes it possible to see 3D by crossing your eyes! + Render Stereoscopic 3D + Whether to enable stereoscopic 3D, and on which displays. The single display options are only relevant when multiple displays are connected. + On (All Displays) + On (Primary Display Only) + On (Secondary Display Only) Cardboard VR Cardboard Screen Size Scales the screen to a percentage of its original size. @@ -723,7 +731,7 @@ Side by Side - Reverse Side by Side + Side by Side Full Width Anaglyph Interlaced Reverse Interlaced diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index b793a6042..4c7d99c93 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -517,6 +517,8 @@ void QtConfig::ReadLayoutValues() { ReadGlobalSetting(Settings::values.render_3d); ReadGlobalSetting(Settings::values.factor_3d); + ReadGlobalSetting(Settings::values.swap_eyes_3d); + ReadGlobalSetting(Settings::values.render_3d_which_display); ReadGlobalSetting(Settings::values.filter_mode); ReadGlobalSetting(Settings::values.pp_shader_name); ReadGlobalSetting(Settings::values.anaglyph_shader_name); @@ -1083,6 +1085,8 @@ void QtConfig::SaveLayoutValues() { WriteGlobalSetting(Settings::values.render_3d); WriteGlobalSetting(Settings::values.factor_3d); + WriteGlobalSetting(Settings::values.swap_eyes_3d); + WriteGlobalSetting(Settings::values.render_3d_which_display); WriteGlobalSetting(Settings::values.filter_mode); WriteGlobalSetting(Settings::values.pp_shader_name); WriteGlobalSetting(Settings::values.anaglyph_shader_name); diff --git a/src/citra_qt/configuration/configure_enhancements.cpp b/src/citra_qt/configuration/configure_enhancements.cpp index a40654a5d..aba7a7ac8 100644 --- a/src/citra_qt/configuration/configure_enhancements.cpp +++ b/src/citra_qt/configuration/configure_enhancements.cpp @@ -57,6 +57,7 @@ void ConfigureEnhancements::SetConfiguration() { ui->render_3d_combobox->setCurrentIndex( static_cast(Settings::values.render_3d.GetValue())); + ui->swap_eyes_3d->setChecked(Settings::values.swap_eyes_3d.GetValue()); ui->factor_3d->setValue(Settings::values.factor_3d.GetValue()); ui->mono_rendering_eye->setCurrentIndex( static_cast(Settings::values.mono_render_option.GetValue())); @@ -111,6 +112,7 @@ void ConfigureEnhancements::ApplyConfiguration() { ui->resolution_factor_combobox); Settings::values.render_3d = static_cast(ui->render_3d_combobox->currentIndex()); + Settings::values.swap_eyes_3d = ui->swap_eyes_3d->isChecked(); Settings::values.factor_3d = ui->factor_3d->value(); Settings::values.mono_render_option = static_cast(ui->mono_rendering_eye->currentIndex()); diff --git a/src/citra_qt/configuration/configure_enhancements.ui b/src/citra_qt/configuration/configure_enhancements.ui index f26943444..ebf8520c4 100644 --- a/src/citra_qt/configuration/configure_enhancements.ui +++ b/src/citra_qt/configuration/configure_enhancements.ui @@ -113,7 +113,7 @@ - Enable linear filtering + Enable Linear Filtering @@ -236,7 +236,7 @@ - Reverse Side by Side + Side by Side Full Width @@ -318,14 +318,21 @@ - - - Disable right eye rendering - - - <html><head/><body><p>Disable Right Eye Rendering</p><p>Disables rendering the right eye image when not using stereoscopic mode. Greatly improves performance in some applications, but can cause flickering in others.</p></body></html> - - + + + Disable Right Eye Rendering + + + <html><head/><body><p>Disable Right Eye Rendering</p><p>Disables rendering the right eye image when not using stereoscopic mode. Greatly improves performance in some applications, but can cause flickering in others.</p></body></html> + + + + + + + Swap Eyes + + diff --git a/src/citra_sdl/default_ini.h b/src/citra_sdl/default_ini.h index 3fb471449..4c97dbb89 100644 --- a/src/citra_sdl/default_ini.h +++ b/src/citra_sdl/default_ini.h @@ -163,6 +163,10 @@ render_3d = # 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 = diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 0af00c2bf..564090d0b 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -108,6 +108,9 @@ void LogSettings() { log_setting("Renderer_DisableRightEyeRender", values.disable_right_eye_render.GetValue()); log_setting("Stereoscopy_Render3d", values.render_3d.GetValue()); log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue()); + log_setting("Stereoscopy_Swap_Eyes", values.swap_eyes_3d.GetValue()); + log_setting("Stereoscopy_Render_3d_to_which_display", + values.render_3d_which_display.GetValue()); log_setting("Stereoscopy_MonoRenderOption", values.mono_render_option.GetValue()); if (values.render_3d.GetValue() == StereoRenderOption::Anaglyph) { log_setting("Renderer_AnaglyphShader", values.anaglyph_shader_name.GetValue()); @@ -219,6 +222,7 @@ void RestoreGlobalState(bool is_powered_on) { values.bg_green.SetGlobal(true); values.bg_blue.SetGlobal(true); values.render_3d.SetGlobal(true); + values.swap_eyes_3d.SetGlobal(true); values.factor_3d.SetGlobal(true); values.filter_mode.SetGlobal(true); values.pp_shader_name.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index ee65c984e..a85ad3c0a 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -72,7 +72,7 @@ enum class SmallScreenPosition : u32 { enum class StereoRenderOption : u32 { Off = 0, SideBySide = 1, - ReverseSideBySide = 2, + SideBySideFull = 2, Anaglyph = 3, Interlaced = 4, ReverseInterlaced = 5, @@ -86,6 +86,14 @@ enum class MonoRenderOption : u32 { RightEye = 1, }; +// on android, which displays to render stereo mode to +enum class StereoWhichDisplay : u32 { + None = 0, // equivalent to StereoRenderOption = Off + Both = 1, + PrimaryOnly = 2, + SecondaryOnly = 3 +}; + enum class AudioEmulation : u32 { HLE = 0, LLE = 1, @@ -562,6 +570,10 @@ struct Values { SwitchableSetting render_3d{StereoRenderOption::Off, "render_3d"}; SwitchableSetting factor_3d{0, "factor_3d"}; + SwitchableSetting swap_eyes_3d{false, "swap_eyes_3d"}; + + SwitchableSetting render_3d_which_display{StereoWhichDisplay::None, + "render_3d_which_display"}; SwitchableSetting mono_render_option{MonoRenderOption::LeftEye, "mono_render_option"}; diff --git a/src/core/frontend/emu_window.cpp b/src/core/frontend/emu_window.cpp index ee81b177e..3216a1675 100644 --- a/src/core/frontend/emu_window.cpp +++ b/src/core/frontend/emu_window.cpp @@ -25,9 +25,8 @@ public: std::mutex mutex; bool touch_pressed = false; ///< True if touchpad area is currently pressed, otherwise false - - float touch_x = 0.0f; ///< Touchpad X-position - float touch_y = 0.0f; ///< Touchpad Y-position + float touch_x = 0.0f; ///< Touchpad X-position + float touch_y = 0.0f; ///< Touchpad Y-position private: class Device : public Input::TouchDevice { @@ -56,24 +55,44 @@ EmuWindow::EmuWindow(bool is_secondary_) : is_secondary{is_secondary_} { EmuWindow::~EmuWindow() = default; +Settings::StereoRenderOption EmuWindow::get3DMode() const { + Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); +#ifndef ANDROID + // on desktop, if separate windows and this is the bottom screen, then no stereo + if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows && + ((!is_secondary && Settings::values.swap_screen.GetValue()) || + (is_secondary && !Settings::values.swap_screen.GetValue()))) { + render_3d_mode = Settings::StereoRenderOption::Off; + } +#else + // adjust the StereoRenderOption setting to Off if appropriate on mobile + Settings::StereoWhichDisplay whichDisplay = Settings::values.render_3d_which_display.GetValue(); + if (whichDisplay == Settings::StereoWhichDisplay::None || + whichDisplay == Settings::StereoWhichDisplay::PrimaryOnly && is_secondary || + whichDisplay == Settings::StereoWhichDisplay::SecondaryOnly && !is_secondary) { + render_3d_mode = Settings::StereoRenderOption::Off; + } +#endif + return render_3d_mode; +} + bool EmuWindow::IsWithinTouchscreen(const Layout::FramebufferLayout& layout, unsigned framebuffer_x, unsigned framebuffer_y) { #ifndef ANDROID // If separate windows and the touch is in the primary (top) screen, ignore it. if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows && - !is_secondary && !Settings::values.swap_screen.GetValue()) { + ((!is_secondary && !Settings::values.swap_screen.GetValue()) || + (is_secondary && Settings::values.swap_screen.GetValue()))) { return false; } #endif + Settings::StereoRenderOption render_3d_mode = get3DMode(); - if (!layout.bottom_screen_enabled) { - return false; + if (framebuffer_x > layout.width / 2 && + render_3d_mode == Settings::StereoRenderOption::SideBySideFull) { + framebuffer_x = static_cast(framebuffer_x - layout.width / 2); } - - Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); - - if (render_3d_mode == Settings::StereoRenderOption::SideBySide || - render_3d_mode == Settings::StereoRenderOption::ReverseSideBySide) { + if (render_3d_mode == Settings::StereoRenderOption::SideBySide) { return (framebuffer_y >= layout.bottom_screen.top && framebuffer_y < layout.bottom_screen.bottom && ((framebuffer_x >= layout.bottom_screen.left / 2 && @@ -97,25 +116,19 @@ bool EmuWindow::IsWithinTouchscreen(const Layout::FramebufferLayout& layout, uns } std::tuple EmuWindow::ClipToTouchScreen(unsigned new_x, unsigned new_y) const { - Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); - bool separate_win = false; -#ifndef ANDROID - separate_win = - (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); -#endif + + Settings::StereoRenderOption render_3d_mode = get3DMode(); if (new_x >= framebuffer_layout.width / 2) { - if ((render_3d_mode == Settings::StereoRenderOption::SideBySide || - render_3d_mode == Settings::StereoRenderOption::ReverseSideBySide) && - !separate_win) + if (render_3d_mode == Settings::StereoRenderOption::SideBySide || + render_3d_mode == Settings::StereoRenderOption::SideBySideFull) new_x -= framebuffer_layout.width / 2; else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) new_x -= (framebuffer_layout.width / 2) - (framebuffer_layout.cardboard.user_x_shift * 2); } - if ((render_3d_mode == Settings::StereoRenderOption::SideBySide || - render_3d_mode == Settings::StereoRenderOption::ReverseSideBySide) && - !separate_win) { + + if (render_3d_mode == Settings::StereoRenderOption::SideBySide) { new_x = std::max(new_x, framebuffer_layout.bottom_screen.left / 2); new_x = std::min(new_x, framebuffer_layout.bottom_screen.right / 2 - 1); } else { @@ -140,29 +153,22 @@ void EmuWindow::CreateTouchState() { } bool EmuWindow::TouchPressed(unsigned framebuffer_x, unsigned framebuffer_y) { - Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); - bool separate_win = false; -#ifndef ANDROID - separate_win = - (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); -#endif - if (!IsWithinTouchscreen(framebuffer_layout, framebuffer_x, framebuffer_y)) return false; + Settings::StereoRenderOption render_3d_mode = get3DMode(); if (framebuffer_x >= framebuffer_layout.width / 2) { - if ((render_3d_mode == Settings::StereoRenderOption::SideBySide || - render_3d_mode == Settings::StereoRenderOption::ReverseSideBySide) && - !separate_win) + if (render_3d_mode == Settings::StereoRenderOption::SideBySide || + render_3d_mode == Settings::StereoRenderOption::SideBySideFull) framebuffer_x -= framebuffer_layout.width / 2; else if (render_3d_mode == Settings::StereoRenderOption::CardboardVR) framebuffer_x -= (framebuffer_layout.width / 2) - (framebuffer_layout.cardboard.user_x_shift * 2); } + std::scoped_lock guard(touch_state->mutex); - if ((render_3d_mode == Settings::StereoRenderOption::SideBySide || - render_3d_mode == Settings::StereoRenderOption::ReverseSideBySide) && - !separate_win) { + + if (render_3d_mode == Settings::StereoRenderOption::SideBySide) { touch_state->touch_x = static_cast(framebuffer_x - framebuffer_layout.bottom_screen.left / 2) / (framebuffer_layout.bottom_screen.right / 2 - @@ -204,8 +210,13 @@ void EmuWindow::TouchMoved(unsigned framebuffer_x, unsigned framebuffer_y) { void EmuWindow::UpdateCurrentFramebufferLayout(u32 width, u32 height, bool is_portrait_mode) { Layout::FramebufferLayout layout; - const Settings::LayoutOption layout_option = Settings::values.layout_option.GetValue(); + const Settings::StereoRenderOption stereo_option = get3DMode(); + bool render_full_stereo = (stereo_option == Settings::StereoRenderOption::SideBySideFull); + bool is_bottom = is_secondary; + if (Settings::values.swap_screen.GetValue()) + is_bottom = !is_bottom; + const Settings::PortraitLayoutOption portrait_layout_option = Settings::values.portrait_layout_option.GetValue(); const auto min_size = is_portrait_mode @@ -215,6 +226,9 @@ void EmuWindow::UpdateCurrentFramebufferLayout(u32 width, u32 height, bool is_po width = std::max(width, min_size.first); height = std::max(height, min_size.second); + if (render_full_stereo) { + width = width / 2; + } if (is_portrait_mode) { switch (portrait_layout_option) { case Settings::PortraitLayoutOption::PortraitTopFullWidth: @@ -280,11 +294,16 @@ void EmuWindow::UpdateCurrentFramebufferLayout(u32 width, u32 height, bool is_po layout = Layout::AndroidSecondaryLayout(width, height); } #endif + + if (render_full_stereo) { + layout.width = width * 2; + } UpdateMinimumWindowSize(min_size); if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::CardboardVR) { layout = Layout::GetCardboardSettings(layout); } + layout.render_3d_mode = stereo_option; NotifyFramebufferLayoutChanged(layout); } diff --git a/src/core/frontend/emu_window.h b/src/core/frontend/emu_window.h index f217a7eac..d5b7803cc 100644 --- a/src/core/frontend/emu_window.h +++ b/src/core/frontend/emu_window.h @@ -258,6 +258,8 @@ public: return is_secondary; } + Settings::StereoRenderOption get3DMode() const; + protected: EmuWindow(); EmuWindow(bool is_secondary); diff --git a/src/core/frontend/framebuffer_layout.cpp b/src/core/frontend/framebuffer_layout.cpp index c64a5afb1..392db8867 100644 --- a/src/core/frontend/framebuffer_layout.cpp +++ b/src/core/frontend/framebuffer_layout.cpp @@ -79,7 +79,7 @@ FramebufferLayout SingleFrameLayout(u32 width, u32 height, bool swapped, bool up // TODO: This is kind of gross, make it platform agnostic. -OS #ifdef ANDROID - const float window_aspect_ratio = static_cast(height) / static_cast(width); + const float window_aspect_ratio = static_cast(height) / width; const auto aspect_ratio_setting = Settings::values.aspect_ratio.GetValue(); float emulation_aspect_ratio = (swapped) ? BOT_SCREEN_ASPECT_RATIO : TOP_SCREEN_ASPECT_RATIO; @@ -413,53 +413,56 @@ FramebufferLayout FrameLayoutFromResolutionScale(u32 res_scale, bool is_secondar bool is_portrait) { u32 width, height, gap; gap = (int)(Settings::values.screen_gap.GetValue()) * res_scale; + + FramebufferLayout layout; if (is_portrait) { auto layout_option = Settings::values.portrait_layout_option.GetValue(); switch (layout_option) { case Settings::PortraitLayoutOption::PortraitCustomLayout: - return CustomFrameLayout( - std::max(Settings::values.custom_portrait_top_x.GetValue() + - Settings::values.custom_portrait_top_width.GetValue(), - Settings::values.custom_portrait_bottom_x.GetValue() + - Settings::values.custom_portrait_bottom_width.GetValue()), - std::max(Settings::values.custom_portrait_top_y.GetValue() + - Settings::values.custom_portrait_top_height.GetValue(), - Settings::values.custom_portrait_bottom_y.GetValue() + - Settings::values.custom_portrait_bottom_height.GetValue()), - Settings::values.swap_screen.GetValue(), is_portrait); + width = std::max(Settings::values.custom_portrait_top_x.GetValue() + + Settings::values.custom_portrait_top_width.GetValue(), + Settings::values.custom_portrait_bottom_x.GetValue() + + Settings::values.custom_portrait_bottom_width.GetValue()); + height = std::max(Settings::values.custom_portrait_top_y.GetValue() + + Settings::values.custom_portrait_top_height.GetValue(), + Settings::values.custom_portrait_bottom_y.GetValue() + + Settings::values.custom_portrait_bottom_height.GetValue()); + layout = CustomFrameLayout(width, height, Settings::values.swap_screen.GetValue(), + is_portrait); + + break; case Settings::PortraitLayoutOption::PortraitTopFullWidth: width = Core::kScreenTopWidth * res_scale; // clang-format off height = (static_cast(Core::kScreenTopHeight + Core::kScreenBottomHeight * 1.25) * res_scale) + gap; // clang-format on - return PortraitTopFullFrameLayout(width, height, - Settings::values.swap_screen.GetValue(), - Settings::values.upright_screen.GetValue()); + layout = + PortraitTopFullFrameLayout(width, height, Settings::values.swap_screen.GetValue(), + Settings::values.upright_screen.GetValue()); + break; case Settings::PortraitLayoutOption::PortraitOriginal: width = Core::kScreenTopWidth * res_scale; height = (Core::kScreenTopHeight + Core::kScreenBottomHeight) * res_scale; - return PortraitOriginalLayout(width, height, Settings::values.swap_screen.GetValue()); + layout = PortraitOriginalLayout(width, height, Settings::values.swap_screen.GetValue()); + break; } } else { auto layout_option = Settings::values.layout_option.GetValue(); switch (layout_option) { case Settings::LayoutOption::CustomLayout: - return CustomFrameLayout(std::max(Settings::values.custom_top_x.GetValue() + - Settings::values.custom_top_width.GetValue(), - Settings::values.custom_bottom_x.GetValue() + - Settings::values.custom_bottom_width.GetValue()), - std::max(Settings::values.custom_top_y.GetValue() + - Settings::values.custom_top_height.GetValue(), - Settings::values.custom_bottom_y.GetValue() + - Settings::values.custom_bottom_height.GetValue()), - Settings::values.swap_screen.GetValue(), is_portrait); - - case Settings::LayoutOption::SingleScreen: -#ifndef ANDROID - case Settings::LayoutOption::SeparateWindows: -#endif - { + layout = + CustomFrameLayout(std::max(Settings::values.custom_top_x.GetValue() + + Settings::values.custom_top_width.GetValue(), + Settings::values.custom_bottom_x.GetValue() + + Settings::values.custom_bottom_width.GetValue()), + std::max(Settings::values.custom_top_y.GetValue() + + Settings::values.custom_top_height.GetValue(), + Settings::values.custom_bottom_y.GetValue() + + Settings::values.custom_bottom_height.GetValue()), + Settings::values.swap_screen.GetValue(), is_portrait); + break; + case Settings::LayoutOption::SingleScreen: { const bool swap_screens = is_secondary || Settings::values.swap_screen.GetValue(); if (swap_screens) { width = Core::kScreenBottomWidth * res_scale; @@ -471,8 +474,10 @@ FramebufferLayout FrameLayoutFromResolutionScale(u32 res_scale, bool is_secondar if (Settings::values.upright_screen.GetValue()) { std::swap(width, height); } - return SingleFrameLayout(width, height, swap_screens, - Settings::values.upright_screen.GetValue()); + + layout = SingleFrameLayout(width, height, swap_screens, + Settings::values.upright_screen.GetValue()); + break; } case Settings::LayoutOption::LargeScreen: { @@ -501,10 +506,11 @@ FramebufferLayout FrameLayoutFromResolutionScale(u32 res_scale, bool is_secondar if (Settings::values.upright_screen.GetValue()) { std::swap(width, height); } - return LargeFrameLayout(width, height, Settings::values.swap_screen.GetValue(), - Settings::values.upright_screen.GetValue(), - Settings::values.large_screen_proportion.GetValue(), - Settings::values.small_screen_position.GetValue()); + layout = LargeFrameLayout(width, height, Settings::values.swap_screen.GetValue(), + Settings::values.upright_screen.GetValue(), + Settings::values.large_screen_proportion.GetValue(), + Settings::values.small_screen_position.GetValue()); + break; } case Settings::LayoutOption::SideScreen: width = (Core::kScreenTopWidth + Core::kScreenBottomWidth) * res_scale + gap; @@ -513,10 +519,10 @@ FramebufferLayout FrameLayoutFromResolutionScale(u32 res_scale, bool is_secondar if (Settings::values.upright_screen.GetValue()) { std::swap(width, height); } - return LargeFrameLayout(width, height, Settings::values.swap_screen.GetValue(), - Settings::values.upright_screen.GetValue(), 1, - Settings::SmallScreenPosition::MiddleRight); - + layout = LargeFrameLayout(width, height, Settings::values.swap_screen.GetValue(), + Settings::values.upright_screen.GetValue(), 1, + Settings::SmallScreenPosition::MiddleRight); + break; case Settings::LayoutOption::HybridScreen: height = Core::kScreenTopHeight * res_scale; @@ -532,9 +538,9 @@ FramebufferLayout FrameLayoutFromResolutionScale(u32 res_scale, bool is_secondar std::swap(width, height); } - return HybridScreenLayout(width, height, Settings::values.swap_screen.GetValue(), - Settings::values.upright_screen.GetValue()); - + layout = HybridScreenLayout(width, height, Settings::values.swap_screen.GetValue(), + Settings::values.upright_screen.GetValue()); + break; case Settings::LayoutOption::Default: default: width = Core::kScreenTopWidth * res_scale; @@ -543,10 +549,13 @@ FramebufferLayout FrameLayoutFromResolutionScale(u32 res_scale, bool is_secondar if (Settings::values.upright_screen.GetValue()) { std::swap(width, height); } - return DefaultFrameLayout(width, height, Settings::values.swap_screen.GetValue(), - Settings::values.upright_screen.GetValue()); + layout = DefaultFrameLayout(width, height, Settings::values.swap_screen.GetValue(), + Settings::values.upright_screen.GetValue()); + break; } } + + return layout; UNREACHABLE(); } diff --git a/src/core/frontend/framebuffer_layout.h b/src/core/frontend/framebuffer_layout.h index 8fcb56549..1d15e2c89 100644 --- a/src/core/frontend/framebuffer_layout.h +++ b/src/core/frontend/framebuffer_layout.h @@ -48,6 +48,8 @@ struct FramebufferLayout { u32 GetScalingRatio() const; static float GetAspectRatioValue(Settings::AspectRatio aspect_ratio); + + Settings::StereoRenderOption render_3d_mode = Settings::values.render_3d.GetValue(); }; /** diff --git a/src/video_core/renderer_opengl/renderer_opengl.cpp b/src/video_core/renderer_opengl/renderer_opengl.cpp index e8ec1682c..967fd21fa 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.cpp +++ b/src/video_core/renderer_opengl/renderer_opengl.cpp @@ -326,7 +326,7 @@ void RendererOpenGL::InitOpenGLObjects() { glSamplerParameteri(samplers[i].handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } - ReloadShader(); + ReloadShader(Settings::values.render_3d.GetValue()); // Generate VBO handle for drawing vertex_buffer.Create(); @@ -372,10 +372,10 @@ void RendererOpenGL::InitOpenGLObjects() { state.Apply(); } -void RendererOpenGL::ReloadShader() { +void RendererOpenGL::ReloadShader(Settings::StereoRenderOption render_3d) { // Link shaders and get variable locations std::string shader_data = fragment_shader_precision_OES; - if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph) { + if (render_3d == Settings::StereoRenderOption::Anaglyph) { if (Settings::values.anaglyph_shader_name.GetValue() == "Dubois (builtin)") { shader_data += HostShaders::OPENGL_PRESENT_ANAGLYPH_FRAG; } else { @@ -388,9 +388,8 @@ void RendererOpenGL::ReloadShader() { shader_data += shader_text; } } - } else if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Interlaced || - Settings::values.render_3d.GetValue() == - Settings::StereoRenderOption::ReverseInterlaced) { + } else if (render_3d == Settings::StereoRenderOption::Interlaced || + render_3d == Settings::StereoRenderOption::ReverseInterlaced) { shader_data += HostShaders::OPENGL_PRESENT_INTERLACED_FRAG; } else { if (Settings::values.pp_shader_name.GetValue() == "None (builtin)") { @@ -411,17 +410,16 @@ void RendererOpenGL::ReloadShader() { state.Apply(); uniform_modelview_matrix = glGetUniformLocation(shader.handle, "modelview_matrix"); uniform_color_texture = glGetUniformLocation(shader.handle, "color_texture"); - if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph || - Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Interlaced || - Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::ReverseInterlaced) { + if (render_3d == Settings::StereoRenderOption::Anaglyph || + render_3d == Settings::StereoRenderOption::Interlaced || + render_3d == Settings::StereoRenderOption::ReverseInterlaced) { uniform_color_texture_r = glGetUniformLocation(shader.handle, "color_texture_r"); } - if (Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Interlaced || - Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::ReverseInterlaced) { + if (render_3d == Settings::StereoRenderOption::Interlaced || + render_3d == Settings::StereoRenderOption::ReverseInterlaced) { GLuint uniform_reverse_interlaced = glGetUniformLocation(shader.handle, "reverse_interlaced"); - if (Settings::values.render_3d.GetValue() == - Settings::StereoRenderOption::ReverseInterlaced) + if (render_3d == Settings::StereoRenderOption::ReverseInterlaced) glUniform1i(uniform_reverse_interlaced, 1); else glUniform1i(uniform_reverse_interlaced, 0); @@ -658,7 +656,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f // Update fragment shader before drawing shader.Release(); // Link shaders and get variable locations - ReloadShader(); + ReloadShader(layout.render_3d_mode); } const auto& top_screen = layout.top_screen; @@ -676,9 +674,9 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f glUniform1i(uniform_color_texture, 0); const bool stereo_single_screen = - Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Anaglyph || - Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::Interlaced || - Settings::values.render_3d.GetValue() == Settings::StereoRenderOption::ReverseInterlaced; + layout.render_3d_mode == Settings::StereoRenderOption::Anaglyph || + layout.render_3d_mode == Settings::StereoRenderOption::Interlaced || + layout.render_3d_mode == Settings::StereoRenderOption::ReverseInterlaced; // Bind a second texture for the right eye if in Anaglyph mode if (stereo_single_screen) { @@ -730,6 +728,9 @@ void RendererOpenGL::DrawTopScreen(const Layout::FramebufferLayout& layout, if (!layout.top_screen_enabled) { return; } + int leftside, rightside; + leftside = Settings::values.swap_eyes_3d.GetValue() ? 1 : 0; + rightside = Settings::values.swap_eyes_3d.GetValue() ? 0 : 1; const float top_screen_left = static_cast(top_screen.left); const float top_screen_top = static_cast(top_screen.top); @@ -738,7 +739,7 @@ void RendererOpenGL::DrawTopScreen(const Layout::FramebufferLayout& layout, const auto orientation = layout.is_rotated ? Layout::DisplayOrientation::Landscape : Layout::DisplayOrientation::Portrait; - switch (Settings::values.render_3d.GetValue()) { + switch (layout.render_3d_mode) { case Settings::StereoRenderOption::Off: { const int eye = static_cast(Settings::values.mono_render_option.GetValue()); DrawSingleScreen(screen_infos[eye], top_screen_left, top_screen_top, top_screen_width, @@ -746,29 +747,29 @@ void RendererOpenGL::DrawTopScreen(const Layout::FramebufferLayout& layout, break; } case Settings::StereoRenderOption::SideBySide: { - DrawSingleScreen(screen_infos[0], top_screen_left / 2, top_screen_top, top_screen_width / 2, - top_screen_height, orientation); + DrawSingleScreen(screen_infos[leftside], top_screen_left / 2, top_screen_top, + top_screen_width / 2, top_screen_height, orientation); glUniform1i(uniform_layer, 1); - DrawSingleScreen(screen_infos[1], + DrawSingleScreen(screen_infos[rightside], static_cast((top_screen_left / 2) + (layout.width / 2)), top_screen_top, top_screen_width / 2, top_screen_height, orientation); break; } - case Settings::StereoRenderOption::ReverseSideBySide: { - DrawSingleScreen(screen_infos[1], top_screen_left / 2, top_screen_top, top_screen_width / 2, + case Settings::StereoRenderOption::SideBySideFull: { + DrawSingleScreen(screen_infos[leftside], top_screen_left, top_screen_top, top_screen_width, top_screen_height, orientation); glUniform1i(uniform_layer, 1); - DrawSingleScreen(screen_infos[0], - static_cast((top_screen_left / 2) + (layout.width / 2)), - top_screen_top, top_screen_width / 2, top_screen_height, orientation); + DrawSingleScreen(screen_infos[rightside], + static_cast(top_screen_left + layout.width / 2), top_screen_top, + top_screen_width, top_screen_height, orientation); break; } case Settings::StereoRenderOption::CardboardVR: { - DrawSingleScreen(screen_infos[0], top_screen_left, top_screen_top, top_screen_width, + DrawSingleScreen(screen_infos[leftside], top_screen_left, top_screen_top, top_screen_width, top_screen_height, orientation); glUniform1i(uniform_layer, 1); DrawSingleScreen( - screen_infos[1], + screen_infos[rightside], static_cast(layout.cardboard.top_screen_right_eye + (layout.width / 2)), top_screen_top, top_screen_width, top_screen_height, orientation); break; @@ -776,8 +777,8 @@ void RendererOpenGL::DrawTopScreen(const Layout::FramebufferLayout& layout, case Settings::StereoRenderOption::Anaglyph: case Settings::StereoRenderOption::Interlaced: case Settings::StereoRenderOption::ReverseInterlaced: { - DrawSingleScreenStereo(screen_infos[0], screen_infos[1], top_screen_left, top_screen_top, - top_screen_width, top_screen_height, orientation); + DrawSingleScreenStereo(screen_infos[leftside], screen_infos[rightside], top_screen_left, + top_screen_top, top_screen_width, top_screen_height, orientation); break; } } @@ -797,31 +798,30 @@ void RendererOpenGL::DrawBottomScreen(const Layout::FramebufferLayout& layout, const auto orientation = layout.is_rotated ? Layout::DisplayOrientation::Landscape : Layout::DisplayOrientation::Portrait; - bool separate_win = false; -#ifndef ANDROID - separate_win = - (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); -#endif - - switch (Settings::values.render_3d.GetValue()) { + switch (layout.render_3d_mode) { case Settings::StereoRenderOption::Off: { DrawSingleScreen(screen_infos[2], bottom_screen_left, bottom_screen_top, bottom_screen_width, bottom_screen_height, orientation); break; } case Settings::StereoRenderOption::SideBySide: // Bottom screen is identical on both sides - case Settings::StereoRenderOption::ReverseSideBySide: { - if (separate_win) { - DrawSingleScreen(screen_infos[2], bottom_screen_left, bottom_screen_top, - bottom_screen_width, bottom_screen_height, orientation); - } else { - DrawSingleScreen(screen_infos[2], bottom_screen_left / 2, bottom_screen_top, - bottom_screen_width / 2, bottom_screen_height, orientation); - glUniform1i(uniform_layer, 1); - DrawSingleScreen( - screen_infos[2], static_cast((bottom_screen_left / 2) + (layout.width / 2)), - bottom_screen_top, bottom_screen_width / 2, bottom_screen_height, orientation); - } + { + + DrawSingleScreen(screen_infos[2], bottom_screen_left / 2, bottom_screen_top, + bottom_screen_width / 2, bottom_screen_height, orientation); + glUniform1i(uniform_layer, 1); + DrawSingleScreen( + screen_infos[2], static_cast((bottom_screen_left / 2) + (layout.width / 2)), + bottom_screen_top, bottom_screen_width / 2, bottom_screen_height, orientation); + + break; + } + case Settings::StereoRenderOption::SideBySideFull: { + DrawSingleScreen(screen_infos[2], bottom_screen_left, bottom_screen_top, + bottom_screen_width, bottom_screen_height, orientation); + glUniform1i(uniform_layer, 1); + DrawSingleScreen(screen_infos[2], bottom_screen_left + layout.width / 2, bottom_screen_top, + bottom_screen_width, bottom_screen_height, orientation); break; } case Settings::StereoRenderOption::CardboardVR: { diff --git a/src/video_core/renderer_opengl/renderer_opengl.h b/src/video_core/renderer_opengl/renderer_opengl.h index c37e3833f..2f2b318ed 100644 --- a/src/video_core/renderer_opengl/renderer_opengl.h +++ b/src/video_core/renderer_opengl/renderer_opengl.h @@ -56,7 +56,7 @@ public: private: void InitOpenGLObjects(); - void ReloadShader(); + void ReloadShader(Settings::StereoRenderOption render_3d); void PrepareRendertarget(); void RenderScreenshot(); void RenderToMailbox(const Layout::FramebufferLayout& layout, diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index ceb2e2f3a..51f5b2c7f 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -575,8 +575,7 @@ void RendererVulkan::FillScreen(Common::Vec3 color, const TextureInfo& textu }); } -void RendererVulkan::ReloadPipeline() { - const Settings::StereoRenderOption render_3d = Settings::values.render_3d.GetValue(); +void RendererVulkan::ReloadPipeline(Settings::StereoRenderOption render_3d) { switch (render_3d) { case Settings::StereoRenderOption::Anaglyph: current_pipeline = 1; @@ -748,7 +747,9 @@ void RendererVulkan::DrawTopScreen(const Layout::FramebufferLayout& layout, if (!layout.top_screen_enabled) { return; } - + int leftside, rightside; + leftside = Settings::values.swap_eyes_3d.GetValue() ? 1 : 0; + rightside = Settings::values.swap_eyes_3d.GetValue() ? 0 : 1; const float top_screen_left = static_cast(top_screen.left); const float top_screen_top = static_cast(top_screen.top); const float top_screen_width = static_cast(top_screen.GetWidth()); @@ -756,7 +757,7 @@ void RendererVulkan::DrawTopScreen(const Layout::FramebufferLayout& layout, const auto orientation = layout.is_rotated ? Layout::DisplayOrientation::Landscape : Layout::DisplayOrientation::Portrait; - switch (Settings::values.render_3d.GetValue()) { + switch (layout.render_3d_mode) { case Settings::StereoRenderOption::Off: { const int eye = static_cast(Settings::values.mono_render_option.GetValue()); DrawSingleScreen(eye, top_screen_left, top_screen_top, top_screen_width, top_screen_height, @@ -764,35 +765,36 @@ void RendererVulkan::DrawTopScreen(const Layout::FramebufferLayout& layout, break; } case Settings::StereoRenderOption::SideBySide: { - DrawSingleScreen(0, top_screen_left / 2, top_screen_top, top_screen_width / 2, + DrawSingleScreen(leftside, top_screen_left / 2, top_screen_top, top_screen_width / 2, top_screen_height, orientation); draw_info.layer = 1; - DrawSingleScreen(1, static_cast((top_screen_left / 2) + (layout.width / 2)), + DrawSingleScreen(rightside, static_cast((top_screen_left / 2) + (layout.width / 2)), top_screen_top, top_screen_width / 2, top_screen_height, orientation); break; } - case Settings::StereoRenderOption::ReverseSideBySide: { - DrawSingleScreen(1, top_screen_left / 2, top_screen_top, top_screen_width / 2, + case Settings::StereoRenderOption::SideBySideFull: { + DrawSingleScreen(leftside, top_screen_left, top_screen_top, top_screen_width, top_screen_height, orientation); draw_info.layer = 1; - DrawSingleScreen(0, static_cast((top_screen_left / 2) + (layout.width / 2)), - top_screen_top, top_screen_width / 2, top_screen_height, orientation); + DrawSingleScreen(rightside, top_screen_left + layout.width / 2, top_screen_top, + top_screen_width, top_screen_height, orientation); break; } case Settings::StereoRenderOption::CardboardVR: { - DrawSingleScreen(0, top_screen_left, top_screen_top, top_screen_width, top_screen_height, - orientation); + DrawSingleScreen(leftside, top_screen_left, top_screen_top, top_screen_width, + top_screen_height, orientation); draw_info.layer = 1; DrawSingleScreen( - 1, static_cast(layout.cardboard.top_screen_right_eye + (layout.width / 2)), + rightside, + static_cast(layout.cardboard.top_screen_right_eye + (layout.width / 2)), top_screen_top, top_screen_width, top_screen_height, orientation); break; } case Settings::StereoRenderOption::Anaglyph: case Settings::StereoRenderOption::Interlaced: case Settings::StereoRenderOption::ReverseInterlaced: { - DrawSingleScreenStereo(0, 1, top_screen_left, top_screen_top, top_screen_width, - top_screen_height, orientation); + DrawSingleScreenStereo(leftside, rightside, top_screen_left, top_screen_top, + top_screen_width, top_screen_height, orientation); break; } } @@ -812,31 +814,29 @@ void RendererVulkan::DrawBottomScreen(const Layout::FramebufferLayout& layout, const auto orientation = layout.is_rotated ? Layout::DisplayOrientation::Landscape : Layout::DisplayOrientation::Portrait; - bool separate_win = false; -#ifndef ANDROID - separate_win = - (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows); -#endif - - switch (Settings::values.render_3d.GetValue()) { + switch (layout.render_3d_mode) { case Settings::StereoRenderOption::Off: { DrawSingleScreen(2, bottom_screen_left, bottom_screen_top, bottom_screen_width, bottom_screen_height, orientation); + break; } case Settings::StereoRenderOption::SideBySide: // Bottom screen is identical on both sides - case Settings::StereoRenderOption::ReverseSideBySide: { - if (separate_win) { - DrawSingleScreen(2, bottom_screen_left, bottom_screen_top, bottom_screen_width, - bottom_screen_height, orientation); - } else { - DrawSingleScreen(2, bottom_screen_left / 2, bottom_screen_top, bottom_screen_width / 2, - bottom_screen_height, orientation); - draw_info.layer = 1; - DrawSingleScreen(2, static_cast((bottom_screen_left / 2) + (layout.width / 2)), - bottom_screen_top, bottom_screen_width / 2, bottom_screen_height, - orientation); - } + { + DrawSingleScreen(2, bottom_screen_left / 2, bottom_screen_top, bottom_screen_width / 2, + bottom_screen_height, orientation); + draw_info.layer = 1; + DrawSingleScreen(2, static_cast((bottom_screen_left / 2) + (layout.width / 2)), + bottom_screen_top, bottom_screen_width / 2, bottom_screen_height, + orientation); + break; + } + case Settings::StereoRenderOption::SideBySideFull: { + DrawSingleScreen(2, bottom_screen_left, bottom_screen_top, bottom_screen_width, + bottom_screen_height, orientation); + draw_info.layer = 1; + DrawSingleScreen(2, bottom_screen_left + layout.width / 2, bottom_screen_top, + bottom_screen_width, bottom_screen_height, orientation); break; } case Settings::StereoRenderOption::CardboardVR: { @@ -851,13 +851,8 @@ void RendererVulkan::DrawBottomScreen(const Layout::FramebufferLayout& layout, case Settings::StereoRenderOption::Anaglyph: case Settings::StereoRenderOption::Interlaced: case Settings::StereoRenderOption::ReverseInterlaced: { - if (separate_win) { - DrawSingleScreen(2, bottom_screen_left, bottom_screen_top, bottom_screen_width, - bottom_screen_height, orientation); - } else { - DrawSingleScreenStereo(2, 2, bottom_screen_left, bottom_screen_top, bottom_screen_width, - bottom_screen_height, orientation); - } + DrawSingleScreenStereo(2, 2, bottom_screen_left, bottom_screen_top, bottom_screen_width, + bottom_screen_height, orientation); break; } } @@ -871,7 +866,7 @@ void RendererVulkan::DrawScreens(Frame* frame, const Layout::FramebufferLayout& clear_color.float32[2] = Settings::values.bg_blue.GetValue(); } if (settings.shader_update_requested.exchange(false)) { - ReloadPipeline(); + ReloadPipeline(layout.render_3d_mode); } PrepareDraw(frame, layout); diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h index 15807a4f3..29ed8a66e 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.h +++ b/src/video_core/renderer_vulkan/renderer_vulkan.h @@ -80,7 +80,7 @@ public: void TryPresent(int timeout_ms, bool is_secondary) override {} private: - void ReloadPipeline(); + void ReloadPipeline(Settings::StereoRenderOption render_3d); void CompileShaders(); void BuildLayouts(); void BuildPipelines();